Refactor authentication services and implement JWT validation logic

This commit is contained in:
Aaron Po
2026-02-26 23:44:52 -05:00
parent 0ab2eaaec9
commit 250e5f2c9c
7 changed files with 106 additions and 24 deletions

View File

@@ -11,8 +11,8 @@ namespace API.Core.Controllers
public class AuthController( public class AuthController(
IRegisterService registerService, IRegisterService registerService,
ILoginService loginService, ILoginService loginService,
IConfirmationService confirmationService) IConfirmationService confirmationService
: ControllerBase ) : ControllerBase
{ {
[HttpPost("register")] [HttpPost("register")]
public async Task<ActionResult<UserAccount>> Register( public async Task<ActionResult<UserAccount>> Register(
@@ -69,13 +69,16 @@ namespace API.Core.Controllers
public async Task<ActionResult> Confirm([FromQuery] string token) public async Task<ActionResult> Confirm([FromQuery] string token)
{ {
var rtn = await confirmationService.ConfirmUserAsync(token); var rtn = await confirmationService.ConfirmUserAsync(token);
return Ok(new ResponseBody<ConfirmationPayload> return Ok(
{ new ResponseBody<ConfirmationPayload>
Message = "User with ID " + rtn.userId + " is confirmed.", {
Payload = new ConfirmationPayload( Message = "User with ID " + rtn.userId + " is confirmed.",
rtn.userId, rtn.confirmedAt Payload = new ConfirmationPayload(
) rtn.userId,
}); rtn.confirmedAt
),
}
);
} }
} }
} }

View File

@@ -14,8 +14,8 @@ using Infrastructure.Repository.UserAccount;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.Filters;
using Service.Auth; using Service.Auth;
using Service.UserManagement.User;
using Service.Emails; using Service.Emails;
using Service.UserManagement.User;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);

View File

@@ -1,3 +1,5 @@
using System.Security.Claims;
namespace Infrastructure.Jwt; namespace Infrastructure.Jwt;
public interface ITokenInfrastructure public interface ITokenInfrastructure
@@ -8,4 +10,6 @@ public interface ITokenInfrastructure
DateTime expiry, DateTime expiry,
string secret string secret
); );
}
Task<ClaimsPrincipal> ValidateJwtAsync(string token, string secret);
}

View File

@@ -16,4 +16,8 @@
Version="8.2.1" Version="8.2.1"
/> />
</ItemGroup> </ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Domain.Exceptions\Domain.Exceptions.csproj" />
</ItemGroup>
</Project> </Project>

View File

@@ -3,6 +3,7 @@ using System.Text;
using Microsoft.IdentityModel.JsonWebTokens; using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
using JwtRegisteredClaimNames = System.IdentityModel.Tokens.Jwt.JwtRegisteredClaimNames; using JwtRegisteredClaimNames = System.IdentityModel.Tokens.Jwt.JwtRegisteredClaimNames;
using Domain.Exceptions;
namespace Infrastructure.Jwt; namespace Infrastructure.Jwt;
@@ -16,16 +17,19 @@ public class JwtInfrastructure : ITokenInfrastructure
) )
{ {
var handler = new JsonWebTokenHandler(); var handler = new JsonWebTokenHandler();
var key = Encoding.UTF8.GetBytes(secret);
var key = Encoding.UTF8.GetBytes(
secret ?? throw new InvalidOperationException("secret not set")
);
// Base claims (always present)
var claims = new List<Claim> var claims = new List<Claim>
{ {
new(JwtRegisteredClaimNames.Sub, userId.ToString()), new(JwtRegisteredClaimNames.Sub, userId.ToString()),
new(JwtRegisteredClaimNames.UniqueName, username), 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()), new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
}; };
@@ -41,4 +45,36 @@ public class JwtInfrastructure : ITokenInfrastructure
return handler.CreateToken(tokenDescriptor); return handler.CreateToken(tokenDescriptor);
} }
}
public async Task<ClaimsPrincipal> 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");
}
}
}

View File

@@ -10,12 +10,13 @@ public interface IConfirmationService
Task<ConfirmationServiceReturn> ConfirmUserAsync(string confirmationToken); Task<ConfirmationServiceReturn> ConfirmUserAsync(string confirmationToken);
} }
public class ConfirmationService(IAuthRepository authRepository)
public class ConfirmationService(IAuthRepository authRepository) : IConfirmationService : IConfirmationService
{ {
public async Task<ConfirmationServiceReturn> ConfirmUserAsync(
public async Task<ConfirmationServiceReturn> ConfirmUserAsync(string confirmationToken) string confirmationToken
)
{ {
return new ConfirmationServiceReturn(DateTime.Now, Guid.NewGuid()); return new ConfirmationServiceReturn(DateTime.Now, Guid.NewGuid());
} }
} }

View File

@@ -3,11 +3,21 @@ using Infrastructure.Jwt;
namespace Service.Auth; namespace Service.Auth;
public enum TokenType
{
AccessToken,
RefreshToken,
ConfirmationToken,
}
public interface ITokenService public interface ITokenService
{ {
public string GenerateAccessToken(UserAccount user); public string GenerateAccessToken(UserAccount user);
public string GenerateRefreshToken(UserAccount user); public string GenerateRefreshToken(UserAccount user);
public string GenerateConfirmationToken(UserAccount user); public string GenerateConfirmationToken(UserAccount user);
public string GenerateToken<T>(UserAccount user)
where T : struct, Enum;
} }
public static class TokenServiceExpirationHours public static class TokenServiceExpirationHours
@@ -76,4 +86,28 @@ public class TokenService(ITokenInfrastructure tokenInfrastructure)
_confirmationTokenSecret _confirmationTokenSecret
); );
} }
public string GenerateToken<T>(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");
}
}
} }