From 6f76cb208474b8a6deebf4698e19d17a9f5daac3 Mon Sep 17 00:00:00 2001 From: Dominic Villemure Date: Sun, 9 Jun 2024 23:44:37 -0400 Subject: [PATCH] #oauth changed GoogleController for the jwt flow ( using a common token if we connect from our app or from google ) --- Directory.Packages.props | 1 + .../Common/Interfaces/IIdentityService.cs | 5 +- src/Application/Users/Commands/Login.cs | 27 + src/Infrastructure/DependencyInjection.cs | 17 +- .../Identity/IdentityService.cs | 25 +- src/Infrastructure/Services/UserService.cs | 134 ---- src/Infrastructure/Utils/GenerateJwtToken.cs | 31 + src/Infrastructure/Utils/RandomGenerator.cs | 58 ++ src/Web/Controllers/FacebookController.cs | 18 +- src/Web/Controllers/GoogleController.cs | 85 ++- src/Web/DependencyInjection.cs | 51 +- src/Web/Endpoints/Users.cs | 9 +- src/Web/Program.cs | 56 +- src/Web/Web.csproj | 1 + src/Web/appsettings.Development.dist | 30 +- src/Web/wwwroot/api/specification.json | 632 ++---------------- 16 files changed, 338 insertions(+), 842 deletions(-) create mode 100644 src/Application/Users/Commands/Login.cs delete mode 100644 src/Infrastructure/Services/UserService.cs create mode 100644 src/Infrastructure/Utils/GenerateJwtToken.cs create mode 100644 src/Infrastructure/Utils/RandomGenerator.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index b8593ab..f4732ee 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -16,6 +16,7 @@ + diff --git a/src/Application/Common/Interfaces/IIdentityService.cs b/src/Application/Common/Interfaces/IIdentityService.cs index 5451d37..cd97117 100644 --- a/src/Application/Common/Interfaces/IIdentityService.cs +++ b/src/Application/Common/Interfaces/IIdentityService.cs @@ -10,15 +10,12 @@ public interface IIdentityService Task FindUserByIdAsync(string id); Task GetCurrentUserAsync(); Task FindUserByEmailAsync(string id); + Task LoginAsync(string email, string password); Task GetUserByUserNameAsync(string userName); Task IsInRoleAsync(string userId, string role); Task AuthorizeAsync(string userId, string policyName); Task AddRoleAsync(string userId, string role); Task> GetCurrentUserRolesAsync(); - - Task<(Result Result, string UserId)> CreateUserAsync(string userName, string password); - Task<(Result Result, string UserId)> CreateUserAsync(Userinfo userInfo); - Task DeleteUserAsync(string userId); } diff --git a/src/Application/Users/Commands/Login.cs b/src/Application/Users/Commands/Login.cs new file mode 100644 index 0000000..f61f810 --- /dev/null +++ b/src/Application/Users/Commands/Login.cs @@ -0,0 +1,27 @@ +using Hutopy.Application.Common.Interfaces; + +namespace Hutopy.Application.Users.Commands; +public record LoginCommand : IRequest +{ + public required string EmailAddress { get; init; } + public required string Password { get; init; } +} + +public class LoginCommandHandler : IRequestHandler +{ + private readonly IApplicationDbContext _context; + private readonly IIdentityService _identityService; + + public LoginCommandHandler(IApplicationDbContext context, IIdentityService identityService) + { + _context = context; + _identityService = identityService; + } + + public async Task Handle(LoginCommand request, CancellationToken cancellationToken) + { + var jwt = await _identityService.LoginAsync(request.EmailAddress, request.Password); + + return jwt ?? "Invalid login credentials"; + } +} diff --git a/src/Infrastructure/DependencyInjection.cs b/src/Infrastructure/DependencyInjection.cs index 31b0b30..42ca0c3 100644 --- a/src/Infrastructure/DependencyInjection.cs +++ b/src/Infrastructure/DependencyInjection.cs @@ -48,13 +48,20 @@ public static class DependencyInjection .AddBearerToken(IdentityConstants.BearerScheme); services.AddAuthorizationBuilder(); - - // Might need to change and use AddIdentity() when we need to integrate connection via third party ( facebook, google ) - services - .AddIdentityCore() + + services.AddIdentityCore(options => + { + options.Password.RequireDigit = false; + options.Password.RequireLowercase = false; + options.Password.RequireUppercase = false; + options.Password.RequireNonAlphanumeric = false; + options.Password.RequiredLength = 8; + }) .AddRoles() .AddEntityFrameworkStores() - .AddApiEndpoints(); + .AddApiEndpoints() + .AddSignInManager>() + .AddDefaultTokenProviders(); services.AddSingleton(TimeProvider.System); services.AddScoped(); diff --git a/src/Infrastructure/Identity/IdentityService.cs b/src/Infrastructure/Identity/IdentityService.cs index b7f7d77..0a25594 100644 --- a/src/Infrastructure/Identity/IdentityService.cs +++ b/src/Infrastructure/Identity/IdentityService.cs @@ -2,17 +2,21 @@ using Google.Apis.Oauth2.v2.Data; using System.Security.Claims; using Hutopy.Application.Common.Interfaces; using Hutopy.Application.Common.Models; +using Hutopy.Infrastructure.Utils; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Configuration; namespace Hutopy.Infrastructure.Identity; public class IdentityService( UserManager userManager, + SignInManager signInManager, IUserClaimsPrincipalFactory userClaimsPrincipalFactory, IAuthorizationService authorizationService, - IHttpContextAccessor contextAccessor + IHttpContextAccessor contextAccessor, + IConfiguration configuration ) : IIdentityService { @@ -205,4 +209,23 @@ public class IdentityService( return userRoles; } + + public async Task LoginAsync(string userName, string password) + { + var result = await signInManager.PasswordSignInAsync(userName, password, isPersistent: false, lockoutOnFailure: false); + + if (!result.Succeeded) + { + return null; + } + + var user = await GetUserByUserNameAsync(userName); + var token = JwtTokenHelper.GenerateJwtToken( + issuer: configuration["Jwt:Issuer"] ?? "", + audience: configuration["Jwt:Audience"] ?? "", + key: configuration["Jwt:Key"] ?? "", + userId: user?.Id ?? ""); + + return token; + } } diff --git a/src/Infrastructure/Services/UserService.cs b/src/Infrastructure/Services/UserService.cs deleted file mode 100644 index 4e52033..0000000 --- a/src/Infrastructure/Services/UserService.cs +++ /dev/null @@ -1,134 +0,0 @@ -using System.Text; -using System.Security.Claims; -using Hutopy.Domain.Interfaces; -using Hutopy.Domain.Models; -using Hutopy.Infrastructure.Identity; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Identity; - -namespace Hutopy.Infrastructure.Services; - -public class UserService(UserManager userManager, IHttpContextAccessor contextAccessor) : IUserService -{ - public async Task CreateUserAsync(string email, string userName, string firstName, string lastName, string password) - { - var applicationUser = new ApplicationUser - { - UserName = userName, - Email = email, - FirstName = firstName, - LastName = lastName - }; - - //todo: Need to handle errors better for the user. - var response = await userManager.CreateAsync(applicationUser, password); - - if (response.Errors.Any()) - { - throw new Exception("Failed to create user", new AggregateException(response.Errors.Select(e => new Exception(e.Description)))); - } - } - - public async Task FindUserByIdAsync(string id) - { - var response = await userManager.FindByIdAsync(id); - - if (response == null) return null; - - var userModel = new UserModel - { - Id = response.Id, - UserName = response.UserName, - FirstName = response.FirstName, - LastName = response.LastName, - Email = response.Email, - }; - - return userModel; - } - - public async Task GetCurrentUserAsync() - { - // todo: Get the id of the user doing the request. - var currentUserId = contextAccessor.HttpContext?.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value; - if (string.IsNullOrEmpty(currentUserId)) - { - return null; - } - - return await FindUserByIdAsync(currentUserId); - } - - public async Task FindUserByEmailAsync(string email) - { - var response = await userManager.FindByEmailAsync(email); - - if (response == null) return null; - - var userModel = new UserModel - { - Id = response.Id, - UserName = response.UserName, - FirstName = response.FirstName, - LastName = response.LastName, - Email = response.Email - }; - - return userModel; - } -} - -// If we need to add special characters we can alternate between 2 pools. -public class RandomGenerator -{ - // For the moment, numbers and special characters don't work because - // the random generator is designed to handle a single integer. - // We can modify this in the future. - 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/Infrastructure/Utils/GenerateJwtToken.cs b/src/Infrastructure/Utils/GenerateJwtToken.cs new file mode 100644 index 0000000..9f01d6a --- /dev/null +++ b/src/Infrastructure/Utils/GenerateJwtToken.cs @@ -0,0 +1,31 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; +using Microsoft.IdentityModel.Tokens; + +namespace Hutopy.Infrastructure.Utils; + +public static class JwtTokenHelper +{ + public static string GenerateJwtToken(string issuer, string audience, string key, string userId) + { + var claims = new[] + { + new Claim(JwtRegisteredClaimNames.Sub, userId), + new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), + new Claim(ClaimTypes.NameIdentifier, userId) + }; + + var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(key)); + var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256); + + var token = new JwtSecurityToken( + issuer: issuer, + audience: audience, + claims: claims, + expires: DateTime.Now.AddMinutes(30), + signingCredentials: credentials); + + return new JwtSecurityTokenHandler().WriteToken(token); + } +} diff --git a/src/Infrastructure/Utils/RandomGenerator.cs b/src/Infrastructure/Utils/RandomGenerator.cs new file mode 100644 index 0000000..1e5a9a2 --- /dev/null +++ b/src/Infrastructure/Utils/RandomGenerator.cs @@ -0,0 +1,58 @@ +using System.Text; + +namespace Hutopy.Infrastructure.Utils; + +// If we need to add special characters we can alternate between 2 pools. +public class RandomGenerator +{ + // For the moment, numbers and special characters don't work because + // the random generator is designed to handle a single integer. + // We can modify this in the future. + 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/FacebookController.cs b/src/Web/Controllers/FacebookController.cs index 394eea1..679502e 100644 --- a/src/Web/Controllers/FacebookController.cs +++ b/src/Web/Controllers/FacebookController.cs @@ -1,6 +1,6 @@ using System.Security.Claims; -using Hutopy.Domain.Interfaces; -using Hutopy.Infrastructure.Services; +using Hutopy.Application.Common.Interfaces; +using Hutopy.Infrastructure.Utils; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.Facebook; @@ -8,7 +8,7 @@ using Microsoft.AspNetCore.Mvc; namespace Hutopy.Web.Controllers; -public class FacebookController(IUserService userService) : Controller +public class FacebookController(IIdentityService identityService) : Controller { [HttpGet("/api/facebook/sign-in")] public async Task SignIn() @@ -27,10 +27,10 @@ public class FacebookController(IUserService userService) : Controller var claims = authenticateResult.Principal.Claims.ToList(); - var name = claims.FirstOrDefault(c => c.Type == ClaimTypes.Name)?.Value; - var email = claims.FirstOrDefault(c => c.Type == ClaimTypes.Email)?.Value; - var givenName = claims.FirstOrDefault(c => c.Type == ClaimTypes.GivenName)?.Value; - var familyName = claims.FirstOrDefault(c => c.Type == ClaimTypes.Surname)?.Value; + var name = claims.FirstOrDefault(c => c.Type == ClaimTypes.Name)?.Value ?? ""; + var email = claims.FirstOrDefault(c => c.Type == ClaimTypes.Email)?.Value ?? ""; + var givenName = claims.FirstOrDefault(c => c.Type == ClaimTypes.GivenName)?.Value ?? ""; + var familyName = claims.FirstOrDefault(c => c.Type == ClaimTypes.Surname)?.Value ?? ""; var claimsIdentity = new ClaimsIdentity(new List { @@ -40,13 +40,13 @@ public class FacebookController(IUserService userService) : Controller new(ClaimTypes.Surname, familyName) }, CookieAuthenticationDefaults.AuthenticationScheme); - if (await userService.FindUserByEmailAsync(email) != null) + if (await identityService.FindUserByEmailAsync(email) != null) { await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(claimsIdentity)); return Redirect("/"); } - await userService.CreateUserAsync(email, givenName, givenName, familyName, RandomGenerator.RandomString(24)); + await identityService.CreateUserAsync(email, givenName, givenName, familyName, RandomGenerator.RandomString(24)); await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(claimsIdentity)); return Redirect("/"); } diff --git a/src/Web/Controllers/GoogleController.cs b/src/Web/Controllers/GoogleController.cs index 0780277..1f5228e 100644 --- a/src/Web/Controllers/GoogleController.cs +++ b/src/Web/Controllers/GoogleController.cs @@ -1,53 +1,70 @@ using System.Security.Claims; -using Hutopy.Domain.Interfaces; -using Hutopy.Infrastructure.Services; +using Hutopy.Application.Common.Interfaces; +using Hutopy.Infrastructure.Utils; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; -using Microsoft.AspNetCore.Authentication.Google; using Microsoft.AspNetCore.Mvc; +using Newtonsoft.Json.Linq; namespace Hutopy.Web.Controllers; -public class GoogleController(IUserService userService) : Controller +public class GoogleController(IIdentityService identityService, IHttpClientFactory httpClientFactory) : Controller { - [HttpGet("/api/google/sign-in")] - public async Task SignIn() + [HttpPost("/api/google/sign-in")] + public async Task SignIn([FromBody] GoogleSignInRequest request) { - await HttpContext.ChallengeAsync(GoogleDefaults.AuthenticationScheme, new AuthenticationProperties + var httpClient = httpClientFactory.CreateClient(); + // Verify the token with Google + var response = await httpClient.GetAsync($"https://www.googleapis.com/oauth2/v1/userinfo?access_token={request.AccessToken}"); + if (!response.IsSuccessStatusCode) { - RedirectUri = Url.Action("Authorize") - }); - } - - public async Task Authorize() - { - var authenticateResult = await HttpContext.AuthenticateAsync(GoogleDefaults.AuthenticationScheme); + return BadRequest("Invalid Google token."); + } + + var payload = JObject.Parse(await response.Content.ReadAsStringAsync()); + + var email = payload["email"]?.ToString() ?? ""; + var name = payload["name"]?.ToString() ?? ""; + var givenName = payload["given_name"]?.ToString() ?? ""; + var familyName = payload["family_name"]?.ToString() ?? ""; + + if (string.IsNullOrEmpty(email)) + { + return BadRequest("Google token did not contain an email."); + } + + // Check if user exists or create a new one + var user = await identityService.FindUserByEmailAsync(email); + if (user == null) + { + await identityService.CreateUserAsync(email, email, givenName, familyName, RandomGenerator.RandomString(24)); + user = await identityService.FindUserByEmailAsync(email); + } - if (!authenticateResult.Succeeded) return BadRequest(); - - var claims = authenticateResult.Principal.Claims.ToList(); - - var name = claims.FirstOrDefault(c => c.Type == ClaimTypes.Name)?.Value; - var email = claims.FirstOrDefault(c => c.Type == ClaimTypes.Email)?.Value; - var givenName = claims.FirstOrDefault(c => c.Type == ClaimTypes.GivenName)?.Value; - var familyName = claims.FirstOrDefault(c => c.Type == ClaimTypes.Surname)?.Value; - - var claimsIdentity = new ClaimsIdentity(new List + if (user is null) + { + return BadRequest("Unable to find or create the user."); + } + + // Sign in the user + var claims = new List { new(ClaimTypes.Name, name), new(ClaimTypes.Email, email), new(ClaimTypes.GivenName, givenName), new(ClaimTypes.Surname, familyName) - }, CookieAuthenticationDefaults.AuthenticationScheme); - - if (await userService.FindUserByEmailAsync(email) != null) - { - await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(claimsIdentity)); - return Redirect("/"); - } - - await userService.CreateUserAsync(email, givenName, givenName, familyName, RandomGenerator.RandomString(24)); + }; + + var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme); await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(claimsIdentity)); - return Redirect("/"); + + var jwtToken = JwtTokenHelper.GenerateJwtToken("https://hutopy.com", "Hutopy", "V3J3bWFuUml3ZVpQbmxlWmZhWEo3ZkJSZ01YbHBwS24=", user.Id!); + + return Ok(new { accessToken = jwtToken, email }); + } + + public class GoogleSignInRequest + { + public required string AccessToken { get; set; } } } diff --git a/src/Web/DependencyInjection.cs b/src/Web/DependencyInjection.cs index f8ede38..a921805 100644 --- a/src/Web/DependencyInjection.cs +++ b/src/Web/DependencyInjection.cs @@ -1,8 +1,14 @@ -using Azure.Identity; +using System.Text; +using Azure.Identity; using Hutopy.Application.Common.Interfaces; using Hutopy.Infrastructure.Data; using Hutopy.Web.Services; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authentication.Facebook; +using Microsoft.AspNetCore.Authentication.Google; +using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Mvc; +using Microsoft.IdentityModel.Tokens; using NSwag; using NSwag.Generation.Processors.Security; @@ -24,6 +30,8 @@ public static class DependencyInjection services.AddExceptionHandler(); services.AddRazorPages(); + + services.AddHttpClient(); // Customise default API behaviour services.Configure(options => @@ -62,4 +70,45 @@ public static class DependencyInjection return services; } + + public static IServiceCollection AddAuthorizationAndAuthentication(this IServiceCollection services, ConfigurationManager configuration) + { + services.AddAuthentication(options => + { + options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme; + }) + .AddCookie("Identity.Application", options => + { + options.LoginPath = "/api/Users/login"; + }) + .AddCookie() + .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, jwtBearerOptions => + { + jwtBearerOptions.Authority = "https://hutopy.com"; + jwtBearerOptions.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidIssuer = configuration["Jwt:Issuer"], + ValidateAudience = true, + ValidAudience = configuration["Jwt:Audience"], + ValidateLifetime = true, + IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["Jwt:Key"] ?? "")) + }; + }) + .AddGoogle(GoogleDefaults.AuthenticationScheme, options => + { + options.ClientId = configuration["Google:ClientId"] ?? ""; + options.ClientSecret = configuration["Google:ClientSecret"] ?? ""; + }) + .AddFacebook(FacebookDefaults.AuthenticationScheme, options => + { + options.ClientId = configuration["Facebook:ClientId"] ?? + throw new ArgumentNullException("The Facebook ClientId is missing."); + options.ClientSecret = configuration["Facebook:ClientSecret"] ?? + throw new ArgumentNullException("The Facebook ClientSecret is missing."); + }); + + return services; + } } diff --git a/src/Web/Endpoints/Users.cs b/src/Web/Endpoints/Users.cs index 32b8acc..41b558d 100644 --- a/src/Web/Endpoints/Users.cs +++ b/src/Web/Endpoints/Users.cs @@ -10,8 +10,8 @@ public class Users : EndpointGroupBase { app.MapGroup(this) .MapPost(CreateUser) - .MapGet(GetMinimalUser) - .MapIdentityApi(); + .MapPost(Login, "/login") + .MapGet(GetMinimalUser); } private static async Task CreateUser(ISender sender, CreateUserCommand command) @@ -23,4 +23,9 @@ public class Users : EndpointGroupBase { return await sender.Send(query); } + + private static async Task Login(ISender sender, LoginCommand command) + { + return await sender.Send(command); + } } diff --git a/src/Web/Program.cs b/src/Web/Program.cs index 14bda9f..1246d0d 100644 --- a/src/Web/Program.cs +++ b/src/Web/Program.cs @@ -3,12 +3,7 @@ using Hutopy.Infrastructure; using Hutopy.Infrastructure.Data; using Hutopy.Web; using Azure.Identity; -using Hutopy.Infrastructure.Identity; -using Microsoft.AspNetCore.Authentication.Cookies; -using Microsoft.AspNetCore.Authentication.Facebook; -using Microsoft.AspNetCore.Authentication.Google; using Microsoft.AspNetCore.HttpOverrides; -using Microsoft.AspNetCore.Identity; var builder = WebApplication.CreateBuilder(args); @@ -50,49 +45,8 @@ builder.Services.AddKeyVaultIfConfigured(builder.Configuration); builder.Services.AddApplicationServices(); builder.Services.AddInfrastructureServices(builder.Configuration); builder.Services.AddWebServices(); - -// OAuth -builder.Services.AddAuthentication(options => - { - options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; - options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme; - }) - .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.AccessDeniedPath = "/AccessDeniedPathInfo"; - }) - .AddFacebook( - FacebookDefaults.AuthenticationScheme, - options => - { - options.ClientId = builder.Configuration["Facebook:ClientId"] ?? - throw new ArgumentNullException("The Facebook ClientId is missing."); - options.ClientSecret = builder.Configuration["Facebook:ClientSecret"] ?? - throw new ArgumentNullException("The Facebook ClientSecret is missing."); - //options.AccessDeniedPath = "/AccessDeniedPathInfo"; - }); - -// Password hashing -builder.Services.AddIdentity(options => - { - options.Password.RequireDigit = false; - options.Password.RequireLowercase = false; - options.Password.RequireUppercase = false; - options.Password.RequireNonAlphanumeric = false; - options.Password.RequiredLength = 16; - }) - .AddEntityFrameworkStores() - .AddDefaultTokenProviders(); - +builder.Services.AddAuthorizationAndAuthentication(builder.Configuration); builder.Services.AddControllers(); -builder.Services.AddScoped(); var app = builder.Build(); @@ -100,13 +54,15 @@ app.UseForwardedHeaders( new ForwardedHeadersOptions { ForwardedHeaders = ForwardedHeaders.XForwardedProto } ); -app.UseAuthentication(); -app.UseAuthorization(); - app.UseCors("AllowAll"); app.UseCors("AllowHutopyUi"); app.UseCors("AllowHutopyUiPreview"); +app.UseAuthentication(); +app.UseAuthorization(); + + + // Initialize and seed the db. await app.InitialiseDatabaseAsync(); diff --git a/src/Web/Web.csproj b/src/Web/Web.csproj index d5e0c62..9193242 100644 --- a/src/Web/Web.csproj +++ b/src/Web/Web.csproj @@ -16,6 +16,7 @@ + diff --git a/src/Web/appsettings.Development.dist b/src/Web/appsettings.Development.dist index 58d6716..d25bfda 100644 --- a/src/Web/appsettings.Development.dist +++ b/src/Web/appsettings.Development.dist @@ -9,21 +9,15 @@ }, "Google": { "ClientId": "", - "ClientSecret": "", - "ProjectId": "", - "AuthUri": "", - "TokenUri": "", - "AuthProviderX509CertUrl": "", - "RedirectUris": [ - "https://hutopy.ca", - "https://hutopy.com", - "http://localhost" - ], - "JavascriptOrigins": [ - "https://hutopy.ca", - "https://hutopy.com", - "http://localhost" - ] - } -} - + "ClientSecret": "" + }, + "Facebook": { + "ClientId": "", + "ClientSecret": "" + }, + "Jwt": { + "Issuer": "", + "Audience": "", + "Key": "" + } +} \ No newline at end of file diff --git a/src/Web/wwwroot/api/specification.json b/src/Web/wwwroot/api/specification.json index 2a98d65..5b73a58 100644 --- a/src/Web/wwwroot/api/specification.json +++ b/src/Web/wwwroot/api/specification.json @@ -273,75 +273,22 @@ } } }, - "/api/Users/register": { - "post": { - "tags": [ - "Users" - ], - "operationId": "PostApiUsersRegister", - "requestBody": { - "x-name": "registration", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RegisterRequest" - } - } - }, - "x-position": 1 - }, - "responses": { - "200": { - "description": "" - }, - "400": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HttpValidationProblemDetails" - } - } - } - } - } - } - }, "/api/Users/login": { "post": { "tags": [ "Users" ], - "operationId": "PostApiUsersLogin", - "parameters": [ - { - "name": "useCookies", - "in": "query", - "schema": { - "type": "boolean", - "nullable": true - }, - "x-position": 2 - }, - { - "name": "useSessionCookies", - "in": "query", - "schema": { - "type": "boolean", - "nullable": true - }, - "x-position": 3 - } - ], + "operationId": "Login", "requestBody": { - "x-name": "login", + "x-name": "command", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/LoginRequest" + "$ref": "#/components/schemas/LoginCommand" } } }, + "required": true, "x-position": 1 }, "responses": { @@ -350,7 +297,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AccessTokenResponse" + "type": "string" } } } @@ -358,305 +305,6 @@ } } }, - "/api/Users/refresh": { - "post": { - "tags": [ - "Users" - ], - "operationId": "PostApiUsersRefresh", - "requestBody": { - "x-name": "refreshRequest", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RefreshRequest" - } - } - }, - "x-position": 1 - }, - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AccessTokenResponse" - } - } - } - } - } - } - }, - "/api/Users/confirmEmail": { - "get": { - "tags": [ - "Users" - ], - "operationId": "GetApiUsersConfirmEmail", - "parameters": [ - { - "name": "userId", - "in": "query", - "schema": { - "type": "string", - "nullable": true - }, - "x-position": 1 - }, - { - "name": "code", - "in": "query", - "schema": { - "type": "string", - "nullable": true - }, - "x-position": 2 - }, - { - "name": "changedEmail", - "in": "query", - "schema": { - "type": "string", - "nullable": true - }, - "x-position": 3 - } - ], - "responses": { - "200": { - "description": "" - } - } - } - }, - "/api/Users/resendConfirmationEmail": { - "post": { - "tags": [ - "Users" - ], - "operationId": "PostApiUsersResendConfirmationEmail", - "requestBody": { - "x-name": "resendRequest", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ResendConfirmationEmailRequest" - } - } - }, - "x-position": 1 - }, - "responses": { - "200": { - "description": "" - } - } - } - }, - "/api/Users/forgotPassword": { - "post": { - "tags": [ - "Users" - ], - "operationId": "PostApiUsersForgotPassword", - "requestBody": { - "x-name": "resetRequest", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ForgotPasswordRequest" - } - } - }, - "x-position": 1 - }, - "responses": { - "200": { - "description": "" - }, - "400": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HttpValidationProblemDetails" - } - } - } - } - } - } - }, - "/api/Users/resetPassword": { - "post": { - "tags": [ - "Users" - ], - "operationId": "PostApiUsersResetPassword", - "requestBody": { - "x-name": "resetRequest", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ResetPasswordRequest" - } - } - }, - "x-position": 1 - }, - "responses": { - "200": { - "description": "" - }, - "400": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HttpValidationProblemDetails" - } - } - } - } - } - } - }, - "/api/Users/manage/2fa": { - "post": { - "tags": [ - "Users" - ], - "operationId": "PostApiUsersManage2fa", - "requestBody": { - "x-name": "tfaRequest", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TwoFactorRequest" - } - } - }, - "x-position": 1 - }, - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TwoFactorResponse" - } - } - } - }, - "400": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HttpValidationProblemDetails" - } - } - } - }, - "404": { - "description": "" - } - }, - "security": [ - { - "JWT": [] - } - ] - } - }, - "/api/Users/manage/info": { - "get": { - "tags": [ - "Users" - ], - "operationId": "GetApiUsersManageInfo", - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InfoResponse" - } - } - } - }, - "400": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HttpValidationProblemDetails" - } - } - } - }, - "404": { - "description": "" - } - }, - "security": [ - { - "JWT": [] - } - ] - }, - "post": { - "tags": [ - "Users" - ], - "operationId": "PostApiUsersManageInfo", - "requestBody": { - "x-name": "infoRequest", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InfoRequest" - } - } - }, - "x-position": 1 - }, - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InfoResponse" - } - } - } - }, - "400": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HttpValidationProblemDetails" - } - } - } - }, - "404": { - "description": "" - } - }, - "security": [ - { - "JWT": [] - } - ] - } - }, "/api/WeatherForecasts": { "get": { "tags": [ @@ -699,14 +347,34 @@ } }, "/api/google/sign-in": { - "get": { + "post": { "tags": [ "Google" ], "operationId": "Google_SignIn", + "requestBody": { + "x-name": "request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GoogleSignInRequest" + } + } + }, + "required": true, + "x-position": 1 + }, "responses": { "200": { - "description": "" + "description": "", + "content": { + "application/octet-stream": { + "schema": { + "type": "string", + "format": "binary" + } + } + } } } } @@ -986,6 +654,18 @@ } } }, + "LoginCommand": { + "type": "object", + "additionalProperties": false, + "properties": { + "emailAddress": { + "type": "string" + }, + "password": { + "type": "string" + } + } + }, "MinimalUserDto": { "type": "object", "additionalProperties": false, @@ -1001,231 +681,6 @@ } } }, - "HttpValidationProblemDetails": { - "allOf": [ - { - "$ref": "#/components/schemas/ProblemDetails" - }, - { - "type": "object", - "additionalProperties": { - "nullable": true - }, - "properties": { - "errors": { - "type": "object", - "additionalProperties": { - "type": "array", - "items": { - "type": "string" - } - } - } - } - } - ] - }, - "ProblemDetails": { - "type": "object", - "additionalProperties": { - "nullable": true - }, - "properties": { - "type": { - "type": "string", - "nullable": true - }, - "title": { - "type": "string", - "nullable": true - }, - "status": { - "type": "integer", - "format": "int32", - "nullable": true - }, - "detail": { - "type": "string", - "nullable": true - }, - "instance": { - "type": "string", - "nullable": true - } - } - }, - "RegisterRequest": { - "type": "object", - "additionalProperties": false, - "properties": { - "email": { - "type": "string" - }, - "password": { - "type": "string" - } - } - }, - "AccessTokenResponse": { - "type": "object", - "additionalProperties": false, - "properties": { - "tokenType": { - "type": "string" - }, - "accessToken": { - "type": "string" - }, - "expiresIn": { - "type": "integer", - "format": "int64" - }, - "refreshToken": { - "type": "string" - } - } - }, - "LoginRequest": { - "type": "object", - "additionalProperties": false, - "properties": { - "email": { - "type": "string" - }, - "password": { - "type": "string" - }, - "twoFactorCode": { - "type": "string", - "nullable": true - }, - "twoFactorRecoveryCode": { - "type": "string", - "nullable": true - } - } - }, - "RefreshRequest": { - "type": "object", - "additionalProperties": false, - "properties": { - "refreshToken": { - "type": "string" - } - } - }, - "ResendConfirmationEmailRequest": { - "type": "object", - "additionalProperties": false, - "properties": { - "email": { - "type": "string" - } - } - }, - "ForgotPasswordRequest": { - "type": "object", - "additionalProperties": false, - "properties": { - "email": { - "type": "string" - } - } - }, - "ResetPasswordRequest": { - "type": "object", - "additionalProperties": false, - "properties": { - "email": { - "type": "string" - }, - "resetCode": { - "type": "string" - }, - "newPassword": { - "type": "string" - } - } - }, - "TwoFactorResponse": { - "type": "object", - "additionalProperties": false, - "properties": { - "sharedKey": { - "type": "string" - }, - "recoveryCodesLeft": { - "type": "integer", - "format": "int32" - }, - "recoveryCodes": { - "type": "array", - "nullable": true, - "items": { - "type": "string" - } - }, - "isTwoFactorEnabled": { - "type": "boolean" - }, - "isMachineRemembered": { - "type": "boolean" - } - } - }, - "TwoFactorRequest": { - "type": "object", - "additionalProperties": false, - "properties": { - "enable": { - "type": "boolean", - "nullable": true - }, - "twoFactorCode": { - "type": "string", - "nullable": true - }, - "resetSharedKey": { - "type": "boolean" - }, - "resetRecoveryCodes": { - "type": "boolean" - }, - "forgetMachine": { - "type": "boolean" - } - } - }, - "InfoResponse": { - "type": "object", - "additionalProperties": false, - "properties": { - "email": { - "type": "string" - }, - "isEmailConfirmed": { - "type": "boolean" - } - } - }, - "InfoRequest": { - "type": "object", - "additionalProperties": false, - "properties": { - "newEmail": { - "type": "string", - "nullable": true - }, - "newPassword": { - "type": "string", - "nullable": true - }, - "oldPassword": { - "type": "string", - "nullable": true - } - } - }, "WeatherForecast": { "type": "object", "additionalProperties": false, @@ -1247,6 +702,15 @@ "nullable": true } } + }, + "GoogleSignInRequest": { + "type": "object", + "additionalProperties": false, + "properties": { + "accessToken": { + "type": "string" + } + } } }, "securitySchemes": {