From bbbfddd6cbd09616aa3336480f0601744832f698 Mon Sep 17 00:00:00 2001 From: Kamigen <46357922+Edouard127@users.noreply.github.com> Date: Wed, 8 May 2024 19:04:25 -0400 Subject: [PATCH] Feature: Google oauth --- src/Infrastructure/Services/UserService.cs | 70 ++++++++++++++++------ src/Web/Controllers/GoogleController.cs | 54 +++++++++++++++++ src/Web/Endpoints/Google.cs | 26 -------- src/Web/Program.cs | 43 +++++++------ src/Web/wwwroot/api/specification.json | 34 ++++------- 5 files changed, 143 insertions(+), 84 deletions(-) create mode 100644 src/Web/Controllers/GoogleController.cs delete mode 100644 src/Web/Endpoints/Google.cs diff --git a/src/Infrastructure/Services/UserService.cs b/src/Infrastructure/Services/UserService.cs index b45e3b3..12910a4 100644 --- a/src/Infrastructure/Services/UserService.cs +++ b/src/Infrastructure/Services/UserService.cs @@ -32,7 +32,7 @@ public class UserService(UserManager 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 FindUserByIdAsync(string id) @@ -82,23 +82,55 @@ public class UserService(UserManager 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]) + } +} diff --git a/src/Web/Controllers/GoogleController.cs b/src/Web/Controllers/GoogleController.cs new file mode 100644 index 0000000..173413a --- /dev/null +++ b/src/Web/Controllers/GoogleController.cs @@ -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 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 + { + new(ClaimTypes.Name, userInfo.Name), + new(ClaimTypes.Email, userInfo.Email), + new(ClaimTypes.GivenName, userInfo.GivenName), + new(ClaimTypes.Surname, userInfo.FamilyName) + }, CookieAuthenticationDefaults.AuthenticationScheme))); + + return Redirect("/"); + } +} diff --git a/src/Web/Endpoints/Google.cs b/src/Web/Endpoints/Google.cs deleted file mode 100644 index 0a9a92d..0000000 --- a/src/Web/Endpoints/Google.cs +++ /dev/null @@ -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 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); - } -} diff --git a/src/Web/Program.cs b/src/Web/Program.cs index b6c6745..5407839 100644 --- a/src/Web/Program.cs +++ b/src/Web/Program.cs @@ -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(options => + { + options.Password.RequireDigit = true; + options.Password.RequireLowercase = false; + options.Password.RequireUppercase = true; + options.Password.RequireNonAlphanumeric = true; + options.Password.RequiredLength = 8; + }) + .AddEntityFrameworkStores() + .AddDefaultTokenProviders(); + builder.Services.AddControllers(); builder.Services.AddScoped(); var app = builder.Build(); +app.UseForwardedHeaders( + new ForwardedHeadersOptions { ForwardedHeaders = ForwardedHeaders.XForwardedProto } +); + app.UseAuthentication(); app.UseAuthorization(); diff --git a/src/Web/wwwroot/api/specification.json b/src/Web/wwwroot/api/specification.json index 79be20e..fcb4d9f 100644 --- a/src/Web/wwwroot/api/specification.json +++ b/src/Web/wwwroot/api/specification.json @@ -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": {