From 6a22162a7ec0c0322be48bcf712efec917988dfc Mon Sep 17 00:00:00 2001 From: "TL\\anwarahm" Date: Sat, 31 Dec 2022 00:40:26 +0500 Subject: [PATCH 1/7] Identity and JWT authentication setup added UserEndpoints folder created --- Directory.Packages.props | 4 + .../Clean.Architecture.Core.csproj | 1 + .../Interfaces/IUserRepository.cs | 13 ++ .../RoleAggregate/Role.cs | 13 ++ .../UserAggregate/User.cs | 33 +++++ .../Clean.Architecture.Infrastructure.csproj | 1 + .../Data/AppDbContext.cs | 5 +- .../StartupSetup.cs | 2 +- .../UserRepository.cs | 24 ++++ .../Auth/Jwt/AccessToken.cs | 31 +++++ .../Auth/Jwt/IJwtService.cs | 10 ++ .../Auth/Jwt/JwtService.cs | 10 ++ .../Auth/SiteSettings.cs | 33 +++++ .../Clean.Architecture.SharedKernel.csproj | 1 + .../Utilities/IdentityExtension.cs | 36 +++++ .../Clean.Architecture.Web.csproj | 1 + .../Endpoints/UserEndpoints/Login.cs | 5 + .../Endpoints/UserEndpoints/RefreshToken.cs | 5 + .../Endpoints/UserEndpoints/SingUp.cs | 5 + src/Clean.Architecture.Web/Program.cs | 127 +++++++++++++++++- src/Clean.Architecture.Web/appsettings.json | 21 ++- 21 files changed, 376 insertions(+), 5 deletions(-) create mode 100644 src/Clean.Architecture.Core/Interfaces/IUserRepository.cs create mode 100644 src/Clean.Architecture.Core/RoleAggregate/Role.cs create mode 100644 src/Clean.Architecture.Core/UserAggregate/User.cs create mode 100644 src/Clean.Architecture.Infrastructure/UserRepository.cs create mode 100644 src/Clean.Architecture.SharedKernel/Auth/Jwt/AccessToken.cs create mode 100644 src/Clean.Architecture.SharedKernel/Auth/Jwt/IJwtService.cs create mode 100644 src/Clean.Architecture.SharedKernel/Auth/Jwt/JwtService.cs create mode 100644 src/Clean.Architecture.SharedKernel/Auth/SiteSettings.cs create mode 100644 src/Clean.Architecture.SharedKernel/Utilities/IdentityExtension.cs create mode 100644 src/Clean.Architecture.Web/Endpoints/UserEndpoints/Login.cs create mode 100644 src/Clean.Architecture.Web/Endpoints/UserEndpoints/RefreshToken.cs create mode 100644 src/Clean.Architecture.Web/Endpoints/UserEndpoints/SingUp.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index e7fbec2ea..68a9c13fc 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -22,12 +22,15 @@ + + + @@ -38,6 +41,7 @@ + diff --git a/src/Clean.Architecture.Core/Clean.Architecture.Core.csproj b/src/Clean.Architecture.Core/Clean.Architecture.Core.csproj index 934d828fb..6b27c23b2 100644 --- a/src/Clean.Architecture.Core/Clean.Architecture.Core.csproj +++ b/src/Clean.Architecture.Core/Clean.Architecture.Core.csproj @@ -13,6 +13,7 @@ + diff --git a/src/Clean.Architecture.Core/Interfaces/IUserRepository.cs b/src/Clean.Architecture.Core/Interfaces/IUserRepository.cs new file mode 100644 index 000000000..191183e8c --- /dev/null +++ b/src/Clean.Architecture.Core/Interfaces/IUserRepository.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Clean.Architecture.Core.UserAggregate; + +namespace Clean.Architecture.Core.Interfaces; +public interface IUserRepository +{ + Task UpdateLastLoginDateAsync(User user, CancellationToken cancellationToken); + Task GetByIdAsync(int userId, CancellationToken cancellationToken); +} diff --git a/src/Clean.Architecture.Core/RoleAggregate/Role.cs b/src/Clean.Architecture.Core/RoleAggregate/Role.cs new file mode 100644 index 000000000..1385b1438 --- /dev/null +++ b/src/Clean.Architecture.Core/RoleAggregate/Role.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Clean.Architecture.SharedKernel.Interfaces; +using Microsoft.AspNetCore.Identity; + +namespace Clean.Architecture.Core.RoleAggregate; +public class Role : IdentityRole, IAggregateRoot +{ + public string Description { get; set; } = default!; +} diff --git a/src/Clean.Architecture.Core/UserAggregate/User.cs b/src/Clean.Architecture.Core/UserAggregate/User.cs new file mode 100644 index 000000000..fd6f132a5 --- /dev/null +++ b/src/Clean.Architecture.Core/UserAggregate/User.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading.Tasks; +using Clean.Architecture.SharedKernel.Interfaces; +using Microsoft.AspNetCore.Identity; + +namespace Clean.Architecture.Core.UserAggregate; +public class User : IdentityUser, IAggregateRoot +{ + public User() + { + IsActive = true; + } + + public string FullName { get; set; } = default!; + + public int Age { get; set; } + + public GenderType Gender { get; set; } + + public bool IsActive { get; set; } + + public DateTimeOffset? LastLoginDate { get; set; } +} + +public enum GenderType +{ + Male = 1, + Female = 2 +} diff --git a/src/Clean.Architecture.Infrastructure/Clean.Architecture.Infrastructure.csproj b/src/Clean.Architecture.Infrastructure/Clean.Architecture.Infrastructure.csproj index 27e3650be..1a3de0526 100644 --- a/src/Clean.Architecture.Infrastructure/Clean.Architecture.Infrastructure.csproj +++ b/src/Clean.Architecture.Infrastructure/Clean.Architecture.Infrastructure.csproj @@ -10,6 +10,7 @@ + diff --git a/src/Clean.Architecture.Infrastructure/Data/AppDbContext.cs b/src/Clean.Architecture.Infrastructure/Data/AppDbContext.cs index 6de13cae8..e09ba5540 100644 --- a/src/Clean.Architecture.Infrastructure/Data/AppDbContext.cs +++ b/src/Clean.Architecture.Infrastructure/Data/AppDbContext.cs @@ -1,13 +1,16 @@ using System.Reflection; using Clean.Architecture.Core.ContributorAggregate; using Clean.Architecture.Core.ProjectAggregate; +using Clean.Architecture.Core.RoleAggregate; +using Clean.Architecture.Core.UserAggregate; using Clean.Architecture.SharedKernel; using Clean.Architecture.SharedKernel.Interfaces; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; namespace Clean.Architecture.Infrastructure.Data; -public class AppDbContext : DbContext +public class AppDbContext : IdentityDbContext { private readonly IDomainEventDispatcher? _dispatcher; diff --git a/src/Clean.Architecture.Infrastructure/StartupSetup.cs b/src/Clean.Architecture.Infrastructure/StartupSetup.cs index f17deeeaf..a9c24a335 100644 --- a/src/Clean.Architecture.Infrastructure/StartupSetup.cs +++ b/src/Clean.Architecture.Infrastructure/StartupSetup.cs @@ -8,5 +8,5 @@ public static class StartupSetup { public static void AddDbContext(this IServiceCollection services, string connectionString) => services.AddDbContext(options => - options.UseSqlite(connectionString)); // will be created in web project root + options.UseSqlServer(connectionString)); // will be created in web project root } diff --git a/src/Clean.Architecture.Infrastructure/UserRepository.cs b/src/Clean.Architecture.Infrastructure/UserRepository.cs new file mode 100644 index 000000000..29a7cc1e7 --- /dev/null +++ b/src/Clean.Architecture.Infrastructure/UserRepository.cs @@ -0,0 +1,24 @@ +using Clean.Architecture.Core.Interfaces; +using Clean.Architecture.Core.UserAggregate; +using Clean.Architecture.SharedKernel.Interfaces; + +namespace Clean.Architecture.Infrastructure; +public class UserRepository : IUserRepository +{ + readonly IRepository _repository; + public UserRepository(IRepository repository) + { + _repository = repository; + } + + public async Task GetByIdAsync(int userId, CancellationToken cancellationToken) + { + return await _repository.GetByIdAsync(userId, cancellationToken); + } + + public async Task UpdateLastLoginDateAsync(User user, CancellationToken cancellationToken) + { + user.LastLoginDate = DateTime.Now; + await _repository.UpdateAsync(user, cancellationToken); + } +} diff --git a/src/Clean.Architecture.SharedKernel/Auth/Jwt/AccessToken.cs b/src/Clean.Architecture.SharedKernel/Auth/Jwt/AccessToken.cs new file mode 100644 index 000000000..82112ebfc --- /dev/null +++ b/src/Clean.Architecture.SharedKernel/Auth/Jwt/AccessToken.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.IdentityModel.Tokens.Jwt; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Clean.Architecture.Infrastructure.Auth.Jwt; +public class AccessToken +{ + public string access_token { get; set; } + public string refresh_token { get; set; } = default!; + public string token_type { get; set; } + public int expires_in { get; set; } + public int refreshToken_expiresIn { get; set; } + + public AccessToken(JwtSecurityToken securityToken) + { + access_token = new JwtSecurityTokenHandler().WriteToken(securityToken); + token_type = "Bearer"; + expires_in = (int)(securityToken.ValidTo - DateTime.UtcNow).TotalSeconds; + } + public AccessToken(JwtSecurityToken securityToken, string refreshToken, int refreshTokenExpiresIn) + { + access_token = new JwtSecurityTokenHandler().WriteToken(securityToken); + token_type = "Bearer"; + expires_in = (int)(securityToken.ValidTo - DateTime.UtcNow).TotalSeconds; + refresh_token = refreshToken; + refreshToken_expiresIn = refreshTokenExpiresIn; + } +} diff --git a/src/Clean.Architecture.SharedKernel/Auth/Jwt/IJwtService.cs b/src/Clean.Architecture.SharedKernel/Auth/Jwt/IJwtService.cs new file mode 100644 index 000000000..fbfe6f184 --- /dev/null +++ b/src/Clean.Architecture.SharedKernel/Auth/Jwt/IJwtService.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Clean.Architecture.Infrastructure.Auth.Jwt; +public interface IJwtService +{ +} diff --git a/src/Clean.Architecture.SharedKernel/Auth/Jwt/JwtService.cs b/src/Clean.Architecture.SharedKernel/Auth/Jwt/JwtService.cs new file mode 100644 index 000000000..2ec47d4b9 --- /dev/null +++ b/src/Clean.Architecture.SharedKernel/Auth/Jwt/JwtService.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Clean.Architecture.Infrastructure.Auth.Jwt; +public class JwtService +{ +} diff --git a/src/Clean.Architecture.SharedKernel/Auth/SiteSettings.cs b/src/Clean.Architecture.SharedKernel/Auth/SiteSettings.cs new file mode 100644 index 000000000..08c0471fe --- /dev/null +++ b/src/Clean.Architecture.SharedKernel/Auth/SiteSettings.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Clean.Architecture.SharedKernel.Auth; +public class SiteSettings +{ + public JwtSettings JwtSettings { get; set; } = default!; + public IdentitySettings IdentitySettings { get; set; } = default!; +} + +public class IdentitySettings +{ + public bool PasswordRequireDigit { get; set; } + public int PasswordRequiredLength { get; set; } + public bool PasswordRequireNonAlphanumeric { get; set; } + public bool PasswordRequireUppercase { get; set; } + public bool PasswordRequireLowercase { get; set; } + public bool RequireUniqueEmail { get; set; } +} + +public class JwtSettings +{ + public string SecretKey { get; set; } = default!; + public string EncryptKey { get; set; } = default!; + public string Issuer { get; set; } = default!; + public string Audience { get; set; } = default!; + public int NotBeforeMinutes { get; set; } + public int ExpirationMinutes { get; set; } + public int RefreshTokenValidityInDays { get; set; } +} diff --git a/src/Clean.Architecture.SharedKernel/Clean.Architecture.SharedKernel.csproj b/src/Clean.Architecture.SharedKernel/Clean.Architecture.SharedKernel.csproj index 36765a764..04735fcac 100644 --- a/src/Clean.Architecture.SharedKernel/Clean.Architecture.SharedKernel.csproj +++ b/src/Clean.Architecture.SharedKernel/Clean.Architecture.SharedKernel.csproj @@ -8,6 +8,7 @@ + diff --git a/src/Clean.Architecture.SharedKernel/Utilities/IdentityExtension.cs b/src/Clean.Architecture.SharedKernel/Utilities/IdentityExtension.cs new file mode 100644 index 000000000..81f1bcb8c --- /dev/null +++ b/src/Clean.Architecture.SharedKernel/Utilities/IdentityExtension.cs @@ -0,0 +1,36 @@ +using System.Globalization; +using System.Security.Claims; +using System.Security.Principal; + +namespace Clean.Architecture.SharedKernel.Utilities; +public static class IdentityExtensions +{ + public static string? FindFirstValue(this ClaimsIdentity identity, string claimType) + { + return identity?.FindFirst(claimType)?.Value; + } + + public static string? FindFirstValue(this IIdentity identity, string claimType) + { + var claimsIdentity = identity as ClaimsIdentity; + return claimsIdentity?.FindFirstValue(claimType); + } + + public static string? GetUserId(this IIdentity identity) + { + return identity?.FindFirstValue(ClaimTypes.NameIdentifier); + } + + public static T? GetUserId(this IIdentity identity) where T : IConvertible + { + var userId = identity?.GetUserId(); + return !string.IsNullOrWhiteSpace(userId) + ? (T)Convert.ChangeType(userId, typeof(T), CultureInfo.InvariantCulture) + : default; + } + + public static string? GetUserName(this IIdentity identity) + { + return identity?.FindFirstValue(ClaimTypes.Name); + } +} diff --git a/src/Clean.Architecture.Web/Clean.Architecture.Web.csproj b/src/Clean.Architecture.Web/Clean.Architecture.Web.csproj index 900615b99..132ffc379 100644 --- a/src/Clean.Architecture.Web/Clean.Architecture.Web.csproj +++ b/src/Clean.Architecture.Web/Clean.Architecture.Web.csproj @@ -18,6 +18,7 @@ + diff --git a/src/Clean.Architecture.Web/Endpoints/UserEndpoints/Login.cs b/src/Clean.Architecture.Web/Endpoints/UserEndpoints/Login.cs new file mode 100644 index 000000000..c90f56694 --- /dev/null +++ b/src/Clean.Architecture.Web/Endpoints/UserEndpoints/Login.cs @@ -0,0 +1,5 @@ +namespace Clean.Architecture.Web.Endpoints.UserEndpoints; + +public class Login +{ +} diff --git a/src/Clean.Architecture.Web/Endpoints/UserEndpoints/RefreshToken.cs b/src/Clean.Architecture.Web/Endpoints/UserEndpoints/RefreshToken.cs new file mode 100644 index 000000000..c23005683 --- /dev/null +++ b/src/Clean.Architecture.Web/Endpoints/UserEndpoints/RefreshToken.cs @@ -0,0 +1,5 @@ +namespace Clean.Architecture.Web.Endpoints.UserEndpoints; + +public class RefreshToken +{ +} diff --git a/src/Clean.Architecture.Web/Endpoints/UserEndpoints/SingUp.cs b/src/Clean.Architecture.Web/Endpoints/UserEndpoints/SingUp.cs new file mode 100644 index 000000000..333eb802d --- /dev/null +++ b/src/Clean.Architecture.Web/Endpoints/UserEndpoints/SingUp.cs @@ -0,0 +1,5 @@ +namespace Clean.Architecture.Web.Endpoints.UserEndpoints; + +public class SingUp +{ +} diff --git a/src/Clean.Architecture.Web/Program.cs b/src/Clean.Architecture.Web/Program.cs index b676fd943..4fd77d455 100644 --- a/src/Clean.Architecture.Web/Program.cs +++ b/src/Clean.Architecture.Web/Program.cs @@ -10,6 +10,17 @@ using FastEndpoints.ApiExplorer; using Microsoft.OpenApi.Models; using Serilog; +using Clean.Architecture.Core.RoleAggregate; +using Clean.Architecture.SharedKernel.Auth; +using Microsoft.AspNetCore.Identity; +using Clean.Architecture.Core.UserAggregate; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using System.Text; +using Microsoft.IdentityModel.Tokens; +using Clean.Architecture.Core.Interfaces; +using System.Security.Claims; +using Clean.Architecture.SharedKernel.Utilities; +using System.Configuration; var builder = WebApplication.CreateBuilder(args); @@ -23,7 +34,7 @@ options.MinimumSameSitePolicy = SameSiteMode.None; }); -string? connectionString = builder.Configuration.GetConnectionString("SqliteConnection"); //Configuration.GetConnectionString("DefaultConnection"); +string? connectionString = builder.Configuration.GetConnectionString("DefaultConnection"); //Configuration.GetConnectionString("DefaultConnection"); builder.Services.AddDbContext(connectionString!); @@ -37,6 +48,9 @@ c.EnableAnnotations(); c.OperationFilter(); }); +var sitSettings = builder.Configuration.GetSection(nameof(SiteSettings)).Get(); +builder.Services.AddCustomIdentity(sitSettings!.IdentitySettings); +builder.Services.AddCustomJwtAuthentication(sitSettings!.JwtSettings); // add list services for diagnostic purposes - see https://github.com/ardalis/AspNetCoreStartupServices builder.Services.Configure(config => @@ -70,6 +84,8 @@ } app.UseRouting(); app.UseFastEndpoints(); +app.UseAuthentication(); +app.UseAuthorization(); app.UseHttpsRedirection(); app.UseStaticFiles(); @@ -93,7 +109,7 @@ { var context = services.GetRequiredService(); // context.Database.Migrate(); - context.Database.EnsureCreated(); + var X = context.Database.EnsureCreated(); SeedData.Initialize(services); } catch (Exception ex) @@ -104,3 +120,110 @@ } app.Run(); + + + + +public static class StartupSetup +{ + public static void AddCustomJwtAuthentication(this IServiceCollection services, JwtSettings settings) + { + services.AddAuthentication(options => + { + options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; + }) + .AddJwtBearer(options => + { + var secretKey = Encoding.UTF8.GetBytes(settings.SecretKey); + var encryptionKey = Encoding.UTF8.GetBytes(settings.EncryptKey); + + var validationParameters = new TokenValidationParameters + { + ClockSkew = TimeSpan.Zero, // default: 5 min + RequireSignedTokens = true, + + ValidateIssuerSigningKey = true, + IssuerSigningKey = new SymmetricSecurityKey(secretKey), + + RequireExpirationTime = true, + ValidateLifetime = true, + + ValidateAudience = true, //default : false + ValidAudience = settings.Audience, + + ValidateIssuer = true, //default : false + ValidIssuer = settings.Issuer, + + TokenDecryptionKey = new SymmetricSecurityKey(encryptionKey) + }; + + options.RequireHttpsMetadata = false; + options.SaveToken = true; + options.TokenValidationParameters = validationParameters; + + options.Events = new JwtBearerEvents + { + OnAuthenticationFailed = context => + { + if (context.Exception != null) + { + throw context.Exception; + } + + return Task.CompletedTask; + }, + OnTokenValidated = async context => + { + var signInManager = context.HttpContext.RequestServices.GetRequiredService>(); + var userRepository = context.HttpContext.RequestServices.GetRequiredService(); + + var claimsIdentity = context.Principal?.Identity as ClaimsIdentity; + if (claimsIdentity?.Claims?.Any() != true) + context.Fail("This token has no claims."); + + //Find user and token from database and perform your custom validation + var userId = claimsIdentity?.GetUserId(); + if (userId == null) + context.Fail("There is no UserId in claims."); + + var user = await userRepository.GetByIdAsync(userId.GetValueOrDefault(), context.HttpContext.RequestAborted); + + if (user == null) + context.Fail("User not found."); + + if (user != null && !user.IsActive) + context.Fail("User is not active."); + + await userRepository.UpdateLastLoginDateAsync(user!, context.HttpContext.RequestAborted); + }, + OnChallenge = context => + { + if (context.AuthenticateFailure != null) + throw context.AuthenticateFailure; + //throw new CleanArchAppException(ApiResultStatusCode.UnAuthorized, "Authenticate failure.", HttpStatusCode.Unauthorized, context.AuthenticateFailure, null); + //throw new CleanArchAppException(ApiResultStatusCode.UnAuthorized, "You are unauthorized to access this resource.", HttpStatusCode.Unauthorized); + throw new Exception("You are unauthorized to access this resource."); + } + }; + }); + } + public static void AddCustomIdentity(this IServiceCollection services, IdentitySettings settings) + { + services.AddIdentity(identityOptions => + { + //Password Settings + identityOptions.Password.RequireDigit = settings.PasswordRequireDigit; + identityOptions.Password.RequiredLength = settings.PasswordRequiredLength; + identityOptions.Password.RequireNonAlphanumeric = settings.PasswordRequireNonAlphanumeric; //#@! + identityOptions.Password.RequireUppercase = settings.PasswordRequireUppercase; + identityOptions.Password.RequireLowercase = settings.PasswordRequireLowercase; + + //UserName Settings + identityOptions.User.RequireUniqueEmail = settings.RequireUniqueEmail; + }) + .AddEntityFrameworkStores() + .AddDefaultTokenProviders(); + } +} diff --git a/src/Clean.Architecture.Web/appsettings.json b/src/Clean.Architecture.Web/appsettings.json index 7331e3538..0b39eeead 100644 --- a/src/Clean.Architecture.Web/appsettings.json +++ b/src/Clean.Architecture.Web/appsettings.json @@ -1,6 +1,6 @@ { "ConnectionStrings": { - "DefaultConnection": "Server=(localdb)\\v11.0;Database=cleanarchitecture;Trusted_Connection=True;MultipleActiveResultSets=true", + "DefaultConnection": "Server=CRLHL-ANWARAHM1\\SQLEXPRESS;Database=cleanarchitecture;Trusted_Connection=True;MultipleActiveResultSets=true;TrustServerCertificate=True", "SqliteConnection": "Data Source=database.sqlite" }, "Serilog": { @@ -28,5 +28,24 @@ // } //} ] + }, + "SiteSettings": { + "JwtSettings": { + "SecretKey": "LongerThan-16Char-SecretKey", + "EncryptKey": "16CharEncryptKey", + "Issuer": "CleanArchTemplate", + "Audience": "CleanArchTemplate", + "NotBeforeMinutes": "0", + "ExpirationMinutes": "1440", + "RefreshTokenValidityInDays": 7 + }, + "IdentitySettings": { + "PasswordRequireDigit": "true", + "PasswordRequiredLength": "6", + "PasswordRequireNonAlphanumeric": "false", + "PasswordRequireUppercase": "false", + "PasswordRequireLowercase": "false", + "RequireUniqueEmail": "true" + } } } From 7202e7e956e49e500bf45c5813d3d05c24d75fa7 Mon Sep 17 00:00:00 2001 From: "TL\\anwarahm" Date: Sat, 31 Dec 2022 21:20:18 +0500 Subject: [PATCH 2/7] Login Endpoint added Auth folder moved to Core project --- .../DefaultCoreModule.cs | 4 + .../Services/Auth/Jwt/AccessToken.cs | 21 ++++ .../Services/Auth/Jwt/IJwtService.cs | 13 +++ .../Services/Auth/Jwt/JwtService.cs | 103 ++++++++++++++++++ .../Services}/Auth/SiteSettings.cs | 2 +- .../{RoleAggregate => UserAggregate}/Role.cs | 2 +- .../Data/AppDbContext.cs | 3 +- .../Auth/Jwt/AccessToken.cs | 31 ------ .../Auth/Jwt/IJwtService.cs | 10 -- .../Auth/Jwt/JwtService.cs | 10 -- .../UserEndpoints/Login.LoginUserRequest.cs | 16 +++ .../UserEndpoints/Login.LoginUserResponse.cs | 8 ++ .../Endpoints/UserEndpoints/Login.cs | 48 +++++++- .../Endpoints/UserEndpoints/RefreshToken.cs | 5 - .../UserEndpoints/SignUp.SignUpUserRequest.cs | 28 +++++ .../Endpoints/UserEndpoints/SignUp.cs | 70 ++++++++++++ .../Endpoints/UserEndpoints/SingUp.cs | 5 - src/Clean.Architecture.Web/Program.cs | 6 +- 18 files changed, 315 insertions(+), 70 deletions(-) create mode 100644 src/Clean.Architecture.Core/Services/Auth/Jwt/AccessToken.cs create mode 100644 src/Clean.Architecture.Core/Services/Auth/Jwt/IJwtService.cs create mode 100644 src/Clean.Architecture.Core/Services/Auth/Jwt/JwtService.cs rename src/{Clean.Architecture.SharedKernel => Clean.Architecture.Core/Services}/Auth/SiteSettings.cs (95%) rename src/Clean.Architecture.Core/{RoleAggregate => UserAggregate}/Role.cs (86%) delete mode 100644 src/Clean.Architecture.SharedKernel/Auth/Jwt/AccessToken.cs delete mode 100644 src/Clean.Architecture.SharedKernel/Auth/Jwt/IJwtService.cs delete mode 100644 src/Clean.Architecture.SharedKernel/Auth/Jwt/JwtService.cs create mode 100644 src/Clean.Architecture.Web/Endpoints/UserEndpoints/Login.LoginUserRequest.cs create mode 100644 src/Clean.Architecture.Web/Endpoints/UserEndpoints/Login.LoginUserResponse.cs delete mode 100644 src/Clean.Architecture.Web/Endpoints/UserEndpoints/RefreshToken.cs create mode 100644 src/Clean.Architecture.Web/Endpoints/UserEndpoints/SignUp.SignUpUserRequest.cs create mode 100644 src/Clean.Architecture.Web/Endpoints/UserEndpoints/SignUp.cs delete mode 100644 src/Clean.Architecture.Web/Endpoints/UserEndpoints/SingUp.cs diff --git a/src/Clean.Architecture.Core/DefaultCoreModule.cs b/src/Clean.Architecture.Core/DefaultCoreModule.cs index 1daac3d74..171659057 100644 --- a/src/Clean.Architecture.Core/DefaultCoreModule.cs +++ b/src/Clean.Architecture.Core/DefaultCoreModule.cs @@ -1,6 +1,7 @@ using Autofac; using Clean.Architecture.Core.Interfaces; using Clean.Architecture.Core.Services; +using Clean.Architecture.Core.Services.Auth.Jwt; namespace Clean.Architecture.Core; @@ -13,5 +14,8 @@ protected override void Load(ContainerBuilder builder) builder.RegisterType() .As().InstancePerLifetimeScope(); + + builder.RegisterType() + .As().InstancePerLifetimeScope(); } } diff --git a/src/Clean.Architecture.Core/Services/Auth/Jwt/AccessToken.cs b/src/Clean.Architecture.Core/Services/Auth/Jwt/AccessToken.cs new file mode 100644 index 000000000..f4fb6f190 --- /dev/null +++ b/src/Clean.Architecture.Core/Services/Auth/Jwt/AccessToken.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.IdentityModel.Tokens.Jwt; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Clean.Architecture.Core.Services.Auth.Jwt; +public class AccessToken +{ + public string Access_Token { get; set; } + public string TokenType { get; set; } + public int ExpiresIn { get; set; } + + public AccessToken(JwtSecurityToken securityToken) + { + Access_Token = new JwtSecurityTokenHandler().WriteToken(securityToken); + TokenType = "Bearer"; + ExpiresIn = (int)(securityToken.ValidTo - DateTime.UtcNow).TotalSeconds; + } +} diff --git a/src/Clean.Architecture.Core/Services/Auth/Jwt/IJwtService.cs b/src/Clean.Architecture.Core/Services/Auth/Jwt/IJwtService.cs new file mode 100644 index 000000000..b2466fa0a --- /dev/null +++ b/src/Clean.Architecture.Core/Services/Auth/Jwt/IJwtService.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Clean.Architecture.Core.UserAggregate; + +namespace Clean.Architecture.Core.Services.Auth.Jwt; +public interface IJwtService +{ + Task GenerateAsync(User user); + int? ValidateJwtAccessTokenAsync(string token); +} diff --git a/src/Clean.Architecture.Core/Services/Auth/Jwt/JwtService.cs b/src/Clean.Architecture.Core/Services/Auth/Jwt/JwtService.cs new file mode 100644 index 000000000..337508cec --- /dev/null +++ b/src/Clean.Architecture.Core/Services/Auth/Jwt/JwtService.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using System.IdentityModel.Tokens.Jwt; +using System.Linq; +using System.Security.Claims; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; +using Clean.Architecture.Core.Services.Auth; +using Clean.Architecture.Core.UserAggregate; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; + +namespace Clean.Architecture.Core.Services.Auth.Jwt; +public class JwtService : IJwtService +{ + private readonly SiteSettings _siteSetting; + private readonly UserManager _userManager; + + public JwtService(IOptionsSnapshot settings, + UserManager userManager) + { + _siteSetting = settings.Value; + _userManager = userManager ?? throw new ArgumentNullException(nameof(userManager)); + } + + public async Task GenerateAsync(User user) + { + var secretKey = Encoding.UTF8.GetBytes(_siteSetting.JwtSettings.SecretKey); // longer that 16 character + var signingCredentials = new SigningCredentials(new SymmetricSecurityKey(secretKey), SecurityAlgorithms.HmacSha256Signature); + + //We can use EncryptingCredentials options in SecurityTokenDescriptor and our JWT token will not be parsed by jwt.io site and it will be decrypted only by our code. + //Hence you can secure your token and who can see it + var encryptionKey = Encoding.UTF8.GetBytes(_siteSetting.JwtSettings.EncryptKey); //must be 16 character + var encryptingCredentials = new EncryptingCredentials(new SymmetricSecurityKey(encryptionKey), SecurityAlgorithms.Aes128KW, SecurityAlgorithms.Aes128CbcHmacSha256); + + var claims = await GetClaimsAsync(user); + + var descriptor = new SecurityTokenDescriptor + { + Issuer = _siteSetting.JwtSettings.Issuer, + Audience = _siteSetting.JwtSettings.Audience, + IssuedAt = DateTime.Now, + NotBefore = DateTime.Now.AddMinutes(_siteSetting.JwtSettings.NotBeforeMinutes), + Expires = DateTime.Now.AddMinutes(_siteSetting.JwtSettings.ExpirationMinutes), + SigningCredentials = signingCredentials, + //EncryptingCredentials = encryptingCredentials, + Subject = new ClaimsIdentity(claims) + }; + + var tokenHandler = new JwtSecurityTokenHandler(); + + var securityToken = tokenHandler.CreateJwtSecurityToken(descriptor); + + return new AccessToken(securityToken: securityToken); + } + + public int? ValidateJwtAccessTokenAsync(string token) + { + var secretKey = Encoding.UTF8.GetBytes(_siteSetting.JwtSettings.SecretKey); // longer that 16 character + var encryptionKey = Encoding.UTF8.GetBytes(_siteSetting.JwtSettings.EncryptKey); //must be 16 character + + var tokenHandler = new JwtSecurityTokenHandler(); + try + { + tokenHandler.ValidateToken(token, new TokenValidationParameters + { + ValidateIssuerSigningKey = true, + IssuerSigningKey = new SymmetricSecurityKey(secretKey), + TokenDecryptionKey = new SymmetricSecurityKey(encryptionKey), + ValidateIssuer = false, + ValidateAudience = false, + // set clockskew to zero so tokens expire exactly at token expiration time (instead of 5 minutes later) + ClockSkew = TimeSpan.Zero + }, out var validatedToken); + + var jwtSecurityToken = (JwtSecurityToken)validatedToken; + var userId = int.Parse(jwtSecurityToken.Claims.First(claim => claim.Type == "nameid").Value); + return userId; + } + catch + { + return null; + } + } + + private async Task> GetClaimsAsync(User user) + { + var claims = new List(); + claims.Add(new Claim(ClaimTypes.NameIdentifier, user.Id.ToString())); + claims.Add(new Claim(ClaimTypes.Name, user.UserName!)); + + var userRoles = await _userManager.GetRolesAsync(user); + + foreach (var role in userRoles) + { + claims.Add(new Claim(ClaimTypes.Role, role)); + } + + return claims; + } +} diff --git a/src/Clean.Architecture.SharedKernel/Auth/SiteSettings.cs b/src/Clean.Architecture.Core/Services/Auth/SiteSettings.cs similarity index 95% rename from src/Clean.Architecture.SharedKernel/Auth/SiteSettings.cs rename to src/Clean.Architecture.Core/Services/Auth/SiteSettings.cs index 08c0471fe..91ec866b4 100644 --- a/src/Clean.Architecture.SharedKernel/Auth/SiteSettings.cs +++ b/src/Clean.Architecture.Core/Services/Auth/SiteSettings.cs @@ -4,7 +4,7 @@ using System.Text; using System.Threading.Tasks; -namespace Clean.Architecture.SharedKernel.Auth; +namespace Clean.Architecture.Core.Services.Auth; public class SiteSettings { public JwtSettings JwtSettings { get; set; } = default!; diff --git a/src/Clean.Architecture.Core/RoleAggregate/Role.cs b/src/Clean.Architecture.Core/UserAggregate/Role.cs similarity index 86% rename from src/Clean.Architecture.Core/RoleAggregate/Role.cs rename to src/Clean.Architecture.Core/UserAggregate/Role.cs index 1385b1438..ec5aec5eb 100644 --- a/src/Clean.Architecture.Core/RoleAggregate/Role.cs +++ b/src/Clean.Architecture.Core/UserAggregate/Role.cs @@ -6,7 +6,7 @@ using Clean.Architecture.SharedKernel.Interfaces; using Microsoft.AspNetCore.Identity; -namespace Clean.Architecture.Core.RoleAggregate; +namespace Clean.Architecture.Core.UserAggregate; public class Role : IdentityRole, IAggregateRoot { public string Description { get; set; } = default!; diff --git a/src/Clean.Architecture.Infrastructure/Data/AppDbContext.cs b/src/Clean.Architecture.Infrastructure/Data/AppDbContext.cs index e09ba5540..28be3f590 100644 --- a/src/Clean.Architecture.Infrastructure/Data/AppDbContext.cs +++ b/src/Clean.Architecture.Infrastructure/Data/AppDbContext.cs @@ -1,7 +1,6 @@ using System.Reflection; using Clean.Architecture.Core.ContributorAggregate; using Clean.Architecture.Core.ProjectAggregate; -using Clean.Architecture.Core.RoleAggregate; using Clean.Architecture.Core.UserAggregate; using Clean.Architecture.SharedKernel; using Clean.Architecture.SharedKernel.Interfaces; @@ -23,7 +22,7 @@ public AppDbContext(DbContextOptions options, public DbSet ToDoItems => Set(); public DbSet Projects => Set(); - public DbSet Contributors => Set(); + public DbSet Contributors => Set(); protected override void OnModelCreating(ModelBuilder modelBuilder) { diff --git a/src/Clean.Architecture.SharedKernel/Auth/Jwt/AccessToken.cs b/src/Clean.Architecture.SharedKernel/Auth/Jwt/AccessToken.cs deleted file mode 100644 index 82112ebfc..000000000 --- a/src/Clean.Architecture.SharedKernel/Auth/Jwt/AccessToken.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IdentityModel.Tokens.Jwt; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Clean.Architecture.Infrastructure.Auth.Jwt; -public class AccessToken -{ - public string access_token { get; set; } - public string refresh_token { get; set; } = default!; - public string token_type { get; set; } - public int expires_in { get; set; } - public int refreshToken_expiresIn { get; set; } - - public AccessToken(JwtSecurityToken securityToken) - { - access_token = new JwtSecurityTokenHandler().WriteToken(securityToken); - token_type = "Bearer"; - expires_in = (int)(securityToken.ValidTo - DateTime.UtcNow).TotalSeconds; - } - public AccessToken(JwtSecurityToken securityToken, string refreshToken, int refreshTokenExpiresIn) - { - access_token = new JwtSecurityTokenHandler().WriteToken(securityToken); - token_type = "Bearer"; - expires_in = (int)(securityToken.ValidTo - DateTime.UtcNow).TotalSeconds; - refresh_token = refreshToken; - refreshToken_expiresIn = refreshTokenExpiresIn; - } -} diff --git a/src/Clean.Architecture.SharedKernel/Auth/Jwt/IJwtService.cs b/src/Clean.Architecture.SharedKernel/Auth/Jwt/IJwtService.cs deleted file mode 100644 index fbfe6f184..000000000 --- a/src/Clean.Architecture.SharedKernel/Auth/Jwt/IJwtService.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Clean.Architecture.Infrastructure.Auth.Jwt; -public interface IJwtService -{ -} diff --git a/src/Clean.Architecture.SharedKernel/Auth/Jwt/JwtService.cs b/src/Clean.Architecture.SharedKernel/Auth/Jwt/JwtService.cs deleted file mode 100644 index 2ec47d4b9..000000000 --- a/src/Clean.Architecture.SharedKernel/Auth/Jwt/JwtService.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Clean.Architecture.Infrastructure.Auth.Jwt; -public class JwtService -{ -} diff --git a/src/Clean.Architecture.Web/Endpoints/UserEndpoints/Login.LoginUserRequest.cs b/src/Clean.Architecture.Web/Endpoints/UserEndpoints/Login.LoginUserRequest.cs new file mode 100644 index 000000000..1b8838c0f --- /dev/null +++ b/src/Clean.Architecture.Web/Endpoints/UserEndpoints/Login.LoginUserRequest.cs @@ -0,0 +1,16 @@ +using Microsoft.Build.Framework; + +namespace Clean.Architecture.Web.Endpoints.UserEndpoints; + +public class LoginUserRequest +{ + public const string Route = "/User/Login"; + [Required] + public string Email { get; set; } = default!; + + [Required] + public string Password { get; set; } = default!; + + [Required] + public string RefreshToken { get; set; } = default!; +} diff --git a/src/Clean.Architecture.Web/Endpoints/UserEndpoints/Login.LoginUserResponse.cs b/src/Clean.Architecture.Web/Endpoints/UserEndpoints/Login.LoginUserResponse.cs new file mode 100644 index 000000000..dae5570d6 --- /dev/null +++ b/src/Clean.Architecture.Web/Endpoints/UserEndpoints/Login.LoginUserResponse.cs @@ -0,0 +1,8 @@ +namespace Clean.Architecture.Web.Endpoints.UserEndpoints; + +public class LoginUserResponse +{ + public string AccessToken { get; set; } = default!; + public string TokenType { get; set; } = default!; + public int ExpiresIn { get; set; } +} diff --git a/src/Clean.Architecture.Web/Endpoints/UserEndpoints/Login.cs b/src/Clean.Architecture.Web/Endpoints/UserEndpoints/Login.cs index c90f56694..91291ecc8 100644 --- a/src/Clean.Architecture.Web/Endpoints/UserEndpoints/Login.cs +++ b/src/Clean.Architecture.Web/Endpoints/UserEndpoints/Login.cs @@ -1,5 +1,49 @@ -namespace Clean.Architecture.Web.Endpoints.UserEndpoints; +using Clean.Architecture.Core.Services.Auth.Jwt; +using Clean.Architecture.Core.UserAggregate; +using FastEndpoints; +using Microsoft.AspNetCore.Identity; -public class Login +namespace Clean.Architecture.Web.Endpoints.UserEndpoints; + +public class Login : Endpoint { + private readonly UserManager _userManager; + private readonly IJwtService _jwtService; + public Login(UserManager userManager, IJwtService jwtService) + { + _userManager = userManager; + _jwtService = jwtService; + } + + public override void Configure() + { + Post(LoginUserRequest.Route); + AllowAnonymous(); + Options(x => x + .WithTags("UserEndpoints")); + } + public override async Task HandleAsync( + LoginUserRequest request, + CancellationToken cancellationToken) + { + if (request is null) + ThrowError("Request body is empty"); + var user = await _userManager.FindByEmailAsync(request.Email); //userName/email can be used to find a unique user + if (user == null) + ThrowError("username or password is incorrect"); + + var isPasswordValid = await _userManager.CheckPasswordAsync(user, request.Password); + if (!isPasswordValid) + ThrowError("username or password is incorrect"); + + var jwt = await _jwtService.GenerateAsync(user); + user.LastLoginDate = DateTime.UtcNow; + await _userManager.UpdateAsync(user); + await SendOkAsync(new LoginUserResponse + { + AccessToken = jwt.Access_Token, + TokenType = jwt.TokenType, + ExpiresIn = jwt.ExpiresIn + }); + } } diff --git a/src/Clean.Architecture.Web/Endpoints/UserEndpoints/RefreshToken.cs b/src/Clean.Architecture.Web/Endpoints/UserEndpoints/RefreshToken.cs deleted file mode 100644 index c23005683..000000000 --- a/src/Clean.Architecture.Web/Endpoints/UserEndpoints/RefreshToken.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace Clean.Architecture.Web.Endpoints.UserEndpoints; - -public class RefreshToken -{ -} diff --git a/src/Clean.Architecture.Web/Endpoints/UserEndpoints/SignUp.SignUpUserRequest.cs b/src/Clean.Architecture.Web/Endpoints/UserEndpoints/SignUp.SignUpUserRequest.cs new file mode 100644 index 000000000..9a42f6c0f --- /dev/null +++ b/src/Clean.Architecture.Web/Endpoints/UserEndpoints/SignUp.SignUpUserRequest.cs @@ -0,0 +1,28 @@ +using System.ComponentModel.DataAnnotations; +using Clean.Architecture.Core.UserAggregate; + +namespace Clean.Architecture.Web.Endpoints.UserEndpoints; + +public class SignUpUserRequest +{ + public const string Route = "/User/SignUp"; + + [Required] + public string UserName { get; set; } = default!; + + [Required] + public string Email { get; set; } = default!; + + + [Required] + public string Password { get; set; } = default!; + + [Required] + public string FullName { get; set; } = default!; + + [Required] + public int Age { get; set; } + + [Required] + public GenderType Gender { get; set; } +} diff --git a/src/Clean.Architecture.Web/Endpoints/UserEndpoints/SignUp.cs b/src/Clean.Architecture.Web/Endpoints/UserEndpoints/SignUp.cs new file mode 100644 index 000000000..110179d2f --- /dev/null +++ b/src/Clean.Architecture.Web/Endpoints/UserEndpoints/SignUp.cs @@ -0,0 +1,70 @@ +using Clean.Architecture.Core.UserAggregate; +using Clean.Architecture.SharedKernel.Interfaces; +using FastEndpoints; +using Microsoft.AspNetCore.Identity; + +namespace Clean.Architecture.Web.Endpoints.UserEndpoints; + +public class SignUp : Endpoint +{ + private readonly UserManager _userManager; + private readonly RoleManager _roleManager; + public SignUp(UserManager userManager, RoleManager roleManager) + { + _userManager = userManager; + _roleManager = roleManager; + } + + public override void Configure() + { + Post(SignUpUserRequest.Route); + AllowAnonymous(); + Options(x => x + .WithTags("UserEndpoints")); + } + public override async Task HandleAsync( + SignUpUserRequest request, + CancellationToken cancellationToken) + { + if (request is null) + ThrowError("Request body is empty"); + //TODO:CHECK FOR USERNAME EMPTY OR NULL + var newUser = new User + { + Age = request.Age, + FullName = request.FullName!, + Gender = request.Gender, + UserName = request.UserName, + Email = request.Email + }; + var createUserResult = await _userManager.CreateAsync(newUser, request.Password); + if (!createUserResult.Succeeded) { ThrowValidationErrors(createUserResult.Errors); } + + var role = await _roleManager.FindByNameAsync("Admin"); + if (role==null) + { + var addRoleResult = await _roleManager.CreateAsync(new Role + { + Name = "Admin", + Description = "admin role" + }); + if (!addRoleResult.Succeeded) { ThrowValidationErrors(addRoleResult.Errors); } + } + else + { + var assignRoleResult = await _userManager.AddToRoleAsync(newUser, role.Name!); + if (!assignRoleResult.Succeeded) { ThrowValidationErrors(assignRoleResult.Errors); } + } + + await SendNoContentAsync(); + } + private void ThrowValidationErrors(IEnumerable identityErrors) + { + foreach (var error in identityErrors) + { + AddError(error.Description, error.Code); + } + ThrowIfAnyErrors(); + } +} + diff --git a/src/Clean.Architecture.Web/Endpoints/UserEndpoints/SingUp.cs b/src/Clean.Architecture.Web/Endpoints/UserEndpoints/SingUp.cs deleted file mode 100644 index 333eb802d..000000000 --- a/src/Clean.Architecture.Web/Endpoints/UserEndpoints/SingUp.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace Clean.Architecture.Web.Endpoints.UserEndpoints; - -public class SingUp -{ -} diff --git a/src/Clean.Architecture.Web/Program.cs b/src/Clean.Architecture.Web/Program.cs index 4fd77d455..88e4663bd 100644 --- a/src/Clean.Architecture.Web/Program.cs +++ b/src/Clean.Architecture.Web/Program.cs @@ -10,8 +10,6 @@ using FastEndpoints.ApiExplorer; using Microsoft.OpenApi.Models; using Serilog; -using Clean.Architecture.Core.RoleAggregate; -using Clean.Architecture.SharedKernel.Auth; using Microsoft.AspNetCore.Identity; using Clean.Architecture.Core.UserAggregate; using Microsoft.AspNetCore.Authentication.JwtBearer; @@ -20,7 +18,7 @@ using Clean.Architecture.Core.Interfaces; using System.Security.Claims; using Clean.Architecture.SharedKernel.Utilities; -using System.Configuration; +using Clean.Architecture.Core.Services.Auth; var builder = WebApplication.CreateBuilder(args); @@ -48,6 +46,8 @@ c.EnableAnnotations(); c.OperationFilter(); }); +builder.Services.Configure(builder.Configuration.GetSection(nameof(SiteSettings))); + var sitSettings = builder.Configuration.GetSection(nameof(SiteSettings)).Get(); builder.Services.AddCustomIdentity(sitSettings!.IdentitySettings); builder.Services.AddCustomJwtAuthentication(sitSettings!.JwtSettings); From c6e4233c6c2150c0b843f4cf2125faadf6107639 Mon Sep 17 00:00:00 2001 From: "TL\\anwarahm" Date: Sun, 1 Jan 2023 19:05:05 +0500 Subject: [PATCH 3/7] Helper class added to wrap JwtBearerEvents --- .../Services/Auth/Jwt/JwtService.cs | 16 +- .../Clean.Architecture.Infrastructure.csproj | 1 + .../DefaultInfrastructureModule.cs | 5 + .../StartupSetup.cs | 149 +++++++++++++++++- .../Clean.Architecture.Web.csproj | 1 - .../Endpoints/ProjectEndpoints/Create.cs | 2 + .../UserEndpoints/Login.LoginUserRequest.cs | 3 - src/Clean.Architecture.Web/Program.cs | 115 -------------- 8 files changed, 162 insertions(+), 130 deletions(-) diff --git a/src/Clean.Architecture.Core/Services/Auth/Jwt/JwtService.cs b/src/Clean.Architecture.Core/Services/Auth/Jwt/JwtService.cs index 337508cec..a91271912 100644 --- a/src/Clean.Architecture.Core/Services/Auth/Jwt/JwtService.cs +++ b/src/Clean.Architecture.Core/Services/Auth/Jwt/JwtService.cs @@ -1,12 +1,6 @@ -using System; -using System.Collections.Generic; -using System.IdentityModel.Tokens.Jwt; -using System.Linq; +using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; -using System.Security.Cryptography; using System.Text; -using System.Threading.Tasks; -using Clean.Architecture.Core.Services.Auth; using Clean.Architecture.Core.UserAggregate; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Options; @@ -30,7 +24,7 @@ public async Task GenerateAsync(User user) var secretKey = Encoding.UTF8.GetBytes(_siteSetting.JwtSettings.SecretKey); // longer that 16 character var signingCredentials = new SigningCredentials(new SymmetricSecurityKey(secretKey), SecurityAlgorithms.HmacSha256Signature); - //We can use EncryptingCredentials options in SecurityTokenDescriptor and our JWT token will not be parsed by jwt.io site and it will be decrypted only by our code. + //We can use EncryptingCredentials options in SecurityTokenDescriptor and hence our JWT token will not be parsed by jwt.io site and it will only be decrypted only by our code. //Hence you can secure your token and who can see it var encryptionKey = Encoding.UTF8.GetBytes(_siteSetting.JwtSettings.EncryptKey); //must be 16 character var encryptingCredentials = new EncryptingCredentials(new SymmetricSecurityKey(encryptionKey), SecurityAlgorithms.Aes128KW, SecurityAlgorithms.Aes128CbcHmacSha256); @@ -59,7 +53,9 @@ public async Task GenerateAsync(User user) public int? ValidateJwtAccessTokenAsync(string token) { var secretKey = Encoding.UTF8.GetBytes(_siteSetting.JwtSettings.SecretKey); // longer that 16 character - var encryptionKey = Encoding.UTF8.GetBytes(_siteSetting.JwtSettings.EncryptKey); //must be 16 character + + //if you are giving a value to EncryptingCredentials while generating a token then uncomment the encryptionKey and TokenDecryptionKey option so token can be parsed while validating. + //var encryptionKey = Encoding.UTF8.GetBytes(_siteSetting.JwtSettings.EncryptKey); //must be 16 character var tokenHandler = new JwtSecurityTokenHandler(); try @@ -68,7 +64,7 @@ public async Task GenerateAsync(User user) { ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey(secretKey), - TokenDecryptionKey = new SymmetricSecurityKey(encryptionKey), + //TokenDecryptionKey = new SymmetricSecurityKey(encryptionKey), ValidateIssuer = false, ValidateAudience = false, // set clockskew to zero so tokens expire exactly at token expiration time (instead of 5 minutes later) diff --git a/src/Clean.Architecture.Infrastructure/Clean.Architecture.Infrastructure.csproj b/src/Clean.Architecture.Infrastructure/Clean.Architecture.Infrastructure.csproj index 1a3de0526..df07409a9 100644 --- a/src/Clean.Architecture.Infrastructure/Clean.Architecture.Infrastructure.csproj +++ b/src/Clean.Architecture.Infrastructure/Clean.Architecture.Infrastructure.csproj @@ -10,6 +10,7 @@ + diff --git a/src/Clean.Architecture.Infrastructure/DefaultInfrastructureModule.cs b/src/Clean.Architecture.Infrastructure/DefaultInfrastructureModule.cs index 4248eff11..f143f8e15 100644 --- a/src/Clean.Architecture.Infrastructure/DefaultInfrastructureModule.cs +++ b/src/Clean.Architecture.Infrastructure/DefaultInfrastructureModule.cs @@ -69,6 +69,11 @@ private void RegisterCommonDependencies(ContainerBuilder builder) .As() .InstancePerLifetimeScope(); + builder + .RegisterType() + .As() + .InstancePerLifetimeScope(); + builder.Register(context => { var c = context.Resolve(); diff --git a/src/Clean.Architecture.Infrastructure/StartupSetup.cs b/src/Clean.Architecture.Infrastructure/StartupSetup.cs index a9c24a335..7ae70bd10 100644 --- a/src/Clean.Architecture.Infrastructure/StartupSetup.cs +++ b/src/Clean.Architecture.Infrastructure/StartupSetup.cs @@ -1,6 +1,17 @@ -using Clean.Architecture.Infrastructure.Data; +using System.Security.Claims; +using System.Text; +using Clean.Architecture.Core.Interfaces; +using Clean.Architecture.Core.Services.Auth; +using Clean.Architecture.Core.UserAggregate; +using Clean.Architecture.Infrastructure.Data; +using Clean.Architecture.SharedKernel.Utilities; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; +using Microsoft.IdentityModel.Tokens; +using System.Text.Json; namespace Clean.Architecture.Infrastructure; @@ -9,4 +20,140 @@ public static class StartupSetup public static void AddDbContext(this IServiceCollection services, string connectionString) => services.AddDbContext(options => options.UseSqlServer(connectionString)); // will be created in web project root + + public static void AddCustomJwtAuthentication(this IServiceCollection services, JwtSettings settings) + { + services.AddAuthentication(options => + { + options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; + }) + .AddJwtBearer(options => + { + var secretKey = Encoding.UTF8.GetBytes(settings.SecretKey); + //You can uncomment encryptionKey and TokenDecryptionKey fields if you are going to use it + //var encryptionKey = Encoding.UTF8.GetBytes(settings.EncryptKey); + + var validationParameters = new TokenValidationParameters + { + ClockSkew = TimeSpan.Zero, // default: 5 min + RequireSignedTokens = true, + + ValidateIssuerSigningKey = true, + IssuerSigningKey = new SymmetricSecurityKey(secretKey), + + RequireExpirationTime = true, + ValidateLifetime = true, + + ValidateAudience = true, //default : false + ValidAudience = settings.Audience, + + ValidateIssuer = true, //default : false + ValidIssuer = settings.Issuer, + + //TokenDecryptionKey = new SymmetricSecurityKey(encryptionKey) + }; + + options.RequireHttpsMetadata = false; + options.SaveToken = true; + options.TokenValidationParameters = validationParameters; + + options.Events = new JwtJwtBearerHelperEvents().GetJwtBeareEvents(); + }); + } + + public static void AddCustomIdentity(this IServiceCollection services, IdentitySettings settings) + { + services.AddIdentity(identityOptions => + { + //Password Settings + identityOptions.Password.RequireDigit = settings.PasswordRequireDigit; + identityOptions.Password.RequiredLength = settings.PasswordRequiredLength; + identityOptions.Password.RequireNonAlphanumeric = settings.PasswordRequireNonAlphanumeric; //#@! + identityOptions.Password.RequireUppercase = settings.PasswordRequireUppercase; + identityOptions.Password.RequireLowercase = settings.PasswordRequireLowercase; + + //UserName Settings + identityOptions.User.RequireUniqueEmail = settings.RequireUniqueEmail; + }) + .AddEntityFrameworkStores() + .AddDefaultTokenProviders(); + } } +/// +/// A helper JwtJwtBearerHelperEvents class which is added to make the code more readable and avoid any complexity +/// +internal class JwtJwtBearerHelperEvents +{ + public JwtBearerEvents GetJwtBeareEvents() + { + return new JwtBearerEvents + { + OnAuthenticationFailed = OnAuthenticationFailedJwtBearerEvent(), + OnTokenValidated = OnTokenValidatedJwtBearerEvent(), + OnChallenge = OnOnChallengeJwtBearerEvent() + }; + } + private Func OnAuthenticationFailedJwtBearerEvent() + { + return (context) => + { + if (context.Exception != null) + { + throw context.Exception; + } + return Task.CompletedTask; + }; + } + private Func OnTokenValidatedJwtBearerEvent() + { + return async context => + { + var signInManager = context.HttpContext.RequestServices.GetRequiredService>(); + var userRepository = context.HttpContext.RequestServices.GetRequiredService(); + + var claimsIdentity = context.Principal!.Identity as ClaimsIdentity; + if (claimsIdentity!.Claims?.Any() != true) + context.Fail("This token has no claims."); + + //var securityStamp = claimsIdentity.FindFirstValue(new ClaimsIdentityOptions().SecurityStampClaimType); + //if (!securityStamp.HasValue()) + // context.Fail("This token has no security stamp"); + + //Get UserId and check if user exists in db + var userId = claimsIdentity.GetUserId(); + if (userId == 0) + context.Fail("UserId has no value in claims."); + var user = await userRepository.GetByIdAsync(userId, context.HttpContext.RequestAborted); + if (user == null) + context.Fail("User not found."); + //if (user.SecurityStamp != Guid.Parse(securityStamp)) + // context.Fail("Token security stamp is not valid."); + + //var validatedUser = await signInManager.ValidateSecurityStampAsync(context.Principal); + //if (validatedUser == null) + // context.Fail("Token security stamp is not valid."); + + if (!user!.IsActive) + context.Fail("User is not active."); + + await userRepository.UpdateLastLoginDateAsync(user, context.HttpContext.RequestAborted); + }; + } + private Func OnOnChallengeJwtBearerEvent() + { + return context => + { + if (context.AuthenticateFailure != null) + throw context.AuthenticateFailure; + context.HandleResponse(); + context.Response.StatusCode = 401; + context.Response.ContentType = "application/json"; + var result = JsonSerializer.Serialize(new { message = "You are not Authorized" }); + return context.Response.WriteAsync(result); + }; + } +} + + diff --git a/src/Clean.Architecture.Web/Clean.Architecture.Web.csproj b/src/Clean.Architecture.Web/Clean.Architecture.Web.csproj index 132ffc379..900615b99 100644 --- a/src/Clean.Architecture.Web/Clean.Architecture.Web.csproj +++ b/src/Clean.Architecture.Web/Clean.Architecture.Web.csproj @@ -18,7 +18,6 @@ - diff --git a/src/Clean.Architecture.Web/Endpoints/ProjectEndpoints/Create.cs b/src/Clean.Architecture.Web/Endpoints/ProjectEndpoints/Create.cs index c10cb36dd..28dcf6c17 100644 --- a/src/Clean.Architecture.Web/Endpoints/ProjectEndpoints/Create.cs +++ b/src/Clean.Architecture.Web/Endpoints/ProjectEndpoints/Create.cs @@ -1,6 +1,7 @@ using Ardalis.ApiEndpoints; using Clean.Architecture.Core.ProjectAggregate; using Clean.Architecture.SharedKernel.Interfaces; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Swashbuckle.AspNetCore.Annotations; @@ -24,6 +25,7 @@ public Create(IRepository repository) OperationId = "Project.Create", Tags = new[] { "ProjectEndpoints" }) ] + [Authorize] public override async Task> HandleAsync( CreateProjectRequest request, CancellationToken cancellationToken = new()) diff --git a/src/Clean.Architecture.Web/Endpoints/UserEndpoints/Login.LoginUserRequest.cs b/src/Clean.Architecture.Web/Endpoints/UserEndpoints/Login.LoginUserRequest.cs index 1b8838c0f..49e9f12b6 100644 --- a/src/Clean.Architecture.Web/Endpoints/UserEndpoints/Login.LoginUserRequest.cs +++ b/src/Clean.Architecture.Web/Endpoints/UserEndpoints/Login.LoginUserRequest.cs @@ -10,7 +10,4 @@ public class LoginUserRequest [Required] public string Password { get; set; } = default!; - - [Required] - public string RefreshToken { get; set; } = default!; } diff --git a/src/Clean.Architecture.Web/Program.cs b/src/Clean.Architecture.Web/Program.cs index 88e4663bd..c59a82cc5 100644 --- a/src/Clean.Architecture.Web/Program.cs +++ b/src/Clean.Architecture.Web/Program.cs @@ -10,14 +10,6 @@ using FastEndpoints.ApiExplorer; using Microsoft.OpenApi.Models; using Serilog; -using Microsoft.AspNetCore.Identity; -using Clean.Architecture.Core.UserAggregate; -using Microsoft.AspNetCore.Authentication.JwtBearer; -using System.Text; -using Microsoft.IdentityModel.Tokens; -using Clean.Architecture.Core.Interfaces; -using System.Security.Claims; -using Clean.Architecture.SharedKernel.Utilities; using Clean.Architecture.Core.Services.Auth; var builder = WebApplication.CreateBuilder(args); @@ -120,110 +112,3 @@ } app.Run(); - - - - -public static class StartupSetup -{ - public static void AddCustomJwtAuthentication(this IServiceCollection services, JwtSettings settings) - { - services.AddAuthentication(options => - { - options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; - options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; - options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; - }) - .AddJwtBearer(options => - { - var secretKey = Encoding.UTF8.GetBytes(settings.SecretKey); - var encryptionKey = Encoding.UTF8.GetBytes(settings.EncryptKey); - - var validationParameters = new TokenValidationParameters - { - ClockSkew = TimeSpan.Zero, // default: 5 min - RequireSignedTokens = true, - - ValidateIssuerSigningKey = true, - IssuerSigningKey = new SymmetricSecurityKey(secretKey), - - RequireExpirationTime = true, - ValidateLifetime = true, - - ValidateAudience = true, //default : false - ValidAudience = settings.Audience, - - ValidateIssuer = true, //default : false - ValidIssuer = settings.Issuer, - - TokenDecryptionKey = new SymmetricSecurityKey(encryptionKey) - }; - - options.RequireHttpsMetadata = false; - options.SaveToken = true; - options.TokenValidationParameters = validationParameters; - - options.Events = new JwtBearerEvents - { - OnAuthenticationFailed = context => - { - if (context.Exception != null) - { - throw context.Exception; - } - - return Task.CompletedTask; - }, - OnTokenValidated = async context => - { - var signInManager = context.HttpContext.RequestServices.GetRequiredService>(); - var userRepository = context.HttpContext.RequestServices.GetRequiredService(); - - var claimsIdentity = context.Principal?.Identity as ClaimsIdentity; - if (claimsIdentity?.Claims?.Any() != true) - context.Fail("This token has no claims."); - - //Find user and token from database and perform your custom validation - var userId = claimsIdentity?.GetUserId(); - if (userId == null) - context.Fail("There is no UserId in claims."); - - var user = await userRepository.GetByIdAsync(userId.GetValueOrDefault(), context.HttpContext.RequestAborted); - - if (user == null) - context.Fail("User not found."); - - if (user != null && !user.IsActive) - context.Fail("User is not active."); - - await userRepository.UpdateLastLoginDateAsync(user!, context.HttpContext.RequestAborted); - }, - OnChallenge = context => - { - if (context.AuthenticateFailure != null) - throw context.AuthenticateFailure; - //throw new CleanArchAppException(ApiResultStatusCode.UnAuthorized, "Authenticate failure.", HttpStatusCode.Unauthorized, context.AuthenticateFailure, null); - //throw new CleanArchAppException(ApiResultStatusCode.UnAuthorized, "You are unauthorized to access this resource.", HttpStatusCode.Unauthorized); - throw new Exception("You are unauthorized to access this resource."); - } - }; - }); - } - public static void AddCustomIdentity(this IServiceCollection services, IdentitySettings settings) - { - services.AddIdentity(identityOptions => - { - //Password Settings - identityOptions.Password.RequireDigit = settings.PasswordRequireDigit; - identityOptions.Password.RequiredLength = settings.PasswordRequiredLength; - identityOptions.Password.RequireNonAlphanumeric = settings.PasswordRequireNonAlphanumeric; //#@! - identityOptions.Password.RequireUppercase = settings.PasswordRequireUppercase; - identityOptions.Password.RequireLowercase = settings.PasswordRequireLowercase; - - //UserName Settings - identityOptions.User.RequireUniqueEmail = settings.RequireUniqueEmail; - }) - .AddEntityFrameworkStores() - .AddDefaultTokenProviders(); - } -} From ea90fc0e11cd601086e657bd3a4e5e50104cb953 Mon Sep 17 00:00:00 2001 From: "TL\\anwarahm" Date: Sun, 1 Jan 2023 19:09:57 +0500 Subject: [PATCH 4/7] appSettings set to defaut values --- src/Clean.Architecture.Infrastructure/StartupSetup.cs | 2 +- src/Clean.Architecture.Web/Program.cs | 2 +- src/Clean.Architecture.Web/appsettings.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Clean.Architecture.Infrastructure/StartupSetup.cs b/src/Clean.Architecture.Infrastructure/StartupSetup.cs index 7ae70bd10..f370dfd23 100644 --- a/src/Clean.Architecture.Infrastructure/StartupSetup.cs +++ b/src/Clean.Architecture.Infrastructure/StartupSetup.cs @@ -19,7 +19,7 @@ public static class StartupSetup { public static void AddDbContext(this IServiceCollection services, string connectionString) => services.AddDbContext(options => - options.UseSqlServer(connectionString)); // will be created in web project root + options.UseSqlite(connectionString)); // will be created in web project root public static void AddCustomJwtAuthentication(this IServiceCollection services, JwtSettings settings) { diff --git a/src/Clean.Architecture.Web/Program.cs b/src/Clean.Architecture.Web/Program.cs index c59a82cc5..14d217083 100644 --- a/src/Clean.Architecture.Web/Program.cs +++ b/src/Clean.Architecture.Web/Program.cs @@ -24,7 +24,7 @@ options.MinimumSameSitePolicy = SameSiteMode.None; }); -string? connectionString = builder.Configuration.GetConnectionString("DefaultConnection"); //Configuration.GetConnectionString("DefaultConnection"); +string? connectionString = builder.Configuration.GetConnectionString("SqliteConnection"); //Configuration.GetConnectionString("DefaultConnection"); builder.Services.AddDbContext(connectionString!); diff --git a/src/Clean.Architecture.Web/appsettings.json b/src/Clean.Architecture.Web/appsettings.json index 0b39eeead..7647e16d9 100644 --- a/src/Clean.Architecture.Web/appsettings.json +++ b/src/Clean.Architecture.Web/appsettings.json @@ -1,6 +1,6 @@ { "ConnectionStrings": { - "DefaultConnection": "Server=CRLHL-ANWARAHM1\\SQLEXPRESS;Database=cleanarchitecture;Trusted_Connection=True;MultipleActiveResultSets=true;TrustServerCertificate=True", + "DefaultConnection": "Server=(localdb)\\v11.0;Database=cleanarchitecture;Database=cleanarchitecture;Trusted_Connection=True;", "SqliteConnection": "Data Source=database.sqlite" }, "Serilog": { From 5d1a8ff3de1a1a6cff08a12ecf8fa0f0df36bb95 Mon Sep 17 00:00:00 2001 From: "TL\\anwarahm" Date: Sun, 16 Apr 2023 20:34:26 +0500 Subject: [PATCH 5/7] Identity: changes moved to Infrastructure --- .editorconfig | 16 +- .../Clean.Architecture.Core.csproj | 1 - .../DefaultCoreModule.cs | 4 - .../Interfaces/IUserRepository.cs | 13 - .../UserAggregate/Role.cs | 13 - .../Clean.Architecture.Infrastructure.csproj | 1 + .../Data/AppDbContext.cs | 4 +- .../DefaultInfrastructureModule.cs | 10 +- .../Identity/AppIdentityDbContext.cs | 22 + .../Identity}/Jwt/AccessToken.cs | 9 +- .../Identity}/Jwt/IJwtService.cs | 3 +- .../Identity}/Jwt/JwtService.cs | 3 +- .../Identity/Role.cs | 8 + .../Identity}/SiteSettings.cs | 2 +- .../Identity}/User.cs | 10 +- ...6152439_IdentityInitialization.Designer.cs | 385 ++++++++++++++++++ .../20230416152439_IdentityInitialization.cs | 295 ++++++++++++++ .../AppIdentityDbContextModelSnapshot.cs | 382 +++++++++++++++++ .../StartupSetup.cs | 28 +- .../UserRepository.cs | 24 -- .../Endpoints/UserEndpoints/Login.cs | 4 +- .../UserEndpoints/SignUp.SignUpUserRequest.cs | 2 +- .../Endpoints/UserEndpoints/SignUp.cs | 3 +- src/Clean.Architecture.Web/Program.cs | 6 +- src/Clean.Architecture.Web/appsettings.json | 2 +- 25 files changed, 1135 insertions(+), 115 deletions(-) delete mode 100644 src/Clean.Architecture.Core/Interfaces/IUserRepository.cs delete mode 100644 src/Clean.Architecture.Core/UserAggregate/Role.cs create mode 100644 src/Clean.Architecture.Infrastructure/Identity/AppIdentityDbContext.cs rename src/{Clean.Architecture.Core/Services/Auth => Clean.Architecture.Infrastructure/Identity}/Jwt/AccessToken.cs (64%) rename src/{Clean.Architecture.Core/Services/Auth => Clean.Architecture.Infrastructure/Identity}/Jwt/IJwtService.cs (71%) rename src/{Clean.Architecture.Core/Services/Auth => Clean.Architecture.Infrastructure/Identity}/Jwt/JwtService.cs (97%) create mode 100644 src/Clean.Architecture.Infrastructure/Identity/Role.cs rename src/{Clean.Architecture.Core/Services/Auth => Clean.Architecture.Infrastructure/Identity}/SiteSettings.cs (94%) rename src/{Clean.Architecture.Core/UserAggregate => Clean.Architecture.Infrastructure/Identity}/User.cs (61%) create mode 100644 src/Clean.Architecture.Infrastructure/Migrations/20230416152439_IdentityInitialization.Designer.cs create mode 100644 src/Clean.Architecture.Infrastructure/Migrations/20230416152439_IdentityInitialization.cs create mode 100644 src/Clean.Architecture.Infrastructure/Migrations/AppIdentityDbContextModelSnapshot.cs delete mode 100644 src/Clean.Architecture.Infrastructure/UserRepository.cs diff --git a/.editorconfig b/.editorconfig index cd17d141f..b84780595 100644 --- a/.editorconfig +++ b/.editorconfig @@ -16,7 +16,7 @@ indent_size = 2 # Code files [*.{cs,csx,vb,vbx}] -indent_size =2 +indent_size = 2 insert_final_newline = true charset = utf-8-bom ############################### @@ -61,18 +61,20 @@ dotnet_naming_style.pascal_case_style.capitalization = pascal_case # Use PascalCase for constant fields dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields -dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style +dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style dotnet_naming_symbols.constant_fields.applicable_kinds = field dotnet_naming_symbols.constant_fields.applicable_accessibilities = * dotnet_naming_symbols.constant_fields.required_modifiers = const -tab_width=2 +tab_width= 2 dotnet_naming_rule.private_members_with_underscore.symbols = private_fields -dotnet_naming_rule.private_members_with_underscore.style = prefix_underscore +dotnet_naming_rule.private_members_with_underscore.style = prefix_underscore dotnet_naming_rule.private_members_with_underscore.severity = suggestion dotnet_naming_symbols.private_fields.applicable_kinds = field dotnet_naming_symbols.private_fields.applicable_accessibilities = private dotnet_naming_style.prefix_underscore.capitalization = camel_case dotnet_naming_style.prefix_underscore.required_prefix = _ +dotnet_style_operator_placement_when_wrapping = beginning_of_line +end_of_line = crlf ############################### # C# Coding Conventions # ############################### @@ -134,6 +136,12 @@ csharp_space_between_method_call_empty_parameter_list_parentheses = false # Wrapping preferences csharp_preserve_single_line_statements = true csharp_preserve_single_line_blocks = true +csharp_using_directive_placement = outside_namespace:silent +csharp_prefer_simple_using_statement = true:suggestion +csharp_style_prefer_method_group_conversion = true:silent +csharp_style_prefer_top_level_statements = true:silent +csharp_style_expression_bodied_lambdas = true:silent +csharp_style_expression_bodied_local_functions = false:silent ############################### # VB Coding Conventions # ############################### diff --git a/src/Clean.Architecture.Core/Clean.Architecture.Core.csproj b/src/Clean.Architecture.Core/Clean.Architecture.Core.csproj index 6b27c23b2..934d828fb 100644 --- a/src/Clean.Architecture.Core/Clean.Architecture.Core.csproj +++ b/src/Clean.Architecture.Core/Clean.Architecture.Core.csproj @@ -13,7 +13,6 @@ - diff --git a/src/Clean.Architecture.Core/DefaultCoreModule.cs b/src/Clean.Architecture.Core/DefaultCoreModule.cs index 171659057..1daac3d74 100644 --- a/src/Clean.Architecture.Core/DefaultCoreModule.cs +++ b/src/Clean.Architecture.Core/DefaultCoreModule.cs @@ -1,7 +1,6 @@ using Autofac; using Clean.Architecture.Core.Interfaces; using Clean.Architecture.Core.Services; -using Clean.Architecture.Core.Services.Auth.Jwt; namespace Clean.Architecture.Core; @@ -14,8 +13,5 @@ protected override void Load(ContainerBuilder builder) builder.RegisterType() .As().InstancePerLifetimeScope(); - - builder.RegisterType() - .As().InstancePerLifetimeScope(); } } diff --git a/src/Clean.Architecture.Core/Interfaces/IUserRepository.cs b/src/Clean.Architecture.Core/Interfaces/IUserRepository.cs deleted file mode 100644 index 191183e8c..000000000 --- a/src/Clean.Architecture.Core/Interfaces/IUserRepository.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Clean.Architecture.Core.UserAggregate; - -namespace Clean.Architecture.Core.Interfaces; -public interface IUserRepository -{ - Task UpdateLastLoginDateAsync(User user, CancellationToken cancellationToken); - Task GetByIdAsync(int userId, CancellationToken cancellationToken); -} diff --git a/src/Clean.Architecture.Core/UserAggregate/Role.cs b/src/Clean.Architecture.Core/UserAggregate/Role.cs deleted file mode 100644 index ec5aec5eb..000000000 --- a/src/Clean.Architecture.Core/UserAggregate/Role.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Clean.Architecture.SharedKernel.Interfaces; -using Microsoft.AspNetCore.Identity; - -namespace Clean.Architecture.Core.UserAggregate; -public class Role : IdentityRole, IAggregateRoot -{ - public string Description { get; set; } = default!; -} diff --git a/src/Clean.Architecture.Infrastructure/Clean.Architecture.Infrastructure.csproj b/src/Clean.Architecture.Infrastructure/Clean.Architecture.Infrastructure.csproj index df07409a9..6dd6af0d0 100644 --- a/src/Clean.Architecture.Infrastructure/Clean.Architecture.Infrastructure.csproj +++ b/src/Clean.Architecture.Infrastructure/Clean.Architecture.Infrastructure.csproj @@ -15,6 +15,7 @@ + diff --git a/src/Clean.Architecture.Infrastructure/Data/AppDbContext.cs b/src/Clean.Architecture.Infrastructure/Data/AppDbContext.cs index 28be3f590..37fa3ccfe 100644 --- a/src/Clean.Architecture.Infrastructure/Data/AppDbContext.cs +++ b/src/Clean.Architecture.Infrastructure/Data/AppDbContext.cs @@ -1,15 +1,13 @@ using System.Reflection; using Clean.Architecture.Core.ContributorAggregate; using Clean.Architecture.Core.ProjectAggregate; -using Clean.Architecture.Core.UserAggregate; using Clean.Architecture.SharedKernel; using Clean.Architecture.SharedKernel.Interfaces; -using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; namespace Clean.Architecture.Infrastructure.Data; -public class AppDbContext : IdentityDbContext +public class AppDbContext : DbContext { private readonly IDomainEventDispatcher? _dispatcher; diff --git a/src/Clean.Architecture.Infrastructure/DefaultInfrastructureModule.cs b/src/Clean.Architecture.Infrastructure/DefaultInfrastructureModule.cs index 9a7ce49d0..5ca135af2 100644 --- a/src/Clean.Architecture.Infrastructure/DefaultInfrastructureModule.cs +++ b/src/Clean.Architecture.Infrastructure/DefaultInfrastructureModule.cs @@ -3,6 +3,7 @@ using Clean.Architecture.Core.Interfaces; using Clean.Architecture.Core.ProjectAggregate; using Clean.Architecture.Infrastructure.Data; +using Clean.Architecture.Infrastructure.Identity.Jwt; using Clean.Architecture.SharedKernel; using Clean.Architecture.SharedKernel.Interfaces; using MediatR; @@ -54,6 +55,9 @@ protected override void Load(ContainerBuilder builder) private void RegisterCommonDependencies(ContainerBuilder builder) { + builder.RegisterType() + .As().InstancePerLifetimeScope(); + builder.RegisterGeneric(typeof(EfRepository<>)) .As(typeof(IRepository<>)) .As(typeof(IReadRepository<>)) @@ -69,11 +73,7 @@ private void RegisterCommonDependencies(ContainerBuilder builder) .As() .InstancePerLifetimeScope(); - builder - .RegisterType() - .As() - .InstancePerLifetimeScope(); - + //builder.Register(context => //{ // var c = context.Resolve(); diff --git a/src/Clean.Architecture.Infrastructure/Identity/AppIdentityDbContext.cs b/src/Clean.Architecture.Infrastructure/Identity/AppIdentityDbContext.cs new file mode 100644 index 000000000..d2f3f26a5 --- /dev/null +++ b/src/Clean.Architecture.Infrastructure/Identity/AppIdentityDbContext.cs @@ -0,0 +1,22 @@ +using System.Reflection.Emit; +using System.Reflection; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; + +namespace Clean.Architecture.Infrastructure.Identity; +public class AppIdentityDbContext : IdentityDbContext +{ + public AppIdentityDbContext(DbContextOptions options) + : base(options) + { + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly()); + // Customize the ASP.NET Identity model and override the defaults if needed. + // For example, you can rename the ASP.NET Identity table names and more. + // Add your customizations after calling base.OnModelCreating(builder); + } +} diff --git a/src/Clean.Architecture.Core/Services/Auth/Jwt/AccessToken.cs b/src/Clean.Architecture.Infrastructure/Identity/Jwt/AccessToken.cs similarity index 64% rename from src/Clean.Architecture.Core/Services/Auth/Jwt/AccessToken.cs rename to src/Clean.Architecture.Infrastructure/Identity/Jwt/AccessToken.cs index f4fb6f190..9ddbec867 100644 --- a/src/Clean.Architecture.Core/Services/Auth/Jwt/AccessToken.cs +++ b/src/Clean.Architecture.Infrastructure/Identity/Jwt/AccessToken.cs @@ -1,11 +1,6 @@ -using System; -using System.Collections.Generic; -using System.IdentityModel.Tokens.Jwt; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.IdentityModel.Tokens.Jwt; -namespace Clean.Architecture.Core.Services.Auth.Jwt; +namespace Clean.Architecture.Infrastructure.Identity.Jwt; public class AccessToken { public string Access_Token { get; set; } diff --git a/src/Clean.Architecture.Core/Services/Auth/Jwt/IJwtService.cs b/src/Clean.Architecture.Infrastructure/Identity/Jwt/IJwtService.cs similarity index 71% rename from src/Clean.Architecture.Core/Services/Auth/Jwt/IJwtService.cs rename to src/Clean.Architecture.Infrastructure/Identity/Jwt/IJwtService.cs index b2466fa0a..590d12d1c 100644 --- a/src/Clean.Architecture.Core/Services/Auth/Jwt/IJwtService.cs +++ b/src/Clean.Architecture.Infrastructure/Identity/Jwt/IJwtService.cs @@ -3,9 +3,8 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -using Clean.Architecture.Core.UserAggregate; -namespace Clean.Architecture.Core.Services.Auth.Jwt; +namespace Clean.Architecture.Infrastructure.Identity.Jwt; public interface IJwtService { Task GenerateAsync(User user); diff --git a/src/Clean.Architecture.Core/Services/Auth/Jwt/JwtService.cs b/src/Clean.Architecture.Infrastructure/Identity/Jwt/JwtService.cs similarity index 97% rename from src/Clean.Architecture.Core/Services/Auth/Jwt/JwtService.cs rename to src/Clean.Architecture.Infrastructure/Identity/Jwt/JwtService.cs index a91271912..2f1be7943 100644 --- a/src/Clean.Architecture.Core/Services/Auth/Jwt/JwtService.cs +++ b/src/Clean.Architecture.Infrastructure/Identity/Jwt/JwtService.cs @@ -1,12 +1,11 @@ using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Text; -using Clean.Architecture.Core.UserAggregate; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; -namespace Clean.Architecture.Core.Services.Auth.Jwt; +namespace Clean.Architecture.Infrastructure.Identity.Jwt; public class JwtService : IJwtService { private readonly SiteSettings _siteSetting; diff --git a/src/Clean.Architecture.Infrastructure/Identity/Role.cs b/src/Clean.Architecture.Infrastructure/Identity/Role.cs new file mode 100644 index 000000000..42bc43637 --- /dev/null +++ b/src/Clean.Architecture.Infrastructure/Identity/Role.cs @@ -0,0 +1,8 @@ +using Clean.Architecture.SharedKernel.Interfaces; +using Microsoft.AspNetCore.Identity; + +namespace Clean.Architecture.Infrastructure.Identity; +public class Role : IdentityRole, IAggregateRoot +{ + public string Description { get; set; } = default!; +} diff --git a/src/Clean.Architecture.Core/Services/Auth/SiteSettings.cs b/src/Clean.Architecture.Infrastructure/Identity/SiteSettings.cs similarity index 94% rename from src/Clean.Architecture.Core/Services/Auth/SiteSettings.cs rename to src/Clean.Architecture.Infrastructure/Identity/SiteSettings.cs index 91ec866b4..4400fb1a8 100644 --- a/src/Clean.Architecture.Core/Services/Auth/SiteSettings.cs +++ b/src/Clean.Architecture.Infrastructure/Identity/SiteSettings.cs @@ -4,7 +4,7 @@ using System.Text; using System.Threading.Tasks; -namespace Clean.Architecture.Core.Services.Auth; +namespace Clean.Architecture.Infrastructure.Identity; public class SiteSettings { public JwtSettings JwtSettings { get; set; } = default!; diff --git a/src/Clean.Architecture.Core/UserAggregate/User.cs b/src/Clean.Architecture.Infrastructure/Identity/User.cs similarity index 61% rename from src/Clean.Architecture.Core/UserAggregate/User.cs rename to src/Clean.Architecture.Infrastructure/Identity/User.cs index fd6f132a5..2af38ce40 100644 --- a/src/Clean.Architecture.Core/UserAggregate/User.cs +++ b/src/Clean.Architecture.Infrastructure/Identity/User.cs @@ -1,13 +1,7 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.CompilerServices; -using System.Text; -using System.Threading.Tasks; -using Clean.Architecture.SharedKernel.Interfaces; +using Clean.Architecture.SharedKernel.Interfaces; using Microsoft.AspNetCore.Identity; -namespace Clean.Architecture.Core.UserAggregate; +namespace Clean.Architecture.Infrastructure.Identity; public class User : IdentityUser, IAggregateRoot { public User() diff --git a/src/Clean.Architecture.Infrastructure/Migrations/20230416152439_IdentityInitialization.Designer.cs b/src/Clean.Architecture.Infrastructure/Migrations/20230416152439_IdentityInitialization.Designer.cs new file mode 100644 index 000000000..2ca764810 --- /dev/null +++ b/src/Clean.Architecture.Infrastructure/Migrations/20230416152439_IdentityInitialization.Designer.cs @@ -0,0 +1,385 @@ +// +using System; +using Clean.Architecture.Infrastructure.Identity; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Clean.Architecture.Infrastructure.Migrations +{ + [DbContext(typeof(AppIdentityDbContext))] + [Migration("20230416152439_IdentityInitialization")] + partial class IdentityInitialization + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.4") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Clean.Architecture.Core.ContributorAggregate.Contributor", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.HasKey("Id"); + + b.ToTable("Contributor"); + }); + + modelBuilder.Entity("Clean.Architecture.Core.ProjectAggregate.Project", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Priority") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("Project"); + }); + + modelBuilder.Entity("Clean.Architecture.Core.ProjectAggregate.ToDoItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ContributorId") + .HasColumnType("int"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsDone") + .HasColumnType("bit"); + + b.Property("ProjectId") + .HasColumnType("int"); + + b.Property("Title") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("ProjectId"); + + b.ToTable("ToDoItem"); + }); + + modelBuilder.Entity("Clean.Architecture.Infrastructure.Identity.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Clean.Architecture.Infrastructure.Identity.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("Age") + .HasColumnType("int"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("FullName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Gender") + .HasColumnType("int"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("LastLoginDate") + .HasColumnType("datetimeoffset"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("int"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("int"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Clean.Architecture.Core.ProjectAggregate.ToDoItem", b => + { + b.HasOne("Clean.Architecture.Core.ProjectAggregate.Project", null) + .WithMany("Items") + .HasForeignKey("ProjectId"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Clean.Architecture.Infrastructure.Identity.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Clean.Architecture.Infrastructure.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Clean.Architecture.Infrastructure.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Clean.Architecture.Infrastructure.Identity.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Clean.Architecture.Infrastructure.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Clean.Architecture.Infrastructure.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Clean.Architecture.Core.ProjectAggregate.Project", b => + { + b.Navigation("Items"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Clean.Architecture.Infrastructure/Migrations/20230416152439_IdentityInitialization.cs b/src/Clean.Architecture.Infrastructure/Migrations/20230416152439_IdentityInitialization.cs new file mode 100644 index 000000000..cc1c390ae --- /dev/null +++ b/src/Clean.Architecture.Infrastructure/Migrations/20230416152439_IdentityInitialization.cs @@ -0,0 +1,295 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Clean.Architecture.Infrastructure.Migrations +{ + /// + public partial class IdentityInitialization : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "AspNetRoles", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + Description = table.Column(type: "nvarchar(max)", nullable: false), + Name = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + NormalizedName = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + ConcurrencyStamp = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoles", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetUsers", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + FullName = table.Column(type: "nvarchar(max)", nullable: false), + Age = table.Column(type: "int", nullable: false), + Gender = table.Column(type: "int", nullable: false), + IsActive = table.Column(type: "bit", nullable: false), + LastLoginDate = table.Column(type: "datetimeoffset", nullable: true), + UserName = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + NormalizedUserName = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + Email = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + NormalizedEmail = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + EmailConfirmed = table.Column(type: "bit", nullable: false), + PasswordHash = table.Column(type: "nvarchar(max)", nullable: true), + SecurityStamp = table.Column(type: "nvarchar(max)", nullable: true), + ConcurrencyStamp = table.Column(type: "nvarchar(max)", nullable: true), + PhoneNumber = table.Column(type: "nvarchar(max)", nullable: true), + PhoneNumberConfirmed = table.Column(type: "bit", nullable: false), + TwoFactorEnabled = table.Column(type: "bit", nullable: false), + LockoutEnd = table.Column(type: "datetimeoffset", nullable: true), + LockoutEnabled = table.Column(type: "bit", nullable: false), + AccessFailedCount = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUsers", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Contributor", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + Name = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Contributor", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Project", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + Name = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: false), + Priority = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Project", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetRoleClaims", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + RoleId = table.Column(type: "int", nullable: false), + ClaimType = table.Column(type: "nvarchar(max)", nullable: true), + ClaimValue = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoleClaims", x => x.Id); + table.ForeignKey( + name: "FK_AspNetRoleClaims_AspNetRoles_RoleId", + column: x => x.RoleId, + principalTable: "AspNetRoles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserClaims", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + UserId = table.Column(type: "int", nullable: false), + ClaimType = table.Column(type: "nvarchar(max)", nullable: true), + ClaimValue = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserClaims", x => x.Id); + table.ForeignKey( + name: "FK_AspNetUserClaims_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserLogins", + columns: table => new + { + LoginProvider = table.Column(type: "nvarchar(450)", nullable: false), + ProviderKey = table.Column(type: "nvarchar(450)", nullable: false), + ProviderDisplayName = table.Column(type: "nvarchar(max)", nullable: true), + UserId = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey }); + table.ForeignKey( + name: "FK_AspNetUserLogins_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserRoles", + columns: table => new + { + UserId = table.Column(type: "int", nullable: false), + RoleId = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId }); + table.ForeignKey( + name: "FK_AspNetUserRoles_AspNetRoles_RoleId", + column: x => x.RoleId, + principalTable: "AspNetRoles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_AspNetUserRoles_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserTokens", + columns: table => new + { + UserId = table.Column(type: "int", nullable: false), + LoginProvider = table.Column(type: "nvarchar(450)", nullable: false), + Name = table.Column(type: "nvarchar(450)", nullable: false), + Value = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name }); + table.ForeignKey( + name: "FK_AspNetUserTokens_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ToDoItem", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + Title = table.Column(type: "nvarchar(max)", nullable: false), + Description = table.Column(type: "nvarchar(max)", nullable: false), + ContributorId = table.Column(type: "int", nullable: true), + IsDone = table.Column(type: "bit", nullable: false), + ProjectId = table.Column(type: "int", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_ToDoItem", x => x.Id); + table.ForeignKey( + name: "FK_ToDoItem_Project_ProjectId", + column: x => x.ProjectId, + principalTable: "Project", + principalColumn: "Id"); + }); + + migrationBuilder.CreateIndex( + name: "IX_AspNetRoleClaims_RoleId", + table: "AspNetRoleClaims", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "RoleNameIndex", + table: "AspNetRoles", + column: "NormalizedName", + unique: true, + filter: "[NormalizedName] IS NOT NULL"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserClaims_UserId", + table: "AspNetUserClaims", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserLogins_UserId", + table: "AspNetUserLogins", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserRoles_RoleId", + table: "AspNetUserRoles", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "EmailIndex", + table: "AspNetUsers", + column: "NormalizedEmail"); + + migrationBuilder.CreateIndex( + name: "UserNameIndex", + table: "AspNetUsers", + column: "NormalizedUserName", + unique: true, + filter: "[NormalizedUserName] IS NOT NULL"); + + migrationBuilder.CreateIndex( + name: "IX_ToDoItem_ProjectId", + table: "ToDoItem", + column: "ProjectId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AspNetRoleClaims"); + + migrationBuilder.DropTable( + name: "AspNetUserClaims"); + + migrationBuilder.DropTable( + name: "AspNetUserLogins"); + + migrationBuilder.DropTable( + name: "AspNetUserRoles"); + + migrationBuilder.DropTable( + name: "AspNetUserTokens"); + + migrationBuilder.DropTable( + name: "Contributor"); + + migrationBuilder.DropTable( + name: "ToDoItem"); + + migrationBuilder.DropTable( + name: "AspNetRoles"); + + migrationBuilder.DropTable( + name: "AspNetUsers"); + + migrationBuilder.DropTable( + name: "Project"); + } + } +} diff --git a/src/Clean.Architecture.Infrastructure/Migrations/AppIdentityDbContextModelSnapshot.cs b/src/Clean.Architecture.Infrastructure/Migrations/AppIdentityDbContextModelSnapshot.cs new file mode 100644 index 000000000..1fbb1d7a4 --- /dev/null +++ b/src/Clean.Architecture.Infrastructure/Migrations/AppIdentityDbContextModelSnapshot.cs @@ -0,0 +1,382 @@ +// +using System; +using Clean.Architecture.Infrastructure.Identity; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Clean.Architecture.Infrastructure.Migrations +{ + [DbContext(typeof(AppIdentityDbContext))] + partial class AppIdentityDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.4") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Clean.Architecture.Core.ContributorAggregate.Contributor", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.HasKey("Id"); + + b.ToTable("Contributor"); + }); + + modelBuilder.Entity("Clean.Architecture.Core.ProjectAggregate.Project", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Priority") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("Project"); + }); + + modelBuilder.Entity("Clean.Architecture.Core.ProjectAggregate.ToDoItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ContributorId") + .HasColumnType("int"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsDone") + .HasColumnType("bit"); + + b.Property("ProjectId") + .HasColumnType("int"); + + b.Property("Title") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("ProjectId"); + + b.ToTable("ToDoItem"); + }); + + modelBuilder.Entity("Clean.Architecture.Infrastructure.Identity.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Clean.Architecture.Infrastructure.Identity.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("Age") + .HasColumnType("int"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("FullName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Gender") + .HasColumnType("int"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("LastLoginDate") + .HasColumnType("datetimeoffset"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("int"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("int"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Clean.Architecture.Core.ProjectAggregate.ToDoItem", b => + { + b.HasOne("Clean.Architecture.Core.ProjectAggregate.Project", null) + .WithMany("Items") + .HasForeignKey("ProjectId"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Clean.Architecture.Infrastructure.Identity.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Clean.Architecture.Infrastructure.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Clean.Architecture.Infrastructure.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Clean.Architecture.Infrastructure.Identity.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Clean.Architecture.Infrastructure.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Clean.Architecture.Infrastructure.Identity.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Clean.Architecture.Core.ProjectAggregate.Project", b => + { + b.Navigation("Items"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Clean.Architecture.Infrastructure/StartupSetup.cs b/src/Clean.Architecture.Infrastructure/StartupSetup.cs index f370dfd23..497203161 100644 --- a/src/Clean.Architecture.Infrastructure/StartupSetup.cs +++ b/src/Clean.Architecture.Infrastructure/StartupSetup.cs @@ -1,8 +1,5 @@ using System.Security.Claims; using System.Text; -using Clean.Architecture.Core.Interfaces; -using Clean.Architecture.Core.Services.Auth; -using Clean.Architecture.Core.UserAggregate; using Clean.Architecture.Infrastructure.Data; using Clean.Architecture.SharedKernel.Utilities; using Microsoft.AspNetCore.Authentication.JwtBearer; @@ -12,6 +9,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.IdentityModel.Tokens; using System.Text.Json; +using Clean.Architecture.Infrastructure.Identity; namespace Clean.Architecture.Infrastructure; @@ -63,8 +61,10 @@ public static void AddCustomJwtAuthentication(this IServiceCollection services, }); } - public static void AddCustomIdentity(this IServiceCollection services, IdentitySettings settings) + public static void AddCustomIdentity(this IServiceCollection services, IdentitySettings settings, string connectionString) { + services.AddDbContext(options => + options.UseSqlServer(connectionString)); services.AddIdentity(identityOptions => { //Password Settings @@ -77,7 +77,7 @@ public static void AddCustomIdentity(this IServiceCollection services, IdentityS //UserName Settings identityOptions.User.RequireUniqueEmail = settings.RequireUniqueEmail; }) - .AddEntityFrameworkStores() + .AddEntityFrameworkStores() .AddDefaultTokenProviders(); } } @@ -110,35 +110,25 @@ private Func OnTokenValidatedJwtBearerEvent() { return async context => { - var signInManager = context.HttpContext.RequestServices.GetRequiredService>(); - var userRepository = context.HttpContext.RequestServices.GetRequiredService(); + var signInManager = context.HttpContext.RequestServices.GetRequiredService>(); var claimsIdentity = context.Principal!.Identity as ClaimsIdentity; if (claimsIdentity!.Claims?.Any() != true) context.Fail("This token has no claims."); - //var securityStamp = claimsIdentity.FindFirstValue(new ClaimsIdentityOptions().SecurityStampClaimType); - //if (!securityStamp.HasValue()) - // context.Fail("This token has no security stamp"); - //Get UserId and check if user exists in db var userId = claimsIdentity.GetUserId(); if (userId == 0) context.Fail("UserId has no value in claims."); - var user = await userRepository.GetByIdAsync(userId, context.HttpContext.RequestAborted); + var user = await signInManager.FindByIdAsync(userId.ToString()); if (user == null) context.Fail("User not found."); - //if (user.SecurityStamp != Guid.Parse(securityStamp)) - // context.Fail("Token security stamp is not valid."); - - //var validatedUser = await signInManager.ValidateSecurityStampAsync(context.Principal); - //if (validatedUser == null) - // context.Fail("Token security stamp is not valid."); if (!user!.IsActive) context.Fail("User is not active."); - await userRepository.UpdateLastLoginDateAsync(user, context.HttpContext.RequestAborted); + user.LastLoginDate = DateTime.Now; + await signInManager.UpdateAsync(user); }; } private Func OnOnChallengeJwtBearerEvent() diff --git a/src/Clean.Architecture.Infrastructure/UserRepository.cs b/src/Clean.Architecture.Infrastructure/UserRepository.cs deleted file mode 100644 index 29a7cc1e7..000000000 --- a/src/Clean.Architecture.Infrastructure/UserRepository.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Clean.Architecture.Core.Interfaces; -using Clean.Architecture.Core.UserAggregate; -using Clean.Architecture.SharedKernel.Interfaces; - -namespace Clean.Architecture.Infrastructure; -public class UserRepository : IUserRepository -{ - readonly IRepository _repository; - public UserRepository(IRepository repository) - { - _repository = repository; - } - - public async Task GetByIdAsync(int userId, CancellationToken cancellationToken) - { - return await _repository.GetByIdAsync(userId, cancellationToken); - } - - public async Task UpdateLastLoginDateAsync(User user, CancellationToken cancellationToken) - { - user.LastLoginDate = DateTime.Now; - await _repository.UpdateAsync(user, cancellationToken); - } -} diff --git a/src/Clean.Architecture.Web/Endpoints/UserEndpoints/Login.cs b/src/Clean.Architecture.Web/Endpoints/UserEndpoints/Login.cs index 91291ecc8..650de5af7 100644 --- a/src/Clean.Architecture.Web/Endpoints/UserEndpoints/Login.cs +++ b/src/Clean.Architecture.Web/Endpoints/UserEndpoints/Login.cs @@ -1,5 +1,5 @@ -using Clean.Architecture.Core.Services.Auth.Jwt; -using Clean.Architecture.Core.UserAggregate; +using Clean.Architecture.Infrastructure.Identity; +using Clean.Architecture.Infrastructure.Identity.Jwt; using FastEndpoints; using Microsoft.AspNetCore.Identity; diff --git a/src/Clean.Architecture.Web/Endpoints/UserEndpoints/SignUp.SignUpUserRequest.cs b/src/Clean.Architecture.Web/Endpoints/UserEndpoints/SignUp.SignUpUserRequest.cs index 9a42f6c0f..2611571ed 100644 --- a/src/Clean.Architecture.Web/Endpoints/UserEndpoints/SignUp.SignUpUserRequest.cs +++ b/src/Clean.Architecture.Web/Endpoints/UserEndpoints/SignUp.SignUpUserRequest.cs @@ -1,5 +1,5 @@ using System.ComponentModel.DataAnnotations; -using Clean.Architecture.Core.UserAggregate; +using Clean.Architecture.Infrastructure.Identity; namespace Clean.Architecture.Web.Endpoints.UserEndpoints; diff --git a/src/Clean.Architecture.Web/Endpoints/UserEndpoints/SignUp.cs b/src/Clean.Architecture.Web/Endpoints/UserEndpoints/SignUp.cs index 110179d2f..54bc42522 100644 --- a/src/Clean.Architecture.Web/Endpoints/UserEndpoints/SignUp.cs +++ b/src/Clean.Architecture.Web/Endpoints/UserEndpoints/SignUp.cs @@ -1,5 +1,4 @@ -using Clean.Architecture.Core.UserAggregate; -using Clean.Architecture.SharedKernel.Interfaces; +using Clean.Architecture.Infrastructure.Identity; using FastEndpoints; using Microsoft.AspNetCore.Identity; diff --git a/src/Clean.Architecture.Web/Program.cs b/src/Clean.Architecture.Web/Program.cs index 22a2acbcc..6f6a1f5bb 100644 --- a/src/Clean.Architecture.Web/Program.cs +++ b/src/Clean.Architecture.Web/Program.cs @@ -10,7 +10,7 @@ using FastEndpoints.ApiExplorer; using Microsoft.OpenApi.Models; using Serilog; -using Clean.Architecture.Core.Services.Auth; +using Clean.Architecture.Infrastructure.Identity; var builder = WebApplication.CreateBuilder(args); @@ -24,7 +24,7 @@ options.MinimumSameSitePolicy = SameSiteMode.None; }); -string? connectionString = builder.Configuration.GetConnectionString("SqliteConnection"); //Configuration.GetConnectionString("DefaultConnection"); +string? connectionString = builder.Configuration.GetConnectionString("DefaultConnection"); //Configuration.GetConnectionString("DefaultConnection"); builder.Services.AddDbContext(connectionString!); @@ -41,7 +41,7 @@ builder.Services.Configure(builder.Configuration.GetSection(nameof(SiteSettings))); var sitSettings = builder.Configuration.GetSection(nameof(SiteSettings)).Get(); -builder.Services.AddCustomIdentity(sitSettings!.IdentitySettings); +builder.Services.AddCustomIdentity(sitSettings!.IdentitySettings, connectionString!); builder.Services.AddCustomJwtAuthentication(sitSettings!.JwtSettings); // add list services for diagnostic purposes - see https://github.com/ardalis/AspNetCoreStartupServices diff --git a/src/Clean.Architecture.Web/appsettings.json b/src/Clean.Architecture.Web/appsettings.json index 7647e16d9..5798eba1f 100644 --- a/src/Clean.Architecture.Web/appsettings.json +++ b/src/Clean.Architecture.Web/appsettings.json @@ -1,6 +1,6 @@ { "ConnectionStrings": { - "DefaultConnection": "Server=(localdb)\\v11.0;Database=cleanarchitecture;Database=cleanarchitecture;Trusted_Connection=True;", + "DefaultConnection": "Server=CRLHL-ANWARAHM1;Database=cleanarchitecture;Trusted_Connection=True;TrustServerCertificate=True", "SqliteConnection": "Data Source=database.sqlite" }, "Serilog": { From a1a83c7ec1f38ad1b238b4a005c1938ba4c62109 Mon Sep 17 00:00:00 2001 From: "TL\\anwarahm" Date: Sun, 16 Apr 2023 21:06:11 +0500 Subject: [PATCH 6/7] Code Rub: checks added and credential in apsettings removed --- .../Identity/Jwt/JwtService.cs | 6 +++--- src/Clean.Architecture.Infrastructure/StartupSetup.cs | 4 ++++ src/Clean.Architecture.Web/Program.cs | 2 +- src/Clean.Architecture.Web/appsettings.json | 2 +- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/Clean.Architecture.Infrastructure/Identity/Jwt/JwtService.cs b/src/Clean.Architecture.Infrastructure/Identity/Jwt/JwtService.cs index 2f1be7943..36cf7aa27 100644 --- a/src/Clean.Architecture.Infrastructure/Identity/Jwt/JwtService.cs +++ b/src/Clean.Architecture.Infrastructure/Identity/Jwt/JwtService.cs @@ -25,8 +25,8 @@ public async Task GenerateAsync(User user) //We can use EncryptingCredentials options in SecurityTokenDescriptor and hence our JWT token will not be parsed by jwt.io site and it will only be decrypted only by our code. //Hence you can secure your token and who can see it - var encryptionKey = Encoding.UTF8.GetBytes(_siteSetting.JwtSettings.EncryptKey); //must be 16 character - var encryptingCredentials = new EncryptingCredentials(new SymmetricSecurityKey(encryptionKey), SecurityAlgorithms.Aes128KW, SecurityAlgorithms.Aes128CbcHmacSha256); + //var encryptionKey = Encoding.UTF8.GetBytes(_siteSetting.JwtSettings.EncryptKey); //must be 16 character + //var encryptingCredentials = new EncryptingCredentials(new SymmetricSecurityKey(encryptionKey), SecurityAlgorithms.Aes128KW, SecurityAlgorithms.Aes128CbcHmacSha256); var claims = await GetClaimsAsync(user); @@ -76,7 +76,7 @@ public async Task GenerateAsync(User user) } catch { - return null; + throw; } } diff --git a/src/Clean.Architecture.Infrastructure/StartupSetup.cs b/src/Clean.Architecture.Infrastructure/StartupSetup.cs index 497203161..5acc09928 100644 --- a/src/Clean.Architecture.Infrastructure/StartupSetup.cs +++ b/src/Clean.Architecture.Infrastructure/StartupSetup.cs @@ -113,6 +113,10 @@ private Func OnTokenValidatedJwtBearerEvent() var signInManager = context.HttpContext.RequestServices.GetRequiredService>(); var claimsIdentity = context.Principal!.Identity as ClaimsIdentity; + + if (claimsIdentity==null) + context.Fail("This token has no claims identity."); + if (claimsIdentity!.Claims?.Any() != true) context.Fail("This token has no claims."); diff --git a/src/Clean.Architecture.Web/Program.cs b/src/Clean.Architecture.Web/Program.cs index 6f6a1f5bb..6cdbd2d09 100644 --- a/src/Clean.Architecture.Web/Program.cs +++ b/src/Clean.Architecture.Web/Program.cs @@ -24,7 +24,7 @@ options.MinimumSameSitePolicy = SameSiteMode.None; }); -string? connectionString = builder.Configuration.GetConnectionString("DefaultConnection"); //Configuration.GetConnectionString("DefaultConnection"); +string? connectionString = builder.Configuration.GetConnectionString("SqliteConnection"); //Configuration.GetConnectionString("DefaultConnection"); builder.Services.AddDbContext(connectionString!); diff --git a/src/Clean.Architecture.Web/appsettings.json b/src/Clean.Architecture.Web/appsettings.json index 5798eba1f..6504e9416 100644 --- a/src/Clean.Architecture.Web/appsettings.json +++ b/src/Clean.Architecture.Web/appsettings.json @@ -1,6 +1,6 @@ { "ConnectionStrings": { - "DefaultConnection": "Server=CRLHL-ANWARAHM1;Database=cleanarchitecture;Trusted_Connection=True;TrustServerCertificate=True", + "DefaultConnection": "Server=(localdb)\\v11.0;Database=cleanarchitecture;Trusted_Connection=True;MultipleActiveResultSets=true", "SqliteConnection": "Data Source=database.sqlite" }, "Serilog": { From 51e5ae1736f9f54fe7794bcc926849277c39977f Mon Sep 17 00:00:00 2001 From: "TL\\anwarahm" Date: Mon, 17 Apr 2023 12:15:29 +0500 Subject: [PATCH 7/7] CODE RUB: unsued variable removed --- src/Clean.Architecture.Web/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Clean.Architecture.Web/Program.cs b/src/Clean.Architecture.Web/Program.cs index 6cdbd2d09..5dec8d0fc 100644 --- a/src/Clean.Architecture.Web/Program.cs +++ b/src/Clean.Architecture.Web/Program.cs @@ -101,7 +101,7 @@ { var context = services.GetRequiredService(); // context.Database.Migrate(); - var X = context.Database.EnsureCreated(); + context.Database.EnsureCreated(); SeedData.Initialize(services); } catch (Exception ex)