From 250e5f2c9c575694c62cbb9afc1110b2c9cbb7df Mon Sep 17 00:00:00 2001 From: Aaron Po Date: Thu, 26 Feb 2026 23:44:52 -0500 Subject: [PATCH] Refactor authentication services and implement JWT validation logic --- .../API.Core/Controllers/AuthController.cs | 23 +++++---- src/Core/API/API.Core/Program.cs | 2 +- .../ITokenInfrastructure.cs | 6 ++- .../Infrastructure.Jwt.csproj | 4 ++ .../Infrastructure.Jwt/JwtInfrastructure.cs | 50 ++++++++++++++++--- .../Service.Auth/IConfirmationService.cs | 11 ++-- .../Service/Service.Auth/ITokenService.cs | 34 +++++++++++++ 7 files changed, 106 insertions(+), 24 deletions(-) diff --git a/src/Core/API/API.Core/Controllers/AuthController.cs b/src/Core/API/API.Core/Controllers/AuthController.cs index 16ae1f8..54d80b5 100644 --- a/src/Core/API/API.Core/Controllers/AuthController.cs +++ b/src/Core/API/API.Core/Controllers/AuthController.cs @@ -11,8 +11,8 @@ namespace API.Core.Controllers public class AuthController( IRegisterService registerService, ILoginService loginService, - IConfirmationService confirmationService) - : ControllerBase + IConfirmationService confirmationService + ) : ControllerBase { [HttpPost("register")] public async Task> Register( @@ -69,13 +69,16 @@ namespace API.Core.Controllers public async Task Confirm([FromQuery] string token) { var rtn = await confirmationService.ConfirmUserAsync(token); - return Ok(new ResponseBody - { - Message = "User with ID " + rtn.userId + " is confirmed.", - Payload = new ConfirmationPayload( - rtn.userId, rtn.confirmedAt - ) - }); + return Ok( + new ResponseBody + { + Message = "User with ID " + rtn.userId + " is confirmed.", + Payload = new ConfirmationPayload( + rtn.userId, + rtn.confirmedAt + ), + } + ); } } -} \ No newline at end of file +} diff --git a/src/Core/API/API.Core/Program.cs b/src/Core/API/API.Core/Program.cs index 07dce8f..918592e 100644 --- a/src/Core/API/API.Core/Program.cs +++ b/src/Core/API/API.Core/Program.cs @@ -14,8 +14,8 @@ using Infrastructure.Repository.UserAccount; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using Service.Auth; -using Service.UserManagement.User; using Service.Emails; +using Service.UserManagement.User; var builder = WebApplication.CreateBuilder(args); diff --git a/src/Core/Infrastructure/Infrastructure.Jwt/ITokenInfrastructure.cs b/src/Core/Infrastructure/Infrastructure.Jwt/ITokenInfrastructure.cs index 6e6b9f1..2535c2b 100644 --- a/src/Core/Infrastructure/Infrastructure.Jwt/ITokenInfrastructure.cs +++ b/src/Core/Infrastructure/Infrastructure.Jwt/ITokenInfrastructure.cs @@ -1,3 +1,5 @@ +using System.Security.Claims; + namespace Infrastructure.Jwt; public interface ITokenInfrastructure @@ -8,4 +10,6 @@ public interface ITokenInfrastructure DateTime expiry, string secret ); -} + + Task ValidateJwtAsync(string token, string secret); +} \ No newline at end of file diff --git a/src/Core/Infrastructure/Infrastructure.Jwt/Infrastructure.Jwt.csproj b/src/Core/Infrastructure/Infrastructure.Jwt/Infrastructure.Jwt.csproj index cddd219..aa4e1d8 100644 --- a/src/Core/Infrastructure/Infrastructure.Jwt/Infrastructure.Jwt.csproj +++ b/src/Core/Infrastructure/Infrastructure.Jwt/Infrastructure.Jwt.csproj @@ -16,4 +16,8 @@ Version="8.2.1" /> + + + + diff --git a/src/Core/Infrastructure/Infrastructure.Jwt/JwtInfrastructure.cs b/src/Core/Infrastructure/Infrastructure.Jwt/JwtInfrastructure.cs index 2f3ce9d..2616d8f 100644 --- a/src/Core/Infrastructure/Infrastructure.Jwt/JwtInfrastructure.cs +++ b/src/Core/Infrastructure/Infrastructure.Jwt/JwtInfrastructure.cs @@ -3,6 +3,7 @@ using System.Text; using Microsoft.IdentityModel.JsonWebTokens; using Microsoft.IdentityModel.Tokens; using JwtRegisteredClaimNames = System.IdentityModel.Tokens.Jwt.JwtRegisteredClaimNames; +using Domain.Exceptions; namespace Infrastructure.Jwt; @@ -16,16 +17,19 @@ public class JwtInfrastructure : ITokenInfrastructure ) { var handler = new JsonWebTokenHandler(); - - var key = Encoding.UTF8.GetBytes( - secret ?? throw new InvalidOperationException("secret not set") - ); - - // Base claims (always present) + var key = Encoding.UTF8.GetBytes(secret); var claims = new List { new(JwtRegisteredClaimNames.Sub, userId.ToString()), new(JwtRegisteredClaimNames.UniqueName, username), + new( + JwtRegisteredClaimNames.Iat, + DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString() + ), + new( + JwtRegisteredClaimNames.Exp, + new DateTimeOffset(expiry).ToUnixTimeSeconds().ToString() + ), new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), }; @@ -41,4 +45,36 @@ public class JwtInfrastructure : ITokenInfrastructure return handler.CreateToken(tokenDescriptor); } -} + + + public async Task ValidateJwtAsync( + string token, + string secret + ) + { + var handler = new JsonWebTokenHandler(); + var keyBytes = Encoding.UTF8.GetBytes( + secret + ); + var parameters = new TokenValidationParameters + { + ValidateIssuer = false, + ValidateAudience = false, + ValidateLifetime = true, + IssuerSigningKey = new SymmetricSecurityKey(keyBytes), + }; + + try + { + var result = await handler.ValidateTokenAsync(token, parameters); + if (!result.IsValid || result.ClaimsIdentity == null) + throw new UnauthorizedAccessException(); + + return new ClaimsPrincipal(result.ClaimsIdentity); + } + catch (Exception e) + { + throw new UnauthorizedException("Invalid token"); + } + } +} \ No newline at end of file diff --git a/src/Core/Service/Service.Auth/IConfirmationService.cs b/src/Core/Service/Service.Auth/IConfirmationService.cs index 58afbdd..0ab6b12 100644 --- a/src/Core/Service/Service.Auth/IConfirmationService.cs +++ b/src/Core/Service/Service.Auth/IConfirmationService.cs @@ -10,12 +10,13 @@ public interface IConfirmationService Task ConfirmUserAsync(string confirmationToken); } - -public class ConfirmationService(IAuthRepository authRepository) : IConfirmationService +public class ConfirmationService(IAuthRepository authRepository) + : IConfirmationService { - - public async Task ConfirmUserAsync(string confirmationToken) + public async Task ConfirmUserAsync( + string confirmationToken + ) { return new ConfirmationServiceReturn(DateTime.Now, Guid.NewGuid()); } -} \ No newline at end of file +} diff --git a/src/Core/Service/Service.Auth/ITokenService.cs b/src/Core/Service/Service.Auth/ITokenService.cs index 81b0f4a..9590031 100644 --- a/src/Core/Service/Service.Auth/ITokenService.cs +++ b/src/Core/Service/Service.Auth/ITokenService.cs @@ -3,11 +3,21 @@ using Infrastructure.Jwt; namespace Service.Auth; +public enum TokenType +{ + AccessToken, + RefreshToken, + ConfirmationToken, +} + public interface ITokenService { public string GenerateAccessToken(UserAccount user); public string GenerateRefreshToken(UserAccount user); public string GenerateConfirmationToken(UserAccount user); + + public string GenerateToken(UserAccount user) + where T : struct, Enum; } public static class TokenServiceExpirationHours @@ -76,4 +86,28 @@ public class TokenService(ITokenInfrastructure tokenInfrastructure) _confirmationTokenSecret ); } + + public string GenerateToken(UserAccount userAccount) + where T : struct, Enum + { + var tokenType = typeof(T); + if (tokenType == typeof(TokenType)) + { + var tokenTypeValue = (TokenType) + Enum.Parse(tokenType, typeof(T).Name); + return tokenTypeValue switch + { + TokenType.AccessToken => GenerateAccessToken(userAccount), + TokenType.RefreshToken => GenerateRefreshToken(userAccount), + TokenType.ConfirmationToken => GenerateConfirmationToken( + userAccount + ), + _ => throw new InvalidOperationException("Invalid token type"), + }; + } + else + { + throw new InvalidOperationException("Invalid token type"); + } + } }