From b6400f375dafcaf3eee16503a3136e34ba6a9d79 Mon Sep 17 00:00:00 2001 From: mahmmoudkinawy Date: Sun, 6 Nov 2022 19:07:15 +0200 Subject: [PATCH 1/2] Refactored login end point in login controller --- .../JWTAPI/Controllers/LoginController.cs | 98 +++++----- .../Resources/UserCredentialsResource.cs | 21 +-- .../Core/Security/Tokens/ITokenHandler.cs | 13 +- .../Core/Security/Tokens/JsonWebToken.cs | 31 ++-- .../Core/Services/IAuthenticationService.cs | 13 +- .../JWTAPI/Core/Services/IUserService.cs | 12 +- src/JWTAPI/JWTAPI/GlobalUsings.cs | 18 ++ src/JWTAPI/JWTAPI/Persistence/DatabaseSeed.cs | 81 +++++---- .../JWTAPI/Persistence/UserRepository.cs | 48 +++-- src/JWTAPI/JWTAPI/Program.cs | 102 ++++++++--- .../JWTAPI/Security/Tokens/TokenHandler.cs | 168 +++++++++--------- .../JWTAPI/Services/AuthenticationService.cs | 89 +++++----- src/JWTAPI/JWTAPI/Services/UserService.cs | 59 +++--- src/JWTAPI/JWTAPI/Startup.cs | 86 --------- 14 files changed, 395 insertions(+), 444 deletions(-) create mode 100644 src/JWTAPI/JWTAPI/GlobalUsings.cs delete mode 100644 src/JWTAPI/JWTAPI/Startup.cs diff --git a/src/JWTAPI/JWTAPI/Controllers/LoginController.cs b/src/JWTAPI/JWTAPI/Controllers/LoginController.cs index 38ed033..abb0833 100644 --- a/src/JWTAPI/JWTAPI/Controllers/LoginController.cs +++ b/src/JWTAPI/JWTAPI/Controllers/LoginController.cs @@ -1,72 +1,62 @@ -using AutoMapper; -using JWTAPI.Controllers.Resources; -using JWTAPI.Core.Security.Tokens; -using JWTAPI.Core.Services; -using Microsoft.AspNetCore.Mvc; +namespace JWTAPI.Controllers; -namespace JWTAPI.Controllers +[ApiController] +[Route("api/")] +public class AuthController : ControllerBase { - [ApiController] - public class AuthController : Controller + private readonly IMapper _mapper; + private readonly IAuthenticationService _authenticationService; + + public AuthController(IMapper mapper, IAuthenticationService authenticationService) + { + _authenticationService = authenticationService; + _mapper = mapper; + } + + [HttpPost("login")] + public async Task LoginAsync( + [FromBody] UserCredentialsResource userCredentials) { - private readonly IMapper _mapper; - private readonly IAuthenticationService _authenticationService; + var response = await _authenticationService + .CreateAccessTokenAsync(userCredentials.Email, userCredentials.Password); - public AuthController(IMapper mapper, IAuthenticationService authenticationService) + if (!response.Success) { - _authenticationService = authenticationService; - _mapper = mapper; + return BadRequest(response.Message); } - [Route("/api/login")] - [HttpPost] - public async Task LoginAsync([FromBody] UserCredentialsResource userCredentials) - { - if (!ModelState.IsValid) - { - return BadRequest(ModelState); - } + var accessTokenResource = _mapper.Map(response.Token); - var response = await _authenticationService.CreateAccessTokenAsync(userCredentials.Email, userCredentials.Password); - if(!response.Success) - { - return BadRequest(response.Message); - } + return Ok(accessTokenResource); + } - var accessTokenResource = _mapper.Map(response.Token); - return Ok(accessTokenResource); + [HttpPost("token/refresh")] + public async Task RefreshTokenAsync([FromBody] RefreshTokenResource refreshTokenResource) + { + if (!ModelState.IsValid) + { + return BadRequest(ModelState); } - [Route("/api/token/refresh")] - [HttpPost] - public async Task RefreshTokenAsync([FromBody] RefreshTokenResource refreshTokenResource) + var response = await _authenticationService.RefreshTokenAsync(refreshTokenResource.Token, refreshTokenResource.UserEmail); + if (!response.Success) { - if (!ModelState.IsValid) - { - return BadRequest(ModelState); - } - - var response = await _authenticationService.RefreshTokenAsync(refreshTokenResource.Token, refreshTokenResource.UserEmail); - if(!response.Success) - { - return BadRequest(response.Message); - } - - var tokenResource = _mapper.Map(response.Token); - return Ok(tokenResource); + return BadRequest(response.Message); } - [Route("/api/token/revoke")] - [HttpPost] - public IActionResult RevokeToken([FromBody] RevokeTokenResource resource) - { - if (!ModelState.IsValid) - { - return BadRequest(ModelState); - } + var tokenResource = _mapper.Map(response.Token); + return Ok(tokenResource); + } - _authenticationService.RevokeRefreshToken(resource.Token, resource.Email); - return NoContent(); + [HttpPost("token/revoke")] + public IActionResult RevokeToken([FromBody] RevokeTokenResource resource) + { + if (!ModelState.IsValid) + { + return BadRequest(ModelState); } + + _authenticationService.RevokeRefreshToken(resource.Token, resource.Email); + return NoContent(); } } \ No newline at end of file diff --git a/src/JWTAPI/JWTAPI/Controllers/Resources/UserCredentialsResource.cs b/src/JWTAPI/JWTAPI/Controllers/Resources/UserCredentialsResource.cs index 97e7e2f..da7747b 100644 --- a/src/JWTAPI/JWTAPI/Controllers/Resources/UserCredentialsResource.cs +++ b/src/JWTAPI/JWTAPI/Controllers/Resources/UserCredentialsResource.cs @@ -1,16 +1,13 @@ -using System.ComponentModel.DataAnnotations; +namespace JWTAPI.Controllers.Resources; -namespace JWTAPI.Controllers.Resources +public class UserCredentialsResource { - public class UserCredentialsResource - { - [Required] - [DataType(DataType.EmailAddress)] - [StringLength(255)] - public string Email { get; set; } + [Required] + [EmailAddress] + [StringLength(255)] + public string Email { get; set; } - [Required] - [StringLength(32)] - public string Password { get; set; } - } + [Required] + [StringLength(32)] + public string Password { get; set; } } \ No newline at end of file diff --git a/src/JWTAPI/JWTAPI/Core/Security/Tokens/ITokenHandler.cs b/src/JWTAPI/JWTAPI/Core/Security/Tokens/ITokenHandler.cs index deff843..325fdd8 100644 --- a/src/JWTAPI/JWTAPI/Core/Security/Tokens/ITokenHandler.cs +++ b/src/JWTAPI/JWTAPI/Core/Security/Tokens/ITokenHandler.cs @@ -1,11 +1,8 @@ -using JWTAPI.Core.Models; +namespace JWTAPI.Core.Security.Tokens; -namespace JWTAPI.Core.Security.Tokens +public interface ITokenHandler { - public interface ITokenHandler - { - AccessToken CreateAccessToken(User user); - RefreshToken TakeRefreshToken(string token, string userEmail); - void RevokeRefreshToken(string token, string userEmail); - } + AccessToken CreateAccessToken(User user); + RefreshToken TakeRefreshToken(string token, string userEmail); + void RevokeRefreshToken(string token, string userEmail); } \ No newline at end of file diff --git a/src/JWTAPI/JWTAPI/Core/Security/Tokens/JsonWebToken.cs b/src/JWTAPI/JWTAPI/Core/Security/Tokens/JsonWebToken.cs index a4b91fd..30048fb 100644 --- a/src/JWTAPI/JWTAPI/Core/Security/Tokens/JsonWebToken.cs +++ b/src/JWTAPI/JWTAPI/Core/Security/Tokens/JsonWebToken.cs @@ -1,22 +1,21 @@ -namespace JWTAPI.Core.Security.Tokens -{ - public abstract class JsonWebToken - { - public string Token { get; protected set; } - public long Expiration { get; protected set; } +namespace JWTAPI.Core.Security.Tokens; - public JsonWebToken(string token, long expiration) - { - if(string.IsNullOrWhiteSpace(token)) - throw new ArgumentException("Invalid token."); +public abstract class JsonWebToken +{ + public string Token { get; protected set; } + public long Expiration { get; protected set; } - if(expiration <= 0) - throw new ArgumentException("Invalid expiration."); + public JsonWebToken(string token, long expiration) + { + if(string.IsNullOrWhiteSpace(token)) + throw new ArgumentException("Invalid token."); - Token = token; - Expiration = expiration; - } + if(expiration <= 0) + throw new ArgumentException("Invalid expiration."); - public bool IsExpired() => DateTime.UtcNow.Ticks > Expiration; + Token = token; + Expiration = expiration; } + + public bool IsExpired() => DateTime.UtcNow.Ticks > Expiration; } \ No newline at end of file diff --git a/src/JWTAPI/JWTAPI/Core/Services/IAuthenticationService.cs b/src/JWTAPI/JWTAPI/Core/Services/IAuthenticationService.cs index a3a99aa..37b872c 100644 --- a/src/JWTAPI/JWTAPI/Core/Services/IAuthenticationService.cs +++ b/src/JWTAPI/JWTAPI/Core/Services/IAuthenticationService.cs @@ -1,11 +1,8 @@ -using JWTAPI.Core.Services.Communication; +namespace JWTAPI.Core.Services; -namespace JWTAPI.Core.Services +public interface IAuthenticationService { - public interface IAuthenticationService - { - Task CreateAccessTokenAsync(string email, string password); - Task RefreshTokenAsync(string refreshToken, string userEmail); - void RevokeRefreshToken(string refreshToken, string userEmail); - } + Task CreateAccessTokenAsync(string email, string password); + Task RefreshTokenAsync(string refreshToken, string userEmail); + void RevokeRefreshToken(string refreshToken, string userEmail); } \ No newline at end of file diff --git a/src/JWTAPI/JWTAPI/Core/Services/IUserService.cs b/src/JWTAPI/JWTAPI/Core/Services/IUserService.cs index d83e7b4..2f47f19 100644 --- a/src/JWTAPI/JWTAPI/Core/Services/IUserService.cs +++ b/src/JWTAPI/JWTAPI/Core/Services/IUserService.cs @@ -1,11 +1,7 @@ -using JWTAPI.Core.Models; -using JWTAPI.Core.Services.Communication; +namespace JWTAPI.Core.Services; -namespace JWTAPI.Core.Services +public interface IUserService { - public interface IUserService - { - Task CreateUserAsync(User user, params ApplicationRole[] userRoles); - Task FindByEmailAsync(string email); - } + Task CreateUserAsync(User user, params ApplicationRole[] userRoles); + Task FindByEmailAsync(string email); } \ No newline at end of file diff --git a/src/JWTAPI/JWTAPI/GlobalUsings.cs b/src/JWTAPI/JWTAPI/GlobalUsings.cs new file mode 100644 index 0000000..ae665c8 --- /dev/null +++ b/src/JWTAPI/JWTAPI/GlobalUsings.cs @@ -0,0 +1,18 @@ +global using AutoMapper; +global using JWTAPI.Controllers.Resources; +global using JWTAPI.Core.Models; +global using JWTAPI.Core.Security.Hashing; +global using JWTAPI.Core.Security.Tokens; +global using JWTAPI.Core.Services; +global using JWTAPI.Core.Services.Communication; +global using Microsoft.AspNetCore.Mvc; +global using Microsoft.Extensions.Options; +global using System.IdentityModel.Tokens.Jwt; +global using System.Security.Claims; +global using System.ComponentModel.DataAnnotations; + +global using JWTAPI.Core.Repositories; + +global using Microsoft.EntityFrameworkCore; + + diff --git a/src/JWTAPI/JWTAPI/Persistence/DatabaseSeed.cs b/src/JWTAPI/JWTAPI/Persistence/DatabaseSeed.cs index accff54..8404b1f 100644 --- a/src/JWTAPI/JWTAPI/Persistence/DatabaseSeed.cs +++ b/src/JWTAPI/JWTAPI/Persistence/DatabaseSeed.cs @@ -1,53 +1,52 @@ -using JWTAPI.Core.Models; -using JWTAPI.Core.Security.Hashing; +namespace JWTAPI.Persistence; -namespace JWTAPI.Persistence +/// +/// EF Core already supports database seeding throught overriding "OnModelCreating", but I decided to create a separate seed class to avoid +/// injecting IPasswordHasher into AppDbContext. +/// To understand how to use database seeding into DbContext classes, check this link: https://docs.microsoft.com/en-us/ef/core/modeling/data-seeding +/// +public class DatabaseSeed { - /// - /// EF Core already supports database seeding throught overriding "OnModelCreating", but I decided to create a separate seed class to avoid - /// injecting IPasswordHasher into AppDbContext. - /// To understand how to use database seeding into DbContext classes, check this link: https://docs.microsoft.com/en-us/ef/core/modeling/data-seeding - /// - public class DatabaseSeed + public static async Task SeedAsync(AppDbContext context, IPasswordHasher passwordHasher) { - public static void Seed(AppDbContext context, IPasswordHasher passwordHasher) + context.Database.EnsureCreated(); + + if (await context.Roles.AnyAsync()) return; + + var roles = new List { - context.Database.EnsureCreated(); - - if (context.Roles.Count() == 0) - { + new Role { Name = ApplicationRole.Common.ToString() }, + new Role { Name = ApplicationRole.Administrator.ToString() } + }; - var roles = new List - { - new Role { Name = ApplicationRole.Common.ToString() }, - new Role { Name = ApplicationRole.Administrator.ToString() } - }; + context.Roles.AddRange(roles); + await context.SaveChangesAsync(); + + var users = new List + { + new User { Email = "admin@admin.com", Password = passwordHasher.HashPassword("12345678") }, + new User { Email = "common@common.com", Password = passwordHasher.HashPassword("12345678") }, + }; - context.Roles.AddRange(roles); - context.SaveChanges(); + users[0].UserRoles.Add(new UserRole + { + RoleId = context.Roles.SingleOrDefault(r => r.Name == ApplicationRole.Administrator.ToString()).Id, + Role = new Role + { + Name = ApplicationRole.Administrator.ToString() } + }); - if (context.Users.Count() == 0) + users[1].UserRoles.Add(new UserRole + { + RoleId = context.Roles.SingleOrDefault(r => r.Name == ApplicationRole.Common.ToString()).Id, + Role = new Role { - var users = new List - { - new User { Email = "admin@admin.com", Password = passwordHasher.HashPassword("12345678") }, - new User { Email = "common@common.com", Password = passwordHasher.HashPassword("12345678") }, - }; - - users[0].UserRoles.Add(new UserRole - { - RoleId = context.Roles.SingleOrDefault(r => r.Name == ApplicationRole.Administrator.ToString()).Id - }); - - users[1].UserRoles.Add(new UserRole - { - RoleId = context.Roles.SingleOrDefault(r => r.Name == ApplicationRole.Common.ToString()).Id - }); - - context.Users.AddRange(users); - context.SaveChanges(); + Name = ApplicationRole.Common.ToString() } - } + }); + + context.Users.AddRange(users); + await context.SaveChangesAsync(); } } \ No newline at end of file diff --git a/src/JWTAPI/JWTAPI/Persistence/UserRepository.cs b/src/JWTAPI/JWTAPI/Persistence/UserRepository.cs index 315ef34..d6410ea 100644 --- a/src/JWTAPI/JWTAPI/Persistence/UserRepository.cs +++ b/src/JWTAPI/JWTAPI/Persistence/UserRepository.cs @@ -1,36 +1,32 @@ -using JWTAPI.Core.Models; -using JWTAPI.Core.Repositories; -using Microsoft.EntityFrameworkCore; +namespace JWTAPI.Persistence; -namespace JWTAPI.Persistence +public class UserRepository : IUserRepository { - public class UserRepository : IUserRepository + private readonly AppDbContext _context; + + public UserRepository(AppDbContext context) { - private readonly AppDbContext _context; + _context = context; + } - public UserRepository(AppDbContext context) - { - _context = context; - } + public async Task AddAsync(User user, ApplicationRole[] userRoles) + { + var roleNames = userRoles.Select(r => r.ToString()).ToList(); + var roles = await _context.Roles.Where(r => roleNames.Contains(r.Name)).ToListAsync(); - public async Task AddAsync(User user, ApplicationRole[] userRoles) + foreach (var role in roles) { - var roleNames = userRoles.Select(r => r.ToString()).ToList(); - var roles = await _context.Roles.Where(r => roleNames.Contains(r.Name)).ToListAsync(); - - foreach(var role in roles) - { - user.UserRoles.Add(new UserRole { RoleId = role.Id }); - } - - _context.Users.Add(user); + user.UserRoles.Add(new UserRole { RoleId = role.Id }); } - public async Task FindByEmailAsync(string email) - { - return await _context.Users.Include(u => u.UserRoles) - .ThenInclude(ur => ur.Role) - .SingleOrDefaultAsync(u => u.Email == email); - } + _context.Users.Add(user); + } + + public async Task FindByEmailAsync(string email) + { + return await _context.Users + .Include(_ => _.UserRoles) + .ThenInclude(_ => _.Role) + .FirstOrDefaultAsync(_ => _.Email.Equals(email)); } } \ No newline at end of file diff --git a/src/JWTAPI/JWTAPI/Program.cs b/src/JWTAPI/JWTAPI/Program.cs index b2afafd..85dc82e 100644 --- a/src/JWTAPI/JWTAPI/Program.cs +++ b/src/JWTAPI/JWTAPI/Program.cs @@ -1,32 +1,86 @@ -using JWTAPI.Core.Security.Hashing; +using JWTAPI.Core.Repositories; +using JWTAPI.Core.Security.Hashing; +using JWTAPI.Core.Security.Tokens; +using JWTAPI.Core.Services; +using JWTAPI.Extensions; using JWTAPI.Persistence; +using JWTAPI.Security.Hashing; +using JWTAPI.Security.Tokens; +using JWTAPI.Services; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.EntityFrameworkCore; +using Microsoft.IdentityModel.Tokens; +using System.Reflection; -namespace JWTAPI +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddDbContext(options => { - public class Program + options.UseInMemoryDatabase("jwtapi"); +}); + +builder.Services.AddControllers(); + +builder.Services.AddCustomSwagger(); + +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); + +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +builder.Services.Configure(builder.Configuration.GetSection("TokenOptions")); +var tokenOptions = builder.Configuration.GetSection("TokenOptions").Get(); + +var signingConfigurations = new SigningConfigurations(tokenOptions.Secret); +builder.Services.AddSingleton(signingConfigurations); + +builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddJwtBearer(jwtBearerOptions => { - public static void Main(string[] args) + jwtBearerOptions.TokenValidationParameters = new TokenValidationParameters() { - var host = CreateHostBuilder(args).Build(); + ValidateAudience = true, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + ValidIssuer = tokenOptions.Issuer, + ValidAudience = tokenOptions.Audience, + IssuerSigningKey = signingConfigurations.SecurityKey, + ClockSkew = TimeSpan.Zero + }; + }); - using (var scope = host.Services.CreateScope()) - { - var services = scope.ServiceProvider; - var context = services.GetService(); - var passwordHasher = services.GetService(); - DatabaseSeed.Seed(context, passwordHasher); - } +builder.Services.AddAutoMapper(Assembly.GetExecutingAssembly()); - host.Run(); - } +var app = builder.Build(); +app.UseDeveloperExceptionPage(); - public static IHostBuilder CreateHostBuilder(string[] args) - { - return Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.UseStartup(); - }); - } - } -} \ No newline at end of file +app.UseRouting(); + +app.UseCustomSwagger(); + +app.UseAuthentication(); +app.UseAuthorization(); + +app.UseEndpoints(endpoints => +{ + endpoints.MapControllers(); +}); + +using var scope = app.Services.CreateScope(); +try +{ + var dbContext = scope.ServiceProvider.GetRequiredService(); + var passwordHasher = scope.ServiceProvider.GetRequiredService(); + await DatabaseSeed.SeedAsync(dbContext, passwordHasher); +} +catch (Exception ex) +{ + var logger = scope.ServiceProvider.GetRequiredService>(); + logger.LogError(ex, "An error occured while applying migrations"); +} + +await app.RunAsync(); \ No newline at end of file diff --git a/src/JWTAPI/JWTAPI/Security/Tokens/TokenHandler.cs b/src/JWTAPI/JWTAPI/Security/Tokens/TokenHandler.cs index 86a328c..6f1d48f 100644 --- a/src/JWTAPI/JWTAPI/Security/Tokens/TokenHandler.cs +++ b/src/JWTAPI/JWTAPI/Security/Tokens/TokenHandler.cs @@ -1,105 +1,105 @@ -using System.IdentityModel.Tokens.Jwt; -using System.Security.Claims; -using JWTAPI.Core.Models; -using JWTAPI.Core.Security.Hashing; -using JWTAPI.Core.Security.Tokens; -using Microsoft.Extensions.Options; - -namespace JWTAPI.Security.Tokens +namespace JWTAPI.Security.Tokens; + +public class TokenHandler : ITokenHandler { - public class TokenHandler : ITokenHandler + private readonly ISet _refreshTokens = new HashSet(); + + private readonly TokenOptions _tokenOptions; + private readonly SigningConfigurations _signingConfigurations; + private readonly IPasswordHasher _passwordHaser; + + public TokenHandler( + IOptions tokenOptionsSnapshot, + SigningConfigurations signingConfigurations, + IPasswordHasher passwordHaser) { - private readonly ISet _refreshTokens = new HashSet(); + _passwordHaser = passwordHaser; + _tokenOptions = tokenOptionsSnapshot.Value; + _signingConfigurations = signingConfigurations; + } - private readonly TokenOptions _tokenOptions; - private readonly SigningConfigurations _signingConfigurations; - private readonly IPasswordHasher _passwordHaser; + public AccessToken CreateAccessToken(User user) + { + var refreshToken = BuildRefreshToken(); + var accessToken = BuildAccessToken(user, refreshToken); - public TokenHandler(IOptions tokenOptionsSnapshot, SigningConfigurations signingConfigurations, IPasswordHasher passwordHaser) + _refreshTokens.Add(new RefreshTokenWithEmail { - _passwordHaser = passwordHaser; - _tokenOptions = tokenOptionsSnapshot.Value; - _signingConfigurations = signingConfigurations; - } + Email = user.Email, + RefreshToken = refreshToken, + }); - public AccessToken CreateAccessToken(User user) - { - var refreshToken = BuildRefreshToken(); - var accessToken = BuildAccessToken(user, refreshToken); - _refreshTokens.Add(new RefreshTokenWithEmail - { - Email = user.Email, - RefreshToken = refreshToken, - }); - - return accessToken; - } + return accessToken; + } - public RefreshToken TakeRefreshToken(string token, string userEmail) + public RefreshToken TakeRefreshToken(string token, string userEmail) + { + if (string.IsNullOrWhiteSpace(token) || string.IsNullOrWhiteSpace(userEmail)) { - if (string.IsNullOrWhiteSpace(token) || string.IsNullOrWhiteSpace(userEmail)) - return null; - - var refreshTokenWithEmail = _refreshTokens.SingleOrDefault(t => t.RefreshToken.Token == token && t.Email == userEmail); - if(refreshTokenWithEmail == null) - { - return null; - } - - _refreshTokens.Remove(refreshTokenWithEmail); - return refreshTokenWithEmail.RefreshToken; + return null; } - public void RevokeRefreshToken(string token, string userEmail) + var refreshTokenWithEmail = _refreshTokens.SingleOrDefault(t => t.RefreshToken.Token == token && t.Email == userEmail); + + if (refreshTokenWithEmail == null) { - TakeRefreshToken(token, userEmail); + return null; } - private RefreshToken BuildRefreshToken() - { - var refreshToken = new RefreshToken - ( - token : _passwordHaser.HashPassword(Guid.NewGuid().ToString()), - expiration : DateTime.UtcNow.AddSeconds(_tokenOptions.RefreshTokenExpiration).Ticks - ); + _refreshTokens.Remove(refreshTokenWithEmail); - return refreshToken; - } + return refreshTokenWithEmail.RefreshToken; + } - private AccessToken BuildAccessToken(User user, RefreshToken refreshToken) + public void RevokeRefreshToken(string token, string userEmail) + { + TakeRefreshToken(token, userEmail); + } + + private RefreshToken BuildRefreshToken() + { + var refreshToken = new RefreshToken + ( + token: _passwordHaser.HashPassword(Guid.NewGuid().ToString()), + expiration: DateTime.UtcNow.AddSeconds(_tokenOptions.RefreshTokenExpiration).Ticks + ); + + return refreshToken; + } + + private AccessToken BuildAccessToken(User user, RefreshToken refreshToken) + { + var accessTokenExpiration = DateTime.UtcNow.AddSeconds(_tokenOptions.AccessTokenExpiration); + + var securityToken = new JwtSecurityToken + ( + issuer: _tokenOptions.Issuer, + audience: _tokenOptions.Audience, + claims: GetClaims(user), + expires: accessTokenExpiration, + notBefore: DateTime.UtcNow, + signingCredentials: _signingConfigurations.SigningCredentials + ); + + var handler = new JwtSecurityTokenHandler(); + var accessToken = handler.WriteToken(securityToken); + + return new AccessToken(accessToken, accessTokenExpiration.Ticks, refreshToken); + } + + private IEnumerable GetClaims(User user) + { + var claims = new List { - var accessTokenExpiration = DateTime.UtcNow.AddSeconds(_tokenOptions.AccessTokenExpiration); - - var securityToken = new JwtSecurityToken - ( - issuer : _tokenOptions.Issuer, - audience : _tokenOptions.Audience, - claims : GetClaims(user), - expires : accessTokenExpiration, - notBefore : DateTime.UtcNow, - signingCredentials : _signingConfigurations.SigningCredentials - ); - - var handler = new JwtSecurityTokenHandler(); - var accessToken = handler.WriteToken(securityToken); - - return new AccessToken(accessToken, accessTokenExpiration.Ticks, refreshToken); - } + new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), + new Claim(JwtRegisteredClaimNames.Sub, user.Email) + }; - private IEnumerable GetClaims(User user) + foreach (var userRole in user.UserRoles) { - var claims = new List - { - new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), - new Claim(JwtRegisteredClaimNames.Sub, user.Email) - }; - - foreach (var userRole in user.UserRoles) - { - claims.Add(new Claim(ClaimTypes.Role, userRole.Role.Name)); - } - - return claims; + claims.Add(new Claim(ClaimTypes.Role, userRole.Role.Name)); } + + return claims; } } diff --git a/src/JWTAPI/JWTAPI/Services/AuthenticationService.cs b/src/JWTAPI/JWTAPI/Services/AuthenticationService.cs index e0bde64..cbd2f4b 100644 --- a/src/JWTAPI/JWTAPI/Services/AuthenticationService.cs +++ b/src/JWTAPI/JWTAPI/Services/AuthenticationService.cs @@ -1,64 +1,61 @@ -using JWTAPI.Core.Security.Hashing; -using JWTAPI.Core.Security.Tokens; -using JWTAPI.Core.Services; -using JWTAPI.Core.Services.Communication; +namespace JWTAPI.Services; -namespace JWTAPI.Services +public class AuthenticationService : IAuthenticationService { - public class AuthenticationService : IAuthenticationService + private readonly IUserService _userService; + private readonly IPasswordHasher _passwordHasher; + private readonly ITokenHandler _tokenHandler; + + public AuthenticationService( + IUserService userService, + IPasswordHasher passwordHasher, + ITokenHandler tokenHandler) { - private readonly IUserService _userService; - private readonly IPasswordHasher _passwordHasher; - private readonly ITokenHandler _tokenHandler; - - public AuthenticationService(IUserService userService, IPasswordHasher passwordHasher, ITokenHandler tokenHandler) + _tokenHandler = tokenHandler; + _passwordHasher = passwordHasher; + _userService = userService; + } + + public async Task CreateAccessTokenAsync(string email, string password) + { + var user = await _userService.FindByEmailAsync(email); + + if (user == null || !_passwordHasher.PasswordMatches(password, user.Password)) { - _tokenHandler = tokenHandler; - _passwordHasher = passwordHasher; - _userService = userService; + return new TokenResponse(false, "Invalid credentials.", null); } - public async Task CreateAccessTokenAsync(string email, string password) - { - var user = await _userService.FindByEmailAsync(email); + var token = _tokenHandler.CreateAccessToken(user); - if (user == null || !_passwordHasher.PasswordMatches(password, user.Password)) - { - return new TokenResponse(false, "Invalid credentials.", null); - } + return new TokenResponse(true, null, token); + } - var token = _tokenHandler.CreateAccessToken(user); + public async Task RefreshTokenAsync(string refreshToken, string userEmail) + { + var token = _tokenHandler.TakeRefreshToken(refreshToken, userEmail); - return new TokenResponse(true, null, token); + if (token == null) + { + return new TokenResponse(false, "Invalid refresh token.", null); } - public async Task RefreshTokenAsync(string refreshToken, string userEmail) + if (token.IsExpired()) { - var token = _tokenHandler.TakeRefreshToken(refreshToken, userEmail); - - if (token == null) - { - return new TokenResponse(false, "Invalid refresh token.", null); - } - - if (token.IsExpired()) - { - return new TokenResponse(false, "Expired refresh token.", null); - } - - var user = await _userService.FindByEmailAsync(userEmail); - if (user == null) - { - return new TokenResponse(false, "Invalid refresh token.", null); - } - - var accessToken = _tokenHandler.CreateAccessToken(user); - return new TokenResponse(true, null, accessToken); + return new TokenResponse(false, "Expired refresh token.", null); } - public void RevokeRefreshToken(string refreshToken, string userEmail) + var user = await _userService.FindByEmailAsync(userEmail); + if (user == null) { - _tokenHandler.RevokeRefreshToken(refreshToken, userEmail); + return new TokenResponse(false, "Invalid refresh token.", null); } + + var accessToken = _tokenHandler.CreateAccessToken(user); + return new TokenResponse(true, null, accessToken); + } + + public void RevokeRefreshToken(string refreshToken, string userEmail) + { + _tokenHandler.RevokeRefreshToken(refreshToken, userEmail); } } \ No newline at end of file diff --git a/src/JWTAPI/JWTAPI/Services/UserService.cs b/src/JWTAPI/JWTAPI/Services/UserService.cs index 82e6f90..302be17 100644 --- a/src/JWTAPI/JWTAPI/Services/UserService.cs +++ b/src/JWTAPI/JWTAPI/Services/UserService.cs @@ -1,43 +1,40 @@ -using JWTAPI.Core.Models; -using JWTAPI.Core.Repositories; -using JWTAPI.Core.Security.Hashing; -using JWTAPI.Core.Services; -using JWTAPI.Core.Services.Communication; +namespace JWTAPI.Services; -namespace JWTAPI.Services +public class UserService : IUserService { - public class UserService : IUserService + private readonly IUserRepository _userRepository; + private readonly IUnitOfWork _unitOfWork; + private readonly IPasswordHasher _passwordHasher; + + public UserService( + IUserRepository userRepository, + IUnitOfWork unitOfWork, + IPasswordHasher passwordHasher) { - private readonly IUserRepository _userRepository; - private readonly IUnitOfWork _unitOfWork; - private readonly IPasswordHasher _passwordHasher; + _passwordHasher = passwordHasher; + _unitOfWork = unitOfWork; + _userRepository = userRepository; + } - public UserService(IUserRepository userRepository, IUnitOfWork unitOfWork, IPasswordHasher passwordHasher) - { - _passwordHasher = passwordHasher; - _unitOfWork = unitOfWork; - _userRepository = userRepository; - } + public async Task CreateUserAsync(User user, params ApplicationRole[] userRoles) + { + var existingUser = await _userRepository.FindByEmailAsync(user.Email); - public async Task CreateUserAsync(User user, params ApplicationRole[] userRoles) + if (existingUser != null) { - var existingUser = await _userRepository.FindByEmailAsync(user.Email); - if(existingUser != null) - { - return new CreateUserResponse(false, "Email already in use.", null); - } + return new CreateUserResponse(false, "Email already in use.", null); + } - user.Password = _passwordHasher.HashPassword(user.Password); + user.Password = _passwordHasher.HashPassword(user.Password); - await _userRepository.AddAsync(user, userRoles); - await _unitOfWork.CompleteAsync(); + await _userRepository.AddAsync(user, userRoles); + await _unitOfWork.CompleteAsync(); - return new CreateUserResponse(true, null, user); - } + return new CreateUserResponse(true, null, user); + } - public async Task FindByEmailAsync(string email) - { - return await _userRepository.FindByEmailAsync(email); - } + public async Task FindByEmailAsync(string email) + { + return await _userRepository.FindByEmailAsync(email); } } \ No newline at end of file diff --git a/src/JWTAPI/JWTAPI/Startup.cs b/src/JWTAPI/JWTAPI/Startup.cs deleted file mode 100644 index fd29eb7..0000000 --- a/src/JWTAPI/JWTAPI/Startup.cs +++ /dev/null @@ -1,86 +0,0 @@ -using JWTAPI.Core.Repositories; -using JWTAPI.Core.Security.Hashing; -using JWTAPI.Core.Security.Tokens; -using JWTAPI.Core.Services; -using JWTAPI.Extensions; -using JWTAPI.Persistence; -using JWTAPI.Security.Hashing; -using JWTAPI.Security.Tokens; -using JWTAPI.Services; -using Microsoft.AspNetCore.Authentication.JwtBearer; -using Microsoft.EntityFrameworkCore; -using Microsoft.IdentityModel.Tokens; - -namespace JWTAPI -{ - public class Startup - { - public IConfiguration Configuration { get; } - - public Startup(IConfiguration configuration) - { - Configuration = configuration; - } - - public void ConfigureServices(IServiceCollection services) - { - services.AddDbContext(options => - { - options.UseInMemoryDatabase("jwtapi"); - }); - - services.AddControllers(); - - services.AddCustomSwagger(); - - services.AddScoped(); - services.AddScoped(); - - services.AddSingleton(); - services.AddSingleton(); - - services.AddScoped(); - services.AddScoped(); - - services.Configure(Configuration.GetSection("TokenOptions")); - var tokenOptions = Configuration.GetSection("TokenOptions").Get(); - - var signingConfigurations = new SigningConfigurations(tokenOptions.Secret); - services.AddSingleton(signingConfigurations); - - services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) - .AddJwtBearer(jwtBearerOptions => - { - jwtBearerOptions.TokenValidationParameters = new TokenValidationParameters() - { - ValidateAudience = true, - ValidateLifetime = true, - ValidateIssuerSigningKey = true, - ValidIssuer = tokenOptions.Issuer, - ValidAudience = tokenOptions.Audience, - IssuerSigningKey = signingConfigurations.SecurityKey, - ClockSkew = TimeSpan.Zero - }; - }); - - services.AddAutoMapper(this.GetType().Assembly); - } - - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) - { - app.UseDeveloperExceptionPage(); - - app.UseRouting(); - - app.UseCustomSwagger(); - - app.UseAuthentication(); - app.UseAuthorization(); - - app.UseEndpoints(endpoints => - { - endpoints.MapControllers(); - }); - } - } -} \ No newline at end of file From efb1676b2a95765b0ee99bddd48df160f9e5a920 Mon Sep 17 00:00:00 2001 From: mahmmoudkinawy Date: Mon, 7 Nov 2022 21:01:17 +0200 Subject: [PATCH 2/2] Updated to .net 6.0 and now performance is more good --- .../JWTAPI/Controllers/LoginController.cs | 18 +- .../JWTAPI/Controllers/ProtectedController.cs | 35 +-- .../Resources/RefreshTokenResource.cs | 20 +- .../Resources/RevokeTokenResource.cs | 16 +- .../Controllers/Resources/TokenResource.cs | 12 +- .../Resources/UserCredentialsResource.cs | 1 - .../Controllers/Resources/UserResource.cs | 14 +- .../JWTAPI/Controllers/UsersController.cs | 55 ++-- .../JWTAPI/Core/Models/ApplicationRole.cs | 10 +- src/JWTAPI/JWTAPI/Core/Models/Role.cs | 19 +- src/JWTAPI/JWTAPI/Core/Models/User.cs | 25 +- src/JWTAPI/JWTAPI/Core/Models/UserRole.cs | 17 +- .../JWTAPI/Core/Repositories/IUnitOfWork.cs | 8 +- .../Core/Repositories/IUserRepository.cs | 12 +- .../Core/Security/Hashing/IPasswordHasher.cs | 10 +- .../Core/Security/Tokens/AccessToken.cs | 18 +- .../Core/Security/Tokens/RefreshToken.cs | 11 +- .../Security/Tokens/RefreshTokenWithEmail.cs | 10 +- .../Services/Communication/BaseResponse.cs | 18 +- .../Communication/CreateUserResponse.cs | 16 +- .../Services/Communication/TokenResponse.cs | 16 +- .../ApplicationServiceExtenstions.cs | 30 ++ .../Extensions/IdentityServiceExtenstions.cs | 33 ++ .../JWTAPI/Extensions/MiddlewareExtensions.cs | 86 +++--- src/JWTAPI/JWTAPI/GlobalUsings.cs | 25 +- .../JWTAPI/Mapping/ModelToResourceProfile.cs | 25 +- .../JWTAPI/Mapping/ResourceToModelProfile.cs | 14 +- src/JWTAPI/JWTAPI/Persistence/AppDbContext.cs | 25 +- src/JWTAPI/JWTAPI/Persistence/UnitOfWork.cs | 24 +- .../JWTAPI/Persistence/UserRepository.cs | 1 - src/JWTAPI/JWTAPI/Program.cs | 57 +--- .../JWTAPI/Security/Hashing/PasswordHasher.cs | 127 ++++---- .../Security/Tokens/SigningConfigurations.cs | 22 +- .../JWTAPI/Security/Tokens/TokenOptions.cs | 16 +- src/JWTAPI/JWTAPI/Services/UserService.cs | 1 - .../Security/Hashing/PasswordHasherTests.cs | 94 +++--- .../Security/Tokens/TokenHandlerTests.cs | 215 +++++++------ .../Services/AuthenticationServiceTests.cs | 285 +++++++++--------- .../JWTAPI.Tests/Services/UserServiceTests.cs | 123 ++++---- tests/JWTAPI.Tests/Usings.cs | 14 + 40 files changed, 732 insertions(+), 846 deletions(-) create mode 100644 src/JWTAPI/JWTAPI/Extensions/ApplicationServiceExtenstions.cs create mode 100644 src/JWTAPI/JWTAPI/Extensions/IdentityServiceExtenstions.cs create mode 100644 tests/JWTAPI.Tests/Usings.cs diff --git a/src/JWTAPI/JWTAPI/Controllers/LoginController.cs b/src/JWTAPI/JWTAPI/Controllers/LoginController.cs index abb0833..26611a2 100644 --- a/src/JWTAPI/JWTAPI/Controllers/LoginController.cs +++ b/src/JWTAPI/JWTAPI/Controllers/LoginController.cs @@ -31,31 +31,25 @@ public async Task LoginAsync( } [HttpPost("token/refresh")] - public async Task RefreshTokenAsync([FromBody] RefreshTokenResource refreshTokenResource) + public async Task RefreshTokenAsync( + [FromBody] RefreshTokenResource refreshTokenResource) { - if (!ModelState.IsValid) - { - return BadRequest(ModelState); - } - - var response = await _authenticationService.RefreshTokenAsync(refreshTokenResource.Token, refreshTokenResource.UserEmail); + var response = await _authenticationService + .RefreshTokenAsync(refreshTokenResource.Token, refreshTokenResource.UserEmail); + if (!response.Success) { return BadRequest(response.Message); } var tokenResource = _mapper.Map(response.Token); + return Ok(tokenResource); } [HttpPost("token/revoke")] public IActionResult RevokeToken([FromBody] RevokeTokenResource resource) { - if (!ModelState.IsValid) - { - return BadRequest(ModelState); - } - _authenticationService.RevokeRefreshToken(resource.Token, resource.Email); return NoContent(); } diff --git a/src/JWTAPI/JWTAPI/Controllers/ProtectedController.cs b/src/JWTAPI/JWTAPI/Controllers/ProtectedController.cs index 7dcd402..180d3a9 100644 --- a/src/JWTAPI/JWTAPI/Controllers/ProtectedController.cs +++ b/src/JWTAPI/JWTAPI/Controllers/ProtectedController.cs @@ -1,25 +1,22 @@ -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; +namespace JWTAPI.Controllers; -namespace JWTAPI.Controllers +[ApiController] +[Route("api/protected")] +public class ProtectedController : ControllerBase { - [ApiController] - public class ProtectedController : Controller + [HttpGet] + [Authorize] + [Route("for-commonusers")] + public IActionResult GetProtectedData() { - [HttpGet] - [Authorize] - [Route("/api/protectedforcommonusers")] - public IActionResult GetProtectedData() - { - return Ok("Hello world from protected controller."); - } + return Ok("Hello world from protected controller."); + } - [HttpGet] - [Authorize(Roles = "Administrator")] - [Route("/api/protectedforadministrators")] - public IActionResult GetProtectedDataForAdmin() - { - return Ok("Hello admin!"); - } + [HttpGet] + [Authorize(Roles = "Administrator")] + [Route("for-administrators")] + public IActionResult GetProtectedDataForAdmin() + { + return Ok("Hello admin!"); } } \ No newline at end of file diff --git a/src/JWTAPI/JWTAPI/Controllers/Resources/RefreshTokenResource.cs b/src/JWTAPI/JWTAPI/Controllers/Resources/RefreshTokenResource.cs index b346ec1..a5c2a29 100644 --- a/src/JWTAPI/JWTAPI/Controllers/Resources/RefreshTokenResource.cs +++ b/src/JWTAPI/JWTAPI/Controllers/Resources/RefreshTokenResource.cs @@ -1,15 +1,11 @@ -using System.ComponentModel.DataAnnotations; - -namespace JWTAPI.Controllers.Resources +namespace JWTAPI.Controllers.Resources; +public class RefreshTokenResource { - public class RefreshTokenResource - { - [Required] - public string Token { get; set; } + [Required] + public string Token { get; set; } - [Required] - [DataType(DataType.EmailAddress)] - [StringLength(255)] - public string UserEmail { get; set; } - } + [Required] + [EmailAddress] + [StringLength(255)] + public string UserEmail { get; set; } } \ No newline at end of file diff --git a/src/JWTAPI/JWTAPI/Controllers/Resources/RevokeTokenResource.cs b/src/JWTAPI/JWTAPI/Controllers/Resources/RevokeTokenResource.cs index 58e3067..05bfecd 100644 --- a/src/JWTAPI/JWTAPI/Controllers/Resources/RevokeTokenResource.cs +++ b/src/JWTAPI/JWTAPI/Controllers/Resources/RevokeTokenResource.cs @@ -1,13 +1,9 @@ -using System.ComponentModel.DataAnnotations; - -namespace JWTAPI.Controllers.Resources +namespace JWTAPI.Controllers.Resources; +public class RevokeTokenResource { - public class RevokeTokenResource - { - [Required] - public string Token { get; set; } + [Required] + public string Token { get; set; } - [Required] - public string Email { get; set; } - } + [Required] + public string Email { get; set; } } \ No newline at end of file diff --git a/src/JWTAPI/JWTAPI/Controllers/Resources/TokenResource.cs b/src/JWTAPI/JWTAPI/Controllers/Resources/TokenResource.cs index a3bd5b0..a66a7a7 100644 --- a/src/JWTAPI/JWTAPI/Controllers/Resources/TokenResource.cs +++ b/src/JWTAPI/JWTAPI/Controllers/Resources/TokenResource.cs @@ -1,9 +1,7 @@ -namespace JWTAPI.Controllers.Resources +namespace JWTAPI.Controllers.Resources; +public class AccessTokenResource { - public class AccessTokenResource - { - public string AccessToken { get; set; } - public string RefreshToken { get; set; } - public long Expiration { get; set; } - } + public string AccessToken { get; set; } + public string RefreshToken { get; set; } + public long Expiration { get; set; } } \ No newline at end of file diff --git a/src/JWTAPI/JWTAPI/Controllers/Resources/UserCredentialsResource.cs b/src/JWTAPI/JWTAPI/Controllers/Resources/UserCredentialsResource.cs index da7747b..5d1133e 100644 --- a/src/JWTAPI/JWTAPI/Controllers/Resources/UserCredentialsResource.cs +++ b/src/JWTAPI/JWTAPI/Controllers/Resources/UserCredentialsResource.cs @@ -1,5 +1,4 @@ namespace JWTAPI.Controllers.Resources; - public class UserCredentialsResource { [Required] diff --git a/src/JWTAPI/JWTAPI/Controllers/Resources/UserResource.cs b/src/JWTAPI/JWTAPI/Controllers/Resources/UserResource.cs index 4619d91..da32e91 100644 --- a/src/JWTAPI/JWTAPI/Controllers/Resources/UserResource.cs +++ b/src/JWTAPI/JWTAPI/Controllers/Resources/UserResource.cs @@ -1,11 +1,7 @@ -using System.Collections.Generic; - -namespace JWTAPI.Controllers.Resources +namespace JWTAPI.Controllers.Resources; +public class UserResource { - public class UserResource - { - public int Id { get; set; } - public string Email { get; set; } - public IEnumerable Roles { get; set; } - } + public int Id { get; set; } + public string Email { get; set; } + public IEnumerable Roles { get; set; } } \ No newline at end of file diff --git a/src/JWTAPI/JWTAPI/Controllers/UsersController.cs b/src/JWTAPI/JWTAPI/Controllers/UsersController.cs index 4988773..d08e155 100644 --- a/src/JWTAPI/JWTAPI/Controllers/UsersController.cs +++ b/src/JWTAPI/JWTAPI/Controllers/UsersController.cs @@ -1,42 +1,33 @@ -using AutoMapper; -using JWTAPI.Controllers.Resources; -using JWTAPI.Core.Models; -using JWTAPI.Core.Services; -using Microsoft.AspNetCore.Mvc; +namespace JWTAPI.Controllers; -namespace JWTAPI.Controllers +[ApiController] +[Route("/api/users")] +public class UsersController : ControllerBase { - [ApiController] - [Route("/api/[controller]")] - public class UsersController : Controller + private readonly IMapper _mapper; + private readonly IUserService _userService; + + public UsersController(IUserService userService, IMapper mapper) { - private readonly IMapper _mapper; - private readonly IUserService _userService; + _userService = userService; + _mapper = mapper; + } - public UsersController(IUserService userService, IMapper mapper) - { - _userService = userService; - _mapper = mapper; - } + [HttpPost] + public async Task CreateUserAsync( + [FromBody] UserCredentialsResource userCredentials) + { + var user = _mapper.Map(userCredentials); + + var response = await _userService.CreateUserAsync(user, ApplicationRole.Common); - [HttpPost] - public async Task CreateUserAsync([FromBody] UserCredentialsResource userCredentials) + if (!response.Success) { - if (!ModelState.IsValid) - { - return BadRequest(ModelState); - } + return BadRequest(response.Message); + } - var user = _mapper.Map(userCredentials); - - var response = await _userService.CreateUserAsync(user, ApplicationRole.Common); - if(!response.Success) - { - return BadRequest(response.Message); - } + var userResource = _mapper.Map(response.User); - var userResource = _mapper.Map(response.User); - return Ok(userResource); - } + return Ok(userResource); } } \ No newline at end of file diff --git a/src/JWTAPI/JWTAPI/Core/Models/ApplicationRole.cs b/src/JWTAPI/JWTAPI/Core/Models/ApplicationRole.cs index 98788a4..8378098 100644 --- a/src/JWTAPI/JWTAPI/Core/Models/ApplicationRole.cs +++ b/src/JWTAPI/JWTAPI/Core/Models/ApplicationRole.cs @@ -1,8 +1,6 @@ -namespace JWTAPI.Core.Models +namespace JWTAPI.Core.Models; +public enum ApplicationRole { - public enum ApplicationRole - { - Common = 1, - Administrator = 2 - } + Common = 1, + Administrator = 2 } \ No newline at end of file diff --git a/src/JWTAPI/JWTAPI/Core/Models/Role.cs b/src/JWTAPI/JWTAPI/Core/Models/Role.cs index 39b6644..5761761 100644 --- a/src/JWTAPI/JWTAPI/Core/Models/Role.cs +++ b/src/JWTAPI/JWTAPI/Core/Models/Role.cs @@ -1,16 +1,11 @@ -using System.Collections.ObjectModel; -using System.ComponentModel.DataAnnotations; - -namespace JWTAPI.Core.Models +namespace JWTAPI.Core.Models; +public class Role { - public class Role - { - public int Id { get; set; } + public int Id { get; set; } - [Required] - [StringLength(50)] - public string Name { get; set; } + [Required] + [StringLength(50)] + public string Name { get; set; } - public ICollection UsersRole { get; set; } = new Collection(); - } + public virtual ICollection UsersRole { get; set; } = new Collection(); } \ No newline at end of file diff --git a/src/JWTAPI/JWTAPI/Core/Models/User.cs b/src/JWTAPI/JWTAPI/Core/Models/User.cs index e93c714..139edaf 100644 --- a/src/JWTAPI/JWTAPI/Core/Models/User.cs +++ b/src/JWTAPI/JWTAPI/Core/Models/User.cs @@ -1,20 +1,15 @@ -using System.Collections.ObjectModel; -using System.ComponentModel.DataAnnotations; - -namespace JWTAPI.Core.Models +namespace JWTAPI.Core.Models; +public class User { - public class User - { - public int Id { get; set; } + public int Id { get; set; } - [Required] - [DataType(DataType.EmailAddress)] - [StringLength(255)] - public string Email { get; set; } + [Required] + [EmailAddress] + [StringLength(255)] + public string Email { get; set; } - [Required] - public string Password { get; set; } + [Required] + public string Password { get; set; } - public ICollection UserRoles { get; set; } = new Collection(); - } + public ICollection UserRoles { get; set; } = new Collection(); } \ No newline at end of file diff --git a/src/JWTAPI/JWTAPI/Core/Models/UserRole.cs b/src/JWTAPI/JWTAPI/Core/Models/UserRole.cs index 7d322f3..a715050 100644 --- a/src/JWTAPI/JWTAPI/Core/Models/UserRole.cs +++ b/src/JWTAPI/JWTAPI/Core/Models/UserRole.cs @@ -1,14 +1,11 @@ -using System.ComponentModel.DataAnnotations.Schema; +namespace JWTAPI.Core.Models; -namespace JWTAPI.Core.Models +[Table("UserRoles")] +public class UserRole { - [Table("UserRoles")] - public class UserRole - { - public int UserId { get; set; } - public User User { get; set; } + public int UserId { get; set; } + public User User { get; set; } - public int RoleId { get; set; } - public Role Role { get; set; } - } + public int RoleId { get; set; } + public Role Role { get; set; } } \ No newline at end of file diff --git a/src/JWTAPI/JWTAPI/Core/Repositories/IUnitOfWork.cs b/src/JWTAPI/JWTAPI/Core/Repositories/IUnitOfWork.cs index 12c4806..8b70103 100644 --- a/src/JWTAPI/JWTAPI/Core/Repositories/IUnitOfWork.cs +++ b/src/JWTAPI/JWTAPI/Core/Repositories/IUnitOfWork.cs @@ -1,7 +1,5 @@ -namespace JWTAPI.Core.Repositories +namespace JWTAPI.Core.Repositories; +public interface IUnitOfWork { - public interface IUnitOfWork - { - Task CompleteAsync(); - } + Task CompleteAsync(); } \ No newline at end of file diff --git a/src/JWTAPI/JWTAPI/Core/Repositories/IUserRepository.cs b/src/JWTAPI/JWTAPI/Core/Repositories/IUserRepository.cs index 84aa88b..4a429b2 100644 --- a/src/JWTAPI/JWTAPI/Core/Repositories/IUserRepository.cs +++ b/src/JWTAPI/JWTAPI/Core/Repositories/IUserRepository.cs @@ -1,10 +1,6 @@ -using JWTAPI.Core.Models; - -namespace JWTAPI.Core.Repositories +namespace JWTAPI.Core.Repositories; +public interface IUserRepository { - public interface IUserRepository - { - Task AddAsync(User user, ApplicationRole[] userRoles); - Task FindByEmailAsync(string email); - } + Task AddAsync(User user, ApplicationRole[] userRoles); + Task FindByEmailAsync(string email); } \ No newline at end of file diff --git a/src/JWTAPI/JWTAPI/Core/Security/Hashing/IPasswordHasher.cs b/src/JWTAPI/JWTAPI/Core/Security/Hashing/IPasswordHasher.cs index 15924d7..ce043d7 100644 --- a/src/JWTAPI/JWTAPI/Core/Security/Hashing/IPasswordHasher.cs +++ b/src/JWTAPI/JWTAPI/Core/Security/Hashing/IPasswordHasher.cs @@ -1,8 +1,6 @@ -namespace JWTAPI.Core.Security.Hashing +namespace JWTAPI.Core.Security.Hashing; +public interface IPasswordHasher { - public interface IPasswordHasher - { - string HashPassword(string password); - bool PasswordMatches(string providedPassword, string passwordHash); - } + string HashPassword(string password); + bool PasswordMatches(string providedPassword, string passwordHash); } \ No newline at end of file diff --git a/src/JWTAPI/JWTAPI/Core/Security/Tokens/AccessToken.cs b/src/JWTAPI/JWTAPI/Core/Security/Tokens/AccessToken.cs index 6816312..d737ea5 100644 --- a/src/JWTAPI/JWTAPI/Core/Security/Tokens/AccessToken.cs +++ b/src/JWTAPI/JWTAPI/Core/Security/Tokens/AccessToken.cs @@ -1,15 +1,11 @@ -namespace JWTAPI.Core.Security.Tokens +namespace JWTAPI.Core.Security.Tokens; +public class AccessToken : JsonWebToken { - public class AccessToken : JsonWebToken - { - public RefreshToken RefreshToken { get; private set; } + public RefreshToken RefreshToken { get; private set; } - public AccessToken(string token, long expiration, RefreshToken refreshToken) : base(token, expiration) - { - if(refreshToken == null) - throw new ArgumentException("Specify a valid refresh token."); - - RefreshToken = refreshToken; - } + public AccessToken(string token, long expiration, RefreshToken refreshToken) : base(token, expiration) + { + RefreshToken = refreshToken + ?? throw new ArgumentException("Specify a valid refresh token."); } } \ No newline at end of file diff --git a/src/JWTAPI/JWTAPI/Core/Security/Tokens/RefreshToken.cs b/src/JWTAPI/JWTAPI/Core/Security/Tokens/RefreshToken.cs index 31d96f2..6500522 100644 --- a/src/JWTAPI/JWTAPI/Core/Security/Tokens/RefreshToken.cs +++ b/src/JWTAPI/JWTAPI/Core/Security/Tokens/RefreshToken.cs @@ -1,9 +1,6 @@ -namespace JWTAPI.Core.Security.Tokens +namespace JWTAPI.Core.Security.Tokens; +public class RefreshToken : JsonWebToken { - public class RefreshToken : JsonWebToken - { - public RefreshToken(string token, long expiration) : base(token, expiration) - { - } - } + public RefreshToken(string token, long expiration) : base(token, expiration) + { } } \ No newline at end of file diff --git a/src/JWTAPI/JWTAPI/Core/Security/Tokens/RefreshTokenWithEmail.cs b/src/JWTAPI/JWTAPI/Core/Security/Tokens/RefreshTokenWithEmail.cs index de0b779..0a867ae 100644 --- a/src/JWTAPI/JWTAPI/Core/Security/Tokens/RefreshTokenWithEmail.cs +++ b/src/JWTAPI/JWTAPI/Core/Security/Tokens/RefreshTokenWithEmail.cs @@ -1,8 +1,6 @@ -namespace JWTAPI.Core.Security.Tokens +namespace JWTAPI.Core.Security.Tokens; +public class RefreshTokenWithEmail { - public class RefreshTokenWithEmail - { - public string Email { get; set; } - public RefreshToken RefreshToken { get; set; } - } + public string Email { get; set; } + public RefreshToken RefreshToken { get; set; } } diff --git a/src/JWTAPI/JWTAPI/Core/Services/Communication/BaseResponse.cs b/src/JWTAPI/JWTAPI/Core/Services/Communication/BaseResponse.cs index 9e10c80..54be2e4 100644 --- a/src/JWTAPI/JWTAPI/Core/Services/Communication/BaseResponse.cs +++ b/src/JWTAPI/JWTAPI/Core/Services/Communication/BaseResponse.cs @@ -1,14 +1,12 @@ -namespace JWTAPI.Core.Services.Communication +namespace JWTAPI.Core.Services.Communication; +public abstract class BaseResponse { - public abstract class BaseResponse - { - public bool Success { get; protected set; } - public string Message { get; protected set; } + public bool Success { get; protected set; } + public string Message { get; protected set; } - public BaseResponse(bool success, string message) - { - Success = success; - Message = message; - } + public BaseResponse(bool success, string message) + { + Success = success; + Message = message; } } \ No newline at end of file diff --git a/src/JWTAPI/JWTAPI/Core/Services/Communication/CreateUserResponse.cs b/src/JWTAPI/JWTAPI/Core/Services/Communication/CreateUserResponse.cs index d573cfa..af4d1b8 100644 --- a/src/JWTAPI/JWTAPI/Core/Services/Communication/CreateUserResponse.cs +++ b/src/JWTAPI/JWTAPI/Core/Services/Communication/CreateUserResponse.cs @@ -1,14 +1,10 @@ -using JWTAPI.Core.Models; - -namespace JWTAPI.Core.Services.Communication +namespace JWTAPI.Core.Services.Communication; +public class CreateUserResponse : BaseResponse { - public class CreateUserResponse : BaseResponse - { - public User User { get; private set; } + public User User { get; private set; } - public CreateUserResponse(bool success, string message, User user) : base(success, message) - { - User = user; - } + public CreateUserResponse(bool success, string message, User user) : base(success, message) + { + User = user; } } \ No newline at end of file diff --git a/src/JWTAPI/JWTAPI/Core/Services/Communication/TokenResponse.cs b/src/JWTAPI/JWTAPI/Core/Services/Communication/TokenResponse.cs index c608d9e..82c4d10 100644 --- a/src/JWTAPI/JWTAPI/Core/Services/Communication/TokenResponse.cs +++ b/src/JWTAPI/JWTAPI/Core/Services/Communication/TokenResponse.cs @@ -1,14 +1,10 @@ -using JWTAPI.Core.Security.Tokens; - -namespace JWTAPI.Core.Services.Communication +namespace JWTAPI.Core.Services.Communication; +public class TokenResponse : BaseResponse { - public class TokenResponse : BaseResponse - { - public AccessToken Token { get; set; } + public AccessToken Token { get; set; } - public TokenResponse(bool success, string message, AccessToken token) : base(success, message) - { - Token = token; - } + public TokenResponse(bool success, string message, AccessToken token) : base(success, message) + { + Token = token; } } \ No newline at end of file diff --git a/src/JWTAPI/JWTAPI/Extensions/ApplicationServiceExtenstions.cs b/src/JWTAPI/JWTAPI/Extensions/ApplicationServiceExtenstions.cs new file mode 100644 index 0000000..91c2e4a --- /dev/null +++ b/src/JWTAPI/JWTAPI/Extensions/ApplicationServiceExtenstions.cs @@ -0,0 +1,30 @@ +namespace JWTAPI.Extensions; +public static class ApplicationServiceExtenstions +{ + public static IServiceCollection AddApplicationServices( + this IServiceCollection services) + { + services.AddControllers(); + + services.AddDbContext(options => + { + options.UseInMemoryDatabase("jwtapi"); + }); + + services.AddScoped(); + + services.AddScoped(); + + services.AddSingleton(); + + services.AddSingleton(); + + services.AddScoped(); + + services.AddScoped(); + + services.AddAutoMapper(Assembly.GetExecutingAssembly()); + + return services; + } +} diff --git a/src/JWTAPI/JWTAPI/Extensions/IdentityServiceExtenstions.cs b/src/JWTAPI/JWTAPI/Extensions/IdentityServiceExtenstions.cs new file mode 100644 index 0000000..3d08d3e --- /dev/null +++ b/src/JWTAPI/JWTAPI/Extensions/IdentityServiceExtenstions.cs @@ -0,0 +1,33 @@ +namespace JWTAPI.Extensions; +public static class IdentityServiceExtenstions +{ + public static IServiceCollection AddIdentityServices( + this IServiceCollection services, + IConfiguration configuration) + { + services.Configure(configuration.GetSection("TokenOptions")); + + var tokenOptions = configuration.GetSection("TokenOptions").Get(); + + var signingConfigurations = new SigningConfigurations(tokenOptions.Secret); + + services.AddSingleton(signingConfigurations); + + services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddJwtBearer(jwtBearerOptions => + { + jwtBearerOptions.TokenValidationParameters = new TokenValidationParameters() + { + ValidateAudience = true, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + ValidIssuer = tokenOptions.Issuer, + ValidAudience = tokenOptions.Audience, + IssuerSigningKey = signingConfigurations.SecurityKey, + ClockSkew = TimeSpan.Zero + }; + }); + + return services; + } +} diff --git a/src/JWTAPI/JWTAPI/Extensions/MiddlewareExtensions.cs b/src/JWTAPI/JWTAPI/Extensions/MiddlewareExtensions.cs index fe36216..1d277fb 100644 --- a/src/JWTAPI/JWTAPI/Extensions/MiddlewareExtensions.cs +++ b/src/JWTAPI/JWTAPI/Extensions/MiddlewareExtensions.cs @@ -1,62 +1,58 @@ -using Microsoft.OpenApi.Models; -using System.Reflection; +namespace JWTAPI.Extensions; -namespace JWTAPI.Extensions +public static class MiddlewareExtensions { - public static class MiddlewareExtensions + public static IServiceCollection AddCustomSwagger(this IServiceCollection services) { - public static IServiceCollection AddCustomSwagger(this IServiceCollection services) + services.AddSwaggerGen(cfg => { - services.AddSwaggerGen(cfg => + cfg.SwaggerDoc("v1", new OpenApiInfo { - cfg.SwaggerDoc("v1", new OpenApiInfo + Title = "JWT API", + Version = "v4", + Description = "Example API that shows how to implement JSON Web Token authentication and authorization with ASP.NET 6, built from scratch.", + Contact = new OpenApiContact { - Title = "JWT API", - Version = "v4", - Description = "Example API that shows how to implement JSON Web Token authentication and authorization with ASP.NET 6, built from scratch.", - Contact = new OpenApiContact - { - Name = "Evandro Gayer Gomes", - Url = new Uri("https://www.linkedin.com/in/evandro-gayer-gomes/?locale=en_US") - }, - License = new OpenApiLicense - { - Name = "MIT", - }, - }); - - cfg.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme + Name = "Evandro Gayer Gomes", + Url = new Uri("https://www.linkedin.com/in/evandro-gayer-gomes/?locale=en_US") + }, + License = new OpenApiLicense { - In = ParameterLocation.Header, - Description = "JSON Web Token to access resources. Example: Bearer {token}", - Name = "Authorization", - Type = SecuritySchemeType.ApiKey - }); + Name = "MIT", + }, + }); - cfg.AddSecurityRequirement(new OpenApiSecurityRequirement + cfg.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme + { + In = ParameterLocation.Header, + Description = "JSON Web Token to access resources. Example: Bearer {token}", + Name = "Authorization", + Type = SecuritySchemeType.ApiKey + }); + + cfg.AddSecurityRequirement(new OpenApiSecurityRequirement + { { + new OpenApiSecurityScheme { - new OpenApiSecurityScheme - { - Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "Bearer" } - }, - new [] { string.Empty } - } - }); + Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "Bearer" } + }, + new [] { string.Empty } + } }); + }); - return services; - } + return services; + } - public static IApplicationBuilder UseCustomSwagger(this IApplicationBuilder app) + public static IApplicationBuilder UseCustomSwagger(this IApplicationBuilder app) + { + app.UseSwagger().UseSwaggerUI(options => { - app.UseSwagger().UseSwaggerUI(options => - { - options.SwaggerEndpoint("/swagger/v1/swagger.json", "JWT API"); - options.DocumentTitle = "JWT API"; - }); + options.SwaggerEndpoint("/swagger/v1/swagger.json", "JWT API"); + options.DocumentTitle = "JWT API"; + }); - return app; - } + return app; } } diff --git a/src/JWTAPI/JWTAPI/GlobalUsings.cs b/src/JWTAPI/JWTAPI/GlobalUsings.cs index ae665c8..cd47d69 100644 --- a/src/JWTAPI/JWTAPI/GlobalUsings.cs +++ b/src/JWTAPI/JWTAPI/GlobalUsings.cs @@ -1,18 +1,29 @@ global using AutoMapper; global using JWTAPI.Controllers.Resources; global using JWTAPI.Core.Models; +global using JWTAPI.Core.Repositories; global using JWTAPI.Core.Security.Hashing; global using JWTAPI.Core.Security.Tokens; global using JWTAPI.Core.Services; global using JWTAPI.Core.Services.Communication; +global using JWTAPI.Extensions; +global using JWTAPI.Persistence; +global using JWTAPI.Security.Hashing; +global using JWTAPI.Security.Tokens; +global using JWTAPI.Services; +global using Microsoft.AspNetCore.Authentication.JwtBearer; +global using Microsoft.AspNetCore.Authorization; global using Microsoft.AspNetCore.Mvc; +global using Microsoft.EntityFrameworkCore; global using Microsoft.Extensions.Options; +global using Microsoft.IdentityModel.Tokens; +global using Microsoft.OpenApi.Models; +global using System.Collections.ObjectModel; +global using System.ComponentModel.DataAnnotations; +global using System.ComponentModel.DataAnnotations.Schema; global using System.IdentityModel.Tokens.Jwt; +global using System.Reflection; +global using System.Runtime.CompilerServices; global using System.Security.Claims; -global using System.ComponentModel.DataAnnotations; - -global using JWTAPI.Core.Repositories; - -global using Microsoft.EntityFrameworkCore; - - +global using System.Security.Cryptography; +global using System.Text; diff --git a/src/JWTAPI/JWTAPI/Mapping/ModelToResourceProfile.cs b/src/JWTAPI/JWTAPI/Mapping/ModelToResourceProfile.cs index f8f3daf..6ac95ad 100644 --- a/src/JWTAPI/JWTAPI/Mapping/ModelToResourceProfile.cs +++ b/src/JWTAPI/JWTAPI/Mapping/ModelToResourceProfile.cs @@ -1,21 +1,14 @@ -using AutoMapper; -using JWTAPI.Controllers.Resources; -using JWTAPI.Core.Models; -using JWTAPI.Core.Security.Tokens; - -namespace JWTAPI.Mapping +namespace JWTAPI.Mapping; +public class ModelToResourceProfile : Profile { - public class ModelToResourceProfile : Profile + public ModelToResourceProfile() { - public ModelToResourceProfile() - { - CreateMap() - .ForMember(u => u.Roles, opt => opt.MapFrom(u => u.UserRoles.Select(ur => ur.Role.Name))); + CreateMap() + .ForMember(u => u.Roles, opt => opt.MapFrom(u => u.UserRoles.Select(ur => ur.Role.Name))); - CreateMap() - .ForMember(a => a.AccessToken, opt => opt.MapFrom(a => a.Token)) - .ForMember(a => a.RefreshToken, opt => opt.MapFrom(a => a.RefreshToken.Token)) - .ForMember(a => a.Expiration, opt => opt.MapFrom(a => a.Expiration)); - } + CreateMap() + .ForMember(a => a.AccessToken, opt => opt.MapFrom(a => a.Token)) + .ForMember(a => a.RefreshToken, opt => opt.MapFrom(a => a.RefreshToken.Token)) + .ForMember(a => a.Expiration, opt => opt.MapFrom(a => a.Expiration)); } } \ No newline at end of file diff --git a/src/JWTAPI/JWTAPI/Mapping/ResourceToModelProfile.cs b/src/JWTAPI/JWTAPI/Mapping/ResourceToModelProfile.cs index 381d3c7..87e3444 100644 --- a/src/JWTAPI/JWTAPI/Mapping/ResourceToModelProfile.cs +++ b/src/JWTAPI/JWTAPI/Mapping/ResourceToModelProfile.cs @@ -1,14 +1,8 @@ -using AutoMapper; -using JWTAPI.Controllers.Resources; -using JWTAPI.Core.Models; - -namespace JWTAPI.Mapping +namespace JWTAPI.Mapping; +public class ResourceToModelProfile : Profile { - public class ResourceToModelProfile : Profile + public ResourceToModelProfile() { - public ResourceToModelProfile() - { - CreateMap(); - } + CreateMap(); } } \ No newline at end of file diff --git a/src/JWTAPI/JWTAPI/Persistence/AppDbContext.cs b/src/JWTAPI/JWTAPI/Persistence/AppDbContext.cs index 23f2b72..b156370 100644 --- a/src/JWTAPI/JWTAPI/Persistence/AppDbContext.cs +++ b/src/JWTAPI/JWTAPI/Persistence/AppDbContext.cs @@ -1,21 +1,16 @@ -using JWTAPI.Core.Models; -using Microsoft.EntityFrameworkCore; - -namespace JWTAPI.Persistence +namespace JWTAPI.Persistence; +public class AppDbContext : DbContext { - public class AppDbContext : DbContext - { - public DbSet Users { get; set; } - public DbSet Roles { get; set; } + public DbSet Users { get; set; } + public DbSet Roles { get; set; } - public AppDbContext(DbContextOptions options) : base(options) - { } + public AppDbContext(DbContextOptions options) : base(options) + { } - protected override void OnModelCreating(ModelBuilder builder) - { - base.OnModelCreating(builder); + protected override void OnModelCreating(ModelBuilder builder) + { + base.OnModelCreating(builder); - builder.Entity().HasKey(ur => new { ur.UserId, ur.RoleId }); - } + builder.Entity().HasKey(ur => new { ur.UserId, ur.RoleId }); } } \ No newline at end of file diff --git a/src/JWTAPI/JWTAPI/Persistence/UnitOfWork.cs b/src/JWTAPI/JWTAPI/Persistence/UnitOfWork.cs index 00f6e71..8fbdda1 100644 --- a/src/JWTAPI/JWTAPI/Persistence/UnitOfWork.cs +++ b/src/JWTAPI/JWTAPI/Persistence/UnitOfWork.cs @@ -1,19 +1,15 @@ -using JWTAPI.Core.Repositories; - -namespace JWTAPI.Persistence +namespace JWTAPI.Persistence; +public class UnitOfWork : IUnitOfWork { - public class UnitOfWork : IUnitOfWork - { - private readonly AppDbContext _context; + private readonly AppDbContext _context; - public UnitOfWork(AppDbContext context) - { - _context = context; - } + public UnitOfWork(AppDbContext context) + { + _context = context; + } - public async Task CompleteAsync() - { - await _context.SaveChangesAsync(); - } + public async Task CompleteAsync() + { + await _context.SaveChangesAsync(); } } \ No newline at end of file diff --git a/src/JWTAPI/JWTAPI/Persistence/UserRepository.cs b/src/JWTAPI/JWTAPI/Persistence/UserRepository.cs index d6410ea..460b55c 100644 --- a/src/JWTAPI/JWTAPI/Persistence/UserRepository.cs +++ b/src/JWTAPI/JWTAPI/Persistence/UserRepository.cs @@ -1,5 +1,4 @@ namespace JWTAPI.Persistence; - public class UserRepository : IUserRepository { private readonly AppDbContext _context; diff --git a/src/JWTAPI/JWTAPI/Program.cs b/src/JWTAPI/JWTAPI/Program.cs index 85dc82e..9145346 100644 --- a/src/JWTAPI/JWTAPI/Program.cs +++ b/src/JWTAPI/JWTAPI/Program.cs @@ -1,61 +1,13 @@ -using JWTAPI.Core.Repositories; -using JWTAPI.Core.Security.Hashing; -using JWTAPI.Core.Security.Tokens; -using JWTAPI.Core.Services; -using JWTAPI.Extensions; -using JWTAPI.Persistence; -using JWTAPI.Security.Hashing; -using JWTAPI.Security.Tokens; -using JWTAPI.Services; -using Microsoft.AspNetCore.Authentication.JwtBearer; -using Microsoft.EntityFrameworkCore; -using Microsoft.IdentityModel.Tokens; -using System.Reflection; - -var builder = WebApplication.CreateBuilder(args); - -builder.Services.AddDbContext(options => -{ - options.UseInMemoryDatabase("jwtapi"); -}); - -builder.Services.AddControllers(); +var builder = WebApplication.CreateBuilder(args); builder.Services.AddCustomSwagger(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); - -builder.Services.AddSingleton(); -builder.Services.AddSingleton(); +builder.Services.AddApplicationServices(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); - -builder.Services.Configure(builder.Configuration.GetSection("TokenOptions")); -var tokenOptions = builder.Configuration.GetSection("TokenOptions").Get(); - -var signingConfigurations = new SigningConfigurations(tokenOptions.Secret); -builder.Services.AddSingleton(signingConfigurations); - -builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) - .AddJwtBearer(jwtBearerOptions => - { - jwtBearerOptions.TokenValidationParameters = new TokenValidationParameters() - { - ValidateAudience = true, - ValidateLifetime = true, - ValidateIssuerSigningKey = true, - ValidIssuer = tokenOptions.Issuer, - ValidAudience = tokenOptions.Audience, - IssuerSigningKey = signingConfigurations.SecurityKey, - ClockSkew = TimeSpan.Zero - }; - }); - -builder.Services.AddAutoMapper(Assembly.GetExecutingAssembly()); +builder.Services.AddIdentityServices(builder.Configuration); var app = builder.Build(); + app.UseDeveloperExceptionPage(); app.UseRouting(); @@ -63,6 +15,7 @@ app.UseCustomSwagger(); app.UseAuthentication(); + app.UseAuthorization(); app.UseEndpoints(endpoints => diff --git a/src/JWTAPI/JWTAPI/Security/Hashing/PasswordHasher.cs b/src/JWTAPI/JWTAPI/Security/Hashing/PasswordHasher.cs index cf43553..68f5d78 100644 --- a/src/JWTAPI/JWTAPI/Security/Hashing/PasswordHasher.cs +++ b/src/JWTAPI/JWTAPI/Security/Hashing/PasswordHasher.cs @@ -1,81 +1,76 @@ -using System.Runtime.CompilerServices; -using System.Security.Cryptography; -using JWTAPI.Core.Security.Hashing; +namespace JWTAPI.Security.Hashing; -namespace JWTAPI.Security.Hashing +/// +/// This password hasher is the same used by ASP.NET Identity. +/// Explanation: https://stackoverflow.com/questions/20621950/asp-net-identity-default-password-hasher-how-does-it-work-and-is-it-secure +/// Full implementation: https://gist.github.com/malkafly/e873228cb9515010bdbe +/// +public class PasswordHasher : IPasswordHasher { - /// - /// This password hasher is the same used by ASP.NET Identity. - /// Explanation: https://stackoverflow.com/questions/20621950/asp-net-identity-default-password-hasher-how-does-it-work-and-is-it-secure - /// Full implementation: https://gist.github.com/malkafly/e873228cb9515010bdbe - /// - public class PasswordHasher : IPasswordHasher + public string HashPassword(string password) { - public string HashPassword(string password) + byte[] salt; + byte[] buffer2; + if (string.IsNullOrEmpty(password)) { - byte[] salt; - byte[] buffer2; - if (string.IsNullOrEmpty(password)) - { - throw new ArgumentNullException("password"); - } - using(Rfc2898DeriveBytes bytes = new Rfc2898DeriveBytes(password, 0x10, 0x3e8)) - { - salt = bytes.Salt; - buffer2 = bytes.GetBytes(0x20); - } - byte[] dst = new byte[0x31]; - Buffer.BlockCopy(salt, 0, dst, 1, 0x10); - Buffer.BlockCopy(buffer2, 0, dst, 0x11, 0x20); - return Convert.ToBase64String(dst); + throw new ArgumentNullException(nameof(password)); } + using (Rfc2898DeriveBytes bytes = new(password, 0x10, 0x3e8)) + { + salt = bytes.Salt; + buffer2 = bytes.GetBytes(0x20); + } + byte[] dst = new byte[0x31]; + Buffer.BlockCopy(salt, 0, dst, 1, 0x10); + Buffer.BlockCopy(buffer2, 0, dst, 0x11, 0x20); + return Convert.ToBase64String(dst); + } - public bool PasswordMatches(string providedPassword, string passwordHash) + public bool PasswordMatches(string providedPassword, string passwordHash) + { + byte[] buffer4; + if (passwordHash == null) + { + return false; + } + if (providedPassword == null) + { + throw new ArgumentNullException(nameof(providedPassword)); + } + byte[] src = Convert.FromBase64String(passwordHash); + if ((src.Length != 0x31) || (src[0] != 0)) { - byte[] buffer4; - if (passwordHash == null) - { - return false; - } - if (providedPassword == null) - { - throw new ArgumentNullException("providedPassword"); - } - byte[] src = Convert.FromBase64String(passwordHash); - if ((src.Length != 0x31) || (src[0] != 0)) - { - return false; - } - byte[] dst = new byte[0x10]; - Buffer.BlockCopy(src, 1, dst, 0, 0x10); - byte[] buffer3 = new byte[0x20]; - Buffer.BlockCopy(src, 0x11, buffer3, 0, 0x20); - using(Rfc2898DeriveBytes bytes = new Rfc2898DeriveBytes(providedPassword, dst, 0x3e8)) - { - buffer4 = bytes.GetBytes(0x20); - } - return ByteArraysEqual(buffer3, buffer4); + return false; } + byte[] dst = new byte[0x10]; + Buffer.BlockCopy(src, 1, dst, 0, 0x10); + byte[] buffer3 = new byte[0x20]; + Buffer.BlockCopy(src, 0x11, buffer3, 0, 0x20); + using (Rfc2898DeriveBytes bytes = new(providedPassword, dst, 0x3e8)) + { + buffer4 = bytes.GetBytes(0x20); + } + return ByteArraysEqual(buffer3, buffer4); + } - [MethodImpl(MethodImplOptions.NoOptimization)] - private bool ByteArraysEqual(byte[] a, byte[] b) + [MethodImpl(MethodImplOptions.NoOptimization)] + private bool ByteArraysEqual(byte[] a, byte[] b) + { + if (ReferenceEquals(a, b)) { - if (ReferenceEquals(a, b)) - { - return true; - } + return true; + } - if (a == null || b == null || a.Length != b.Length) - { - return false; - } + if (a == null || b == null || a.Length != b.Length) + { + return false; + } - bool areSame = true; - for (int i = 0; i < a.Length; i++) - { - areSame &= (a[i] == b[i]); - } - return areSame; + bool areSame = true; + for (int i = 0; i < a.Length; i++) + { + areSame &= (a[i] == b[i]); } + return areSame; } } \ No newline at end of file diff --git a/src/JWTAPI/JWTAPI/Security/Tokens/SigningConfigurations.cs b/src/JWTAPI/JWTAPI/Security/Tokens/SigningConfigurations.cs index 55f087e..3ac916f 100644 --- a/src/JWTAPI/JWTAPI/Security/Tokens/SigningConfigurations.cs +++ b/src/JWTAPI/JWTAPI/Security/Tokens/SigningConfigurations.cs @@ -1,19 +1,15 @@ -using System.Text; -using Microsoft.IdentityModel.Tokens; +namespace JWTAPI.Security.Tokens; -namespace JWTAPI.Security.Tokens +public class SigningConfigurations { - public class SigningConfigurations - { - public SecurityKey SecurityKey { get; } - public SigningCredentials SigningCredentials { get; } + public SecurityKey SecurityKey { get; } + public SigningCredentials SigningCredentials { get; } - public SigningConfigurations(string key) - { - var keyBytes = Encoding.ASCII.GetBytes(key); + public SigningConfigurations(string key) + { + var keyBytes = Encoding.ASCII.GetBytes(key); - SecurityKey = new SymmetricSecurityKey(keyBytes); - SigningCredentials = new SigningCredentials(SecurityKey, SecurityAlgorithms.HmacSha256Signature); - } + SecurityKey = new SymmetricSecurityKey(keyBytes); + SigningCredentials = new SigningCredentials(SecurityKey, SecurityAlgorithms.HmacSha256Signature); } } \ No newline at end of file diff --git a/src/JWTAPI/JWTAPI/Security/Tokens/TokenOptions.cs b/src/JWTAPI/JWTAPI/Security/Tokens/TokenOptions.cs index cc191bf..ec14526 100644 --- a/src/JWTAPI/JWTAPI/Security/Tokens/TokenOptions.cs +++ b/src/JWTAPI/JWTAPI/Security/Tokens/TokenOptions.cs @@ -1,11 +1,9 @@ -namespace JWTAPI.Security.Tokens +namespace JWTAPI.Security.Tokens; +public class TokenOptions { - public class TokenOptions - { - public string Audience { get; set; } - public string Issuer { get; set; } - public long AccessTokenExpiration { get; set; } - public long RefreshTokenExpiration { get; set; } - public string Secret { get; set; } - } + public string Audience { get; set; } + public string Issuer { get; set; } + public long AccessTokenExpiration { get; set; } + public long RefreshTokenExpiration { get; set; } + public string Secret { get; set; } } \ No newline at end of file diff --git a/src/JWTAPI/JWTAPI/Services/UserService.cs b/src/JWTAPI/JWTAPI/Services/UserService.cs index 302be17..0e92f5b 100644 --- a/src/JWTAPI/JWTAPI/Services/UserService.cs +++ b/src/JWTAPI/JWTAPI/Services/UserService.cs @@ -1,5 +1,4 @@ namespace JWTAPI.Services; - public class UserService : IUserService { private readonly IUserRepository _userRepository; diff --git a/tests/JWTAPI.Tests/Security/Hashing/PasswordHasherTests.cs b/tests/JWTAPI.Tests/Security/Hashing/PasswordHasherTests.cs index 53df401..bc685bb 100644 --- a/tests/JWTAPI.Tests/Security/Hashing/PasswordHasherTests.cs +++ b/tests/JWTAPI.Tests/Security/Hashing/PasswordHasherTests.cs @@ -1,55 +1,49 @@ -using System; -using JWTAPI.Core.Security.Hashing; -using JWTAPI.Security.Hashing; -using Xunit; +namespace JWTPAPI.Tests.Security.Hashing; -namespace JWTPAPI.Tests.Security.Hashing +public class PasswordHasherTests { - public class PasswordHasherTests + private IPasswordHasher _passwordHasher = new PasswordHasher(); + + [Fact] + public void Should_Throw_Exception_For_Empty_Password_When_Hashing() + { + var password = ""; + Assert.Throws(() => _passwordHasher.HashPassword(password)); + } + + [Fact] + public void Should_Hash_Passwords() { - private IPasswordHasher _passwordHasher = new PasswordHasher(); - - [Fact] - public void Should_Throw_Exception_For_Empty_Password_When_Hashing() - { - var password = ""; - Assert.Throws(() => _passwordHasher.HashPassword(password)); - } - - [Fact] - public void Should_Hash_Passwords() - { - var firstPassword = "123456"; - var secondPassword = "123456"; - - var firstPasswordAsHash = _passwordHasher.HashPassword(firstPassword); - var secondPasswordAsHash = _passwordHasher.HashPassword(secondPassword); - - Assert.NotSame(firstPasswordAsHash, firstPassword); - Assert.NotSame(secondPasswordAsHash, secondPassword); - Assert.NotSame(firstPasswordAsHash, secondPasswordAsHash); - } - - [Fact] - public void Should_Match_Password_For_Valid_Hash() - { - var firstPassword = "123456"; - var firstPasswordAsHash = _passwordHasher.HashPassword(firstPassword); - - Assert.True(_passwordHasher.PasswordMatches(firstPassword, firstPasswordAsHash)); - } - - [Fact] - public void Should_Return_False_For_Different_Hasher_Passwords() - { - var firstPassword = "123456"; - var secondPassword = "654321"; - - var firstPasswordAsHash = _passwordHasher.HashPassword(firstPassword); - var secondPasswordAsHash = _passwordHasher.HashPassword(secondPassword); - - Assert.False(_passwordHasher.PasswordMatches(firstPassword, secondPasswordAsHash)); - Assert.False(_passwordHasher.PasswordMatches(secondPassword, firstPasswordAsHash)); - } + var firstPassword = "123456"; + var secondPassword = "123456"; + + var firstPasswordAsHash = _passwordHasher.HashPassword(firstPassword); + var secondPasswordAsHash = _passwordHasher.HashPassword(secondPassword); + + Assert.NotSame(firstPasswordAsHash, firstPassword); + Assert.NotSame(secondPasswordAsHash, secondPassword); + Assert.NotSame(firstPasswordAsHash, secondPasswordAsHash); + } + + [Fact] + public void Should_Match_Password_For_Valid_Hash() + { + var firstPassword = "123456"; + var firstPasswordAsHash = _passwordHasher.HashPassword(firstPassword); + + Assert.True(_passwordHasher.PasswordMatches(firstPassword, firstPasswordAsHash)); + } + + [Fact] + public void Should_Return_False_For_Different_Hasher_Passwords() + { + var firstPassword = "123456"; + var secondPassword = "654321"; + + var firstPasswordAsHash = _passwordHasher.HashPassword(firstPassword); + var secondPasswordAsHash = _passwordHasher.HashPassword(secondPassword); + + Assert.False(_passwordHasher.PasswordMatches(firstPassword, secondPasswordAsHash)); + Assert.False(_passwordHasher.PasswordMatches(secondPassword, firstPasswordAsHash)); } } \ No newline at end of file diff --git a/tests/JWTAPI.Tests/Security/Tokens/TokenHandlerTests.cs b/tests/JWTAPI.Tests/Security/Tokens/TokenHandlerTests.cs index 1a805d9..96a4266 100644 --- a/tests/JWTAPI.Tests/Security/Tokens/TokenHandlerTests.cs +++ b/tests/JWTAPI.Tests/Security/Tokens/TokenHandlerTests.cs @@ -1,141 +1,130 @@ -using System; -using System.Collections.ObjectModel; -using JWTAPI.Core.Models; -using JWTAPI.Core.Security.Hashing; -using JWTAPI.Core.Security.Tokens; -using JWTAPI.Security.Tokens; -using Microsoft.Extensions.Options; -using Moq; -using Xunit; - -namespace JWTPAPI.Tests.Security.Tokens +namespace JWTPAPI.Tests.Security.Tokens; + +public class TokenHandlerTests { - public class TokenHandlerTests - { - private Mock> _tokenOptions; - private Mock _passwordHasher; - private SigningConfigurations _signingConfigurations; - private User _user; + private Mock> _tokenOptions; + private Mock _passwordHasher; + private SigningConfigurations _signingConfigurations; + private User _user; - private ITokenHandler _tokenHandler; - private string _testKey = "just_a_long_test_key_to_use_for_tokens"; + private ITokenHandler _tokenHandler; + private string _testKey = "just_a_long_test_key_to_use_for_tokens"; - public TokenHandlerTests() - { - SetupMocks(); - _tokenHandler = new TokenHandler(_tokenOptions.Object, _signingConfigurations, _passwordHasher.Object); - } + public TokenHandlerTests() + { + SetupMocks(); + _tokenHandler = new TokenHandler(_tokenOptions.Object, _signingConfigurations, _passwordHasher.Object); + } - private void SetupMocks() + private void SetupMocks() + { + _tokenOptions = new Mock>(); + _tokenOptions.Setup(to => to.Value).Returns(new TokenOptions { - _tokenOptions = new Mock>(); - _tokenOptions.Setup(to => to.Value).Returns(new TokenOptions - { - Audience = "Testing", - Issuer = "Testing", - AccessTokenExpiration = 30, - RefreshTokenExpiration = 60 - }); + Audience = "Testing", + Issuer = "Testing", + AccessTokenExpiration = 30, + RefreshTokenExpiration = 60 + }); - _passwordHasher = new Mock(); - _passwordHasher.Setup(ph => ph.HashPassword(It.IsAny())).Returns("123"); + _passwordHasher = new Mock(); + _passwordHasher.Setup(ph => ph.HashPassword(It.IsAny())).Returns("123"); - _signingConfigurations = new SigningConfigurations(_testKey); + _signingConfigurations = new SigningConfigurations(_testKey); - _user = new User + _user = new User + { + Id = 1, + Email = "test@test.com", + Password = "123", + UserRoles = new Collection { - Id = 1, - Email = "test@test.com", - Password = "123", - UserRoles = new Collection + new UserRole { - new UserRole + Role = new Role { - Role = new Role - { - Id = 1, - Name = ApplicationRole.Common.ToString() - } + Id = 1, + Name = ApplicationRole.Common.ToString() } } - }; - } + } + }; + } - [Fact] - public void Should_Create_Access_Token() - { - var accessToken = _tokenHandler.CreateAccessToken(_user); - - Assert.NotNull(accessToken); - Assert.NotNull(accessToken.RefreshToken); - Assert.NotEmpty(accessToken.Token); - Assert.NotEmpty(accessToken.RefreshToken.Token); - Assert.True(accessToken.Expiration > DateTime.UtcNow.Ticks); - Assert.True(accessToken.RefreshToken.Expiration > DateTime.UtcNow.Ticks); - Assert.True(accessToken.RefreshToken.Expiration > accessToken.Expiration); - } - - [Fact] - public void Should_Take_Existing_Refresh_Token() - { - var accessToken = _tokenHandler.CreateAccessToken(_user); - var refreshToken = _tokenHandler.TakeRefreshToken(accessToken.RefreshToken.Token, "test@test.com"); + [Fact] + public void Should_Create_Access_Token() + { + var accessToken = _tokenHandler.CreateAccessToken(_user); + + Assert.NotNull(accessToken); + Assert.NotNull(accessToken.RefreshToken); + Assert.NotEmpty(accessToken.Token); + Assert.NotEmpty(accessToken.RefreshToken.Token); + Assert.True(accessToken.Expiration > DateTime.UtcNow.Ticks); + Assert.True(accessToken.RefreshToken.Expiration > DateTime.UtcNow.Ticks); + Assert.True(accessToken.RefreshToken.Expiration > accessToken.Expiration); + } - Assert.NotNull(refreshToken); - Assert.Equal(accessToken.RefreshToken.Token, refreshToken.Token); - Assert.Equal(accessToken.RefreshToken.Expiration, refreshToken.Expiration); - } + [Fact] + public void Should_Take_Existing_Refresh_Token() + { + var accessToken = _tokenHandler.CreateAccessToken(_user); + var refreshToken = _tokenHandler.TakeRefreshToken(accessToken.RefreshToken.Token, "test@test.com"); - [Fact] - public void Should_Return_Null_For_Empty_Refresh_Token_When_Trying_To_Take_Refresh_Token() - { - var refreshToken = _tokenHandler.TakeRefreshToken(string.Empty, "test@test.com"); - Assert.Null(refreshToken); - } + Assert.NotNull(refreshToken); + Assert.Equal(accessToken.RefreshToken.Token, refreshToken.Token); + Assert.Equal(accessToken.RefreshToken.Expiration, refreshToken.Expiration); + } + + [Fact] + public void Should_Return_Null_For_Empty_Refresh_Token_When_Trying_To_Take_Refresh_Token() + { + var refreshToken = _tokenHandler.TakeRefreshToken(string.Empty, "test@test.com"); + Assert.Null(refreshToken); + } [Fact] - public void Should_Return_Null_For_Empty_Email_When_Trying_To_Take_Refresh_Token() + public void Should_Return_Null_For_Empty_Email_When_Trying_To_Take_Refresh_Token() { - var accessToken = _tokenHandler.CreateAccessToken(_user); - var refreshToken = _tokenHandler.TakeRefreshToken(accessToken.RefreshToken.Token, string.Empty); + var accessToken = _tokenHandler.CreateAccessToken(_user); + var refreshToken = _tokenHandler.TakeRefreshToken(accessToken.RefreshToken.Token, string.Empty); - Assert.Null(refreshToken); - } + Assert.Null(refreshToken); + } - [Fact] - public void Should_Return_Null_For_Invalid_Refresh_Token_When_Trying_To_Take_Refresh_oken() - { - var refreshToken = _tokenHandler.TakeRefreshToken("invalid_token", "test@test.com"); - Assert.Null(refreshToken); - } + [Fact] + public void Should_Return_Null_For_Invalid_Refresh_Token_When_Trying_To_Take_Refresh_oken() + { + var refreshToken = _tokenHandler.TakeRefreshToken("invalid_token", "test@test.com"); + Assert.Null(refreshToken); + } - [Fact] - public void Should_Return_Null_For_Invalid_Email_When_Trying_To_Take_Refresh_Token() - { - var accessToken = _tokenHandler.CreateAccessToken(_user); - var refreshToken = _tokenHandler.TakeRefreshToken(accessToken.RefreshToken.Token, "admin@admin.com"); - Assert.Null(refreshToken); - } + [Fact] + public void Should_Return_Null_For_Invalid_Email_When_Trying_To_Take_Refresh_Token() + { + var accessToken = _tokenHandler.CreateAccessToken(_user); + var refreshToken = _tokenHandler.TakeRefreshToken(accessToken.RefreshToken.Token, "admin@admin.com"); + Assert.Null(refreshToken); + } - [Fact] - public void Should_Not_Take_Refresh_Token_That_Was_Already_Taken() - { - var accessToken = _tokenHandler.CreateAccessToken(_user); - var refreshToken = _tokenHandler.TakeRefreshToken(accessToken.RefreshToken.Token, "test@test.com"); - var refreshTokenSecondTime = _tokenHandler.TakeRefreshToken(accessToken.RefreshToken.Token, "test@test.com"); + [Fact] + public void Should_Not_Take_Refresh_Token_That_Was_Already_Taken() + { + var accessToken = _tokenHandler.CreateAccessToken(_user); + var refreshToken = _tokenHandler.TakeRefreshToken(accessToken.RefreshToken.Token, "test@test.com"); + var refreshTokenSecondTime = _tokenHandler.TakeRefreshToken(accessToken.RefreshToken.Token, "test@test.com"); - Assert.NotNull(refreshToken); - Assert.Null(refreshTokenSecondTime); - } + Assert.NotNull(refreshToken); + Assert.Null(refreshTokenSecondTime); + } - [Fact] - public void Should_Revoke_Refresh_Token() - { - var accessToken = _tokenHandler.CreateAccessToken(_user); - _tokenHandler.RevokeRefreshToken(accessToken.RefreshToken.Token, "test@test.com"); - var refreshToken = _tokenHandler.TakeRefreshToken(accessToken.RefreshToken.Token, "test@test.com"); + [Fact] + public void Should_Revoke_Refresh_Token() + { + var accessToken = _tokenHandler.CreateAccessToken(_user); + _tokenHandler.RevokeRefreshToken(accessToken.RefreshToken.Token, "test@test.com"); + var refreshToken = _tokenHandler.TakeRefreshToken(accessToken.RefreshToken.Token, "test@test.com"); - Assert.Null(refreshToken); - } + Assert.Null(refreshToken); } } \ No newline at end of file diff --git a/tests/JWTAPI.Tests/Services/AuthenticationServiceTests.cs b/tests/JWTAPI.Tests/Services/AuthenticationServiceTests.cs index 9ac9b40..19c9bea 100644 --- a/tests/JWTAPI.Tests/Services/AuthenticationServiceTests.cs +++ b/tests/JWTAPI.Tests/Services/AuthenticationServiceTests.cs @@ -1,161 +1,148 @@ -using System; -using System.Collections.ObjectModel; -using System.Threading.Tasks; -using JWTAPI.Core.Models; -using JWTAPI.Core.Security.Hashing; -using JWTAPI.Core.Security.Tokens; -using JWTAPI.Core.Services; -using JWTAPI.Services; -using Moq; -using Xunit; - -namespace JWTPAPI.Tests.Services +namespace JWTPAPI.Tests.Services; +public class AuthenticationServiceTests { - public class AuthenticationServiceTests - { - private bool _calledRefreshToken = false; - - private Mock _userService; - private Mock _passwordHasher; - private Mock _tokenHandler; + private bool _calledRefreshToken = false; - private IAuthenticationService _authenticationService; + private Mock _userService; + private Mock _passwordHasher; + private Mock _tokenHandler; - public AuthenticationServiceTests() - { - SetupMocks(); - _authenticationService = new AuthenticationService(_userService.Object, _passwordHasher.Object, _tokenHandler.Object); - } + private IAuthenticationService _authenticationService; - private void SetupMocks() - { - _userService = new Mock(); - _userService.Setup(u => u.FindByEmailAsync("invalid@invalid.com")) - .Returns(Task.FromResult(null)); + public AuthenticationServiceTests() + { + SetupMocks(); + _authenticationService = new AuthenticationService(_userService.Object, _passwordHasher.Object, _tokenHandler.Object); + } - _userService.Setup(u => u.FindByEmailAsync("test@test.com")) - .ReturnsAsync(new User + private void SetupMocks() + { + _userService = new Mock(); + _userService.Setup(u => u.FindByEmailAsync("invalid@invalid.com")) + .Returns(Task.FromResult(null)); + + _userService.Setup(u => u.FindByEmailAsync("test@test.com")) + .ReturnsAsync(new User + { + Id = 1, + Email = "test@test.com", + Password = "123", + UserRoles = new Collection { - Id = 1, - Email = "test@test.com", - Password = "123", - UserRoles = new Collection + new UserRole { - new UserRole + UserId = 1, + RoleId = 1, + Role = new Role { - UserId = 1, - RoleId = 1, - Role = new Role - { - Id = 1, - Name = ApplicationRole.Common.ToString() - } + Id = 1, + Name = ApplicationRole.Common.ToString() } } - }); - - _passwordHasher = new Mock(); - _passwordHasher.Setup(ph => ph.PasswordMatches(It.IsAny(), It.IsAny())) - .Returns((password, hash) => password == hash); - - _tokenHandler = new Mock(); - _tokenHandler.Setup(h => h.CreateAccessToken(It.IsAny())) - .Returns(new AccessToken - ( - token: "abc", - expiration: DateTime.UtcNow.AddSeconds(30).Ticks, - refreshToken: new RefreshToken - ( - token: "abc", - expiration: DateTime.UtcNow.AddSeconds(60).Ticks - ) - ) - ); - - _tokenHandler.Setup(h => h.TakeRefreshToken("abc", It.IsAny())) - .Returns(new RefreshToken("abc", DateTime.UtcNow.AddSeconds(60).Ticks)); - - _tokenHandler.Setup(h => h.TakeRefreshToken("expired", It.IsAny())) - .Returns(new RefreshToken("expired", DateTime.UtcNow.AddSeconds(-60).Ticks)); - - _tokenHandler.Setup(h => h.TakeRefreshToken("invalid", It.IsAny())) - .Returns(null); - - _tokenHandler.Setup(h => h.RevokeRefreshToken("abc", It.IsAny())) - .Callback(() => _calledRefreshToken = true); - } - - [Fact] - public async Task Should_Create_Access_Token_For_Valid_Credentials() - { - var response = await _authenticationService.CreateAccessTokenAsync("test@test.com", "123"); - - Assert.NotNull(response); - Assert.True(response.Success); - Assert.NotNull(response.Token); - Assert.NotNull(response.Token.RefreshToken); - Assert.Equal("abc", response.Token.Token); - Assert.Equal("abc", response.Token.RefreshToken.Token); - Assert.False(response.Token.IsExpired()); - Assert.False(response.Token.RefreshToken.IsExpired()); - } - - [Fact] - public async Task Should_Not_Create_Access_Token_For_Non_Existing_User() - { - var response = await _authenticationService.CreateAccessTokenAsync("invalid@invalid.com", "123"); - - Assert.NotNull(response); - Assert.False(response.Success); - Assert.Equal("Invalid credentials.", response.Message); - } - - [Fact] - public async Task Should_Not_Create_Access_Token_For_Invalid_Password() - { - var response = await _authenticationService.CreateAccessTokenAsync("invalid@invalid.com", "321"); - - Assert.NotNull(response); - Assert.False(response.Success); - Assert.Equal("Invalid credentials.", response.Message); - } - - [Fact] - public async Task Should_Refresh_Token_For_Valid_Refresh_Token() - { - var response = await _authenticationService.RefreshTokenAsync("abc", "test@test.com"); - - Assert.NotNull(response); - Assert.True(response.Success); - Assert.Equal("abc", response.Token.Token); - } - - [Fact] - public async Task Should_Not_Refresh_Token_When_Token_Is_Expired() - { - var response = await _authenticationService.RefreshTokenAsync("expired", "test@test.com"); - - Assert.NotNull(response); - Assert.False(response.Success); - Assert.Equal("Expired refresh token.", response.Message); - } - - [Fact] - public async Task Should_Not_Refresh_Token_For_Invalid_User_Data() - { - var response = await _authenticationService.RefreshTokenAsync("invalid", "test@test.com"); - - Assert.NotNull(response); - Assert.False(response.Success); - Assert.Equal("Invalid refresh token.", response.Message); - } - - [Fact] - public void Should_Revoke_Refresh_Token() - { - _authenticationService.RevokeRefreshToken("abc", "test@test.com"); - - Assert.True(_calledRefreshToken); - } + } + }); + + _passwordHasher = new Mock(); + _passwordHasher.Setup(ph => ph.PasswordMatches(It.IsAny(), It.IsAny())) + .Returns((password, hash) => password == hash); + + _tokenHandler = new Mock(); + _tokenHandler.Setup(h => h.CreateAccessToken(It.IsAny())) + .Returns(new AccessToken + ( + token: "abc", + expiration: DateTime.UtcNow.AddSeconds(30).Ticks, + refreshToken: new RefreshToken + ( + token: "abc", + expiration: DateTime.UtcNow.AddSeconds(60).Ticks + ) + ) + ); + + _tokenHandler.Setup(h => h.TakeRefreshToken("abc", It.IsAny())) + .Returns(new RefreshToken("abc", DateTime.UtcNow.AddSeconds(60).Ticks)); + + _tokenHandler.Setup(h => h.TakeRefreshToken("expired", It.IsAny())) + .Returns(new RefreshToken("expired", DateTime.UtcNow.AddSeconds(-60).Ticks)); + + _tokenHandler.Setup(h => h.TakeRefreshToken("invalid", It.IsAny())) + .Returns(null); + + _tokenHandler.Setup(h => h.RevokeRefreshToken("abc", It.IsAny())) + .Callback(() => _calledRefreshToken = true); + } + + [Fact] + public async Task Should_Create_Access_Token_For_Valid_Credentials() + { + var response = await _authenticationService.CreateAccessTokenAsync("test@test.com", "123"); + + Assert.NotNull(response); + Assert.True(response.Success); + Assert.NotNull(response.Token); + Assert.NotNull(response.Token.RefreshToken); + Assert.Equal("abc", response.Token.Token); + Assert.Equal("abc", response.Token.RefreshToken.Token); + Assert.False(response.Token.IsExpired()); + Assert.False(response.Token.RefreshToken.IsExpired()); + } + + [Fact] + public async Task Should_Not_Create_Access_Token_For_Non_Existing_User() + { + var response = await _authenticationService.CreateAccessTokenAsync("invalid@invalid.com", "123"); + + Assert.NotNull(response); + Assert.False(response.Success); + Assert.Equal("Invalid credentials.", response.Message); + } + + [Fact] + public async Task Should_Not_Create_Access_Token_For_Invalid_Password() + { + var response = await _authenticationService.CreateAccessTokenAsync("invalid@invalid.com", "321"); + + Assert.NotNull(response); + Assert.False(response.Success); + Assert.Equal("Invalid credentials.", response.Message); + } + + [Fact] + public async Task Should_Refresh_Token_For_Valid_Refresh_Token() + { + var response = await _authenticationService.RefreshTokenAsync("abc", "test@test.com"); + + Assert.NotNull(response); + Assert.True(response.Success); + Assert.Equal("abc", response.Token.Token); + } + + [Fact] + public async Task Should_Not_Refresh_Token_When_Token_Is_Expired() + { + var response = await _authenticationService.RefreshTokenAsync("expired", "test@test.com"); + + Assert.NotNull(response); + Assert.False(response.Success); + Assert.Equal("Expired refresh token.", response.Message); + } + + [Fact] + public async Task Should_Not_Refresh_Token_For_Invalid_User_Data() + { + var response = await _authenticationService.RefreshTokenAsync("invalid", "test@test.com"); + + Assert.NotNull(response); + Assert.False(response.Success); + Assert.Equal("Invalid refresh token.", response.Message); + } + + [Fact] + public void Should_Revoke_Refresh_Token() + { + _authenticationService.RevokeRefreshToken("abc", "test@test.com"); + + Assert.True(_calledRefreshToken); } } \ No newline at end of file diff --git a/tests/JWTAPI.Tests/Services/UserServiceTests.cs b/tests/JWTAPI.Tests/Services/UserServiceTests.cs index 8a3e824..2e6c659 100644 --- a/tests/JWTAPI.Tests/Services/UserServiceTests.cs +++ b/tests/JWTAPI.Tests/Services/UserServiceTests.cs @@ -1,84 +1,73 @@ -using System.Collections.ObjectModel; -using System.Threading.Tasks; -using JWTAPI.Core.Models; -using JWTAPI.Core.Repositories; -using JWTAPI.Core.Security.Hashing; -using JWTAPI.Core.Services; -using JWTAPI.Services; -using Moq; -using Xunit; +namespace JWTPAPI.Tests.Services; -namespace JWTPAPI.Tests.Services +public class UserServiceTests { - public class UserServiceTests - { - private Mock _passwordHasher; - private Mock _userRepository; - private Mock _unitOfWork; + private Mock _passwordHasher; + private Mock _userRepository; + private Mock _unitOfWork; - private IUserService _userService; + private IUserService _userService; - public UserServiceTests() - { - SetupMocks(); - _userService = new UserService(_userRepository.Object, _unitOfWork.Object, _passwordHasher.Object); - } + public UserServiceTests() + { + SetupMocks(); + _userService = new UserService(_userRepository.Object, _unitOfWork.Object, _passwordHasher.Object); + } - private void SetupMocks() - { - _passwordHasher = new Mock(); - _passwordHasher.Setup(ph => ph.HashPassword(It.IsAny())).Returns("123"); + private void SetupMocks() + { + _passwordHasher = new Mock(); + _passwordHasher.Setup(ph => ph.HashPassword(It.IsAny())).Returns("123"); - _userRepository = new Mock(); - _userRepository.Setup(r => r.FindByEmailAsync("test@test.com")) - .ReturnsAsync(new User { Id = 1, Email = "test@test.com", UserRoles = new Collection() }); + _userRepository = new Mock(); + _userRepository.Setup(r => r.FindByEmailAsync("test@test.com")) + .ReturnsAsync(new User { Id = 1, Email = "test@test.com", UserRoles = new Collection() }); - _userRepository.Setup(r => r.FindByEmailAsync("secondtest@secondtest.com")) - .Returns(Task.FromResult(null)); + _userRepository.Setup(r => r.FindByEmailAsync("secondtest@secondtest.com")) + .Returns(Task.FromResult(null)); - _userRepository.Setup(r => r.AddAsync(It.IsAny(), It.IsAny())).Returns(Task.CompletedTask); + _userRepository.Setup(r => r.AddAsync(It.IsAny(), It.IsAny())).Returns(Task.CompletedTask); - _unitOfWork = new Mock(); - _unitOfWork.Setup(u => u.CompleteAsync()).Returns(Task.CompletedTask); - } + _unitOfWork = new Mock(); + _unitOfWork.Setup(u => u.CompleteAsync()).Returns(Task.CompletedTask); + } - [Fact] - public async Task Should_Create_Non_Existing_User() - { - var user = new User { Email = "mytestuser@mytestuser.com", Password = "123", UserRoles = new Collection() }; - - var response = await _userService.CreateUserAsync(user, ApplicationRole.Common); + [Fact] + public async Task Should_Create_Non_Existing_User() + { + var user = new User { Email = "mytestuser@mytestuser.com", Password = "123", UserRoles = new Collection() }; + + var response = await _userService.CreateUserAsync(user, ApplicationRole.Common); - Assert.NotNull(response); - Assert.True(response.Success); - Assert.Equal(user.Email, response.User.Email); - Assert.Equal(user.Password, response.User.Password); - } + Assert.NotNull(response); + Assert.True(response.Success); + Assert.Equal(user.Email, response.User.Email); + Assert.Equal(user.Password, response.User.Password); + } - [Fact] - public async Task Should_Not_Create_User_When_Email_Is_Alreary_In_Use() - { - var user = new User { Email = "test@test.com", Password = "123", UserRoles = new Collection() }; - - var response = await _userService.CreateUserAsync(user, ApplicationRole.Common); + [Fact] + public async Task Should_Not_Create_User_When_Email_Is_Alreary_In_Use() + { + var user = new User { Email = "test@test.com", Password = "123", UserRoles = new Collection() }; + + var response = await _userService.CreateUserAsync(user, ApplicationRole.Common); - Assert.False(response.Success); - Assert.Equal("Email already in use.", response.Message); - } + Assert.False(response.Success); + Assert.Equal("Email already in use.", response.Message); + } - [Fact] - public async Task Should_Find_Existing_User_By_Email() - { - var user = await _userService.FindByEmailAsync("test@test.com"); - Assert.NotNull(user); - Assert.Equal("test@test.com", user.Email); - } + [Fact] + public async Task Should_Find_Existing_User_By_Email() + { + var user = await _userService.FindByEmailAsync("test@test.com"); + Assert.NotNull(user); + Assert.Equal("test@test.com", user.Email); + } - [Fact] - public async Task Should_Return_Null_When_Trying_To_Find_User_By_Invalid_Email() - { - var user = await _userService.FindByEmailAsync("secondtest@secondtest.com"); - Assert.Null(user); - } + [Fact] + public async Task Should_Return_Null_When_Trying_To_Find_User_By_Invalid_Email() + { + var user = await _userService.FindByEmailAsync("secondtest@secondtest.com"); + Assert.Null(user); } } \ No newline at end of file diff --git a/tests/JWTAPI.Tests/Usings.cs b/tests/JWTAPI.Tests/Usings.cs new file mode 100644 index 0000000..e205531 --- /dev/null +++ b/tests/JWTAPI.Tests/Usings.cs @@ -0,0 +1,14 @@ +global using JWTAPI.Core.Models; +global using JWTAPI.Core.Repositories; +global using JWTAPI.Core.Security.Hashing; +global using JWTAPI.Core.Security.Tokens; +global using JWTAPI.Core.Services; +global using JWTAPI.Security.Hashing; +global using JWTAPI.Security.Tokens; +global using JWTAPI.Services; +global using Microsoft.Extensions.Options; +global using Moq; +global using System; +global using System.Collections.ObjectModel; +global using System.Threading.Tasks; +global using Xunit;