Feature: Google oauth

This commit is contained in:
Kamigen
2024-05-08 19:04:25 -04:00
parent cd2bf64af5
commit bbbfddd6cb
5 changed files with 143 additions and 84 deletions

View File

@@ -32,7 +32,7 @@ public class UserService(UserManager<ApplicationUser> userManager) : IUserServic
public async Task CreateUserAsync(Userinfo userInfo)
{
await CreateUserAsync(userInfo.Email, userInfo.GivenName, userInfo.GivenName, userInfo.FamilyName, GeneratePassword(24));
await CreateUserAsync(userInfo.Email, userInfo.GivenName, userInfo.GivenName, userInfo.FamilyName, RandomGenerator.RandomString(24));
}
public async Task<UserModel?> FindUserByIdAsync(string id)
@@ -82,23 +82,55 @@ public class UserService(UserManager<ApplicationUser> userManager) : IUserServic
return userModel;
}
private const string Characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
private const string SpecialCharacters = "!@#$%^&*()_+";
private String GeneratePassword(int length)
{
// Using a string builder has additional overhead, maybe we can find something else
var password = new StringBuilder();
for (var i = 0; i < length; i++)
{
password.Append(Characters[_random.Next(Characters.Length)]);
}
password.Append(SpecialCharacters[_random.Next(SpecialCharacters.Length)]);
return password.ToString();
}
}
public class RandomGenerator
{
private const string LetterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
+ "0123456789"
+ "!@#$%^&*()_+"
+ "-=[];',./`~{}|:\"<>?";
private const int LetterIdxBits = 6;
private const int LetterIdxMask = 1 << LetterIdxBits;
private const int LetterIdxMax = 64 / LetterIdxBits;
private static readonly Random Src = new();
public static byte[] RandBytesMaskSrc(int n)
{
var b = new byte[n];
for (var i = n - 1; i >= 0;)
{
long cache = Src.NextInt64();
int remain = LetterIdxMax;
while (remain != 0)
{
if (i < 0)
break;
if (cache == 0)
cache = Src.NextInt64();
var idx = (int)(cache & LetterIdxMask);
if (idx < LetterBytes.Length)
{
b[i] = (byte)LetterBytes[idx];
i--;
}
cache >>= LetterIdxBits;
remain--;
}
}
return b;
}
public static string RandomString(int length)
{
var bytes = RandBytesMaskSrc(length);
return Encoding.UTF8.GetString(bytes); // Equivalent for *(string*)(&bytes[0])
}
}

View File

@@ -0,0 +1,54 @@
using System.Security.Claims;
using Google.Apis.Oauth2.v2.Data;
using Hutopy.Domain.Interfaces;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.Google;
using Microsoft.AspNetCore.Mvc;
namespace Hutopy.Web.Controllers;
public class GoogleController(
IUserService userService) : Controller
{
[HttpGet("/api/google/sign-in")]
public async Task SignIn()
{
await HttpContext.ChallengeAsync(GoogleDefaults.AuthenticationScheme, new AuthenticationProperties
{
RedirectUri = Url.Action("Callback"),
});
}
public async Task<IActionResult> Callback()
{
var authenticateResult = await HttpContext.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme);
if (!authenticateResult.Succeeded)
{
return BadRequest();
}
var claims = authenticateResult.Principal.Claims.ToList();
var userInfo = new Userinfo
{
Name = claims.FirstOrDefault(c => c.Type == ClaimTypes.Name)?.Value,
Email = claims.FirstOrDefault(c => c.Type == ClaimTypes.Email)?.Value,
GivenName = claims.FirstOrDefault(c => c.Type == ClaimTypes.GivenName)?.Value,
FamilyName = claims.FirstOrDefault(c => c.Type == ClaimTypes.Surname)?.Value
};
await userService.CreateUserAsync(userInfo); // TODO: Don't create user if already exists
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(new ClaimsIdentity(new List<Claim>
{
new(ClaimTypes.Name, userInfo.Name),
new(ClaimTypes.Email, userInfo.Email),
new(ClaimTypes.GivenName, userInfo.GivenName),
new(ClaimTypes.Surname, userInfo.FamilyName)
}, CookieAuthenticationDefaults.AuthenticationScheme)));
return Redirect("/");
}
}

View File

@@ -1,26 +0,0 @@
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Google;
using Microsoft.AspNetCore.Mvc;
namespace Hutopy.Web.Endpoints;
public class Google : EndpointGroupBase
{
public override void Map(WebApplication app)
{
app.MapGroup(this)
.MapGet("/o/sign-in", Callback);
}
private static async Task<IActionResult> Callback(ISender sender, HttpContext context)
{
var properties = new AuthenticationProperties
{
RedirectUri = "/signin-google", ExpiresUtc = DateTimeOffset.UtcNow.AddDays(30),
};
await context.ChallengeAsync(GoogleDefaults.AuthenticationScheme, properties);
return new ChallengeResult(GoogleDefaults.AuthenticationScheme, properties);
}
}

View File

@@ -5,7 +5,11 @@ using Hutopy.Infrastructure.Data;
using Hutopy.Infrastructure.Services;
using Hutopy.Web;
using Azure.Identity;
using Hutopy.Infrastructure.Identity;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.Google;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.Identity;
var builder = WebApplication.CreateBuilder(args);
@@ -49,40 +53,43 @@ builder.Services.AddInfrastructureServices(builder.Configuration);
builder.Services.AddWebServices();
// OAuth
builder.Services.AddAuthorization();
builder.Services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = GoogleDefaults.AuthenticationScheme;
})
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
{
options.Cookie.Name = "Hutopy";
options.Cookie.SecurePolicy =
builder.Environment.IsDevelopment() ? CookieSecurePolicy.None : CookieSecurePolicy.Always;
options.Cookie.SameSite = SameSiteMode.Strict;
options.Cookie.HttpOnly = true;
options.Cookie.IsEssential = true;
options.Cookie.MaxAge = TimeSpan.FromDays(30);
})
.AddGoogle(options =>
.AddCookie()
.AddGoogle(
GoogleDefaults.AuthenticationScheme,
options =>
{
options.ClientId = builder.Configuration["Google:ClientId"] ??
throw new ArgumentNullException("The Google ClientId is missing.");
options.ClientSecret = builder.Configuration["Google:ClientSecret"] ??
throw new ArgumentNullException("The Google ClientSecret is missing.");
options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.Events.OnRedirectToAuthorizationEndpoint = context =>
{
context.Response.Redirect(context.RedirectUri + "&prompt=consent");
return Task.CompletedTask;
};
});
// Password hashing
builder.Services.AddIdentity<ApplicationUser, IdentityRole>(options =>
{
options.Password.RequireDigit = true;
options.Password.RequireLowercase = false;
options.Password.RequireUppercase = true;
options.Password.RequireNonAlphanumeric = true;
options.Password.RequiredLength = 8;
})
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
builder.Services.AddControllers();
builder.Services.AddScoped<IUserService, UserService>();
var app = builder.Build();
app.UseForwardedHeaders(
new ForwardedHeadersOptions { ForwardedHeaders = ForwardedHeaders.XForwardedProto }
);
app.UseAuthentication();
app.UseAuthorization();

View File

@@ -26,27 +26,6 @@
}
}
},
"/api/Google/o/sign-in": {
"get": {
"tags": [
"Google"
],
"operationId": "GetApiGoogleOSignIn",
"responses": {
"200": {
"description": "",
"content": {
"application/octet-stream": {
"schema": {
"type": "string",
"format": "binary"
}
}
}
}
}
}
},
"/api/JoinUs": {
"get": {
"tags": [
@@ -628,6 +607,19 @@
}
]
}
},
"/api/google/sign-in": {
"get": {
"tags": [
"Google"
],
"operationId": "Google_SignIn",
"responses": {
"200": {
"description": ""
}
}
}
}
},
"components": {