Move Infrastructure.Jwt into Features.Users (#287)

This commit is contained in:
Aaron Po
2026-09-11 23:14:30 -04:00
committed by GitHub
parent 5fa78fbdf6
commit 7187631ebc
19 changed files with 19 additions and 50 deletions

View File

@@ -6,7 +6,6 @@ using Features.Users.Services;
using Features.Users.Tests.TestSupport;
using FluentAssertions;
using Infrastructure.Configuration;
using Infrastructure.Jwt;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Configuration;
using Moq;

View File

@@ -5,7 +5,6 @@ using Features.Users.Services;
using Features.Users.Tests.TestSupport;
using FluentAssertions;
using Infrastructure.Configuration;
using Infrastructure.Jwt;
using Microsoft.Extensions.Configuration;
using Moq;

View File

@@ -18,6 +18,7 @@ public static class FeaturesUsersServiceCollectionExtensions
public static IServiceCollection AddFeaturesUsers(this IServiceCollection services)
{
services.AddScoped<ITokenService, TokenService>();
services.AddScoped<ITokenInfrastructure, JwtInfrastructure>();
services.AddScoped<IPasswordInfrastructure, Argon2Infrastructure>();
services.AddScoped<IUserListRepository, UserListRepository>();
services.AddScoped<IUserProfileRepository, UserProfileRepository>();

View File

@@ -15,7 +15,6 @@
<ProjectReference Include="..\..\Domain\Domain.Entities\Domain.Entities.csproj" />
<ProjectReference Include="..\..\Domain\Domain.Exceptions\Domain.Exceptions.csproj" />
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Configuration\Infrastructure.Configuration.csproj" />
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Jwt\Infrastructure.Jwt.csproj" />
<ProjectReference Include="..\..\Database\Database.Connection\Database.Connection.csproj" />
<ProjectReference Include="..\Features.PhotoUpload\Features.PhotoUpload.csproj" />
<ProjectReference Include="..\..\Shared\Shared.Contracts\Shared.Contracts.csproj" />
@@ -24,5 +23,7 @@
<ItemGroup>
<PackageReference Include="Konscious.Security.Cryptography.Argon2" Version="1.3.1" />
<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.16.0" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.16.0" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,17 @@
using System.Security.Claims;
namespace Features.Users.Services;
/// <summary>
/// Service for generating and validating JSON Web Tokens (JWTs) used for authentication.
/// </summary>
public interface ITokenInfrastructure
{
/// <summary>
/// Generates a signed JWT for the given user.
/// </summary>
string GenerateJwt(Guid userId, string username, DateTime expiry, string secret);
/// <exception cref="Domain.Exceptions.UnauthorizedException">Thrown when the token is invalid, expired, or fails validation.</exception>
Task<ClaimsPrincipal> ValidateJwtAsync(string token, string secret);
}

View File

@@ -0,0 +1,84 @@
using System.Security.Claims;
using System.Text;
using Domain.Exceptions;
using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Tokens;
using JwtRegisteredClaimNames = System.IdentityModel.Tokens.Jwt.JwtRegisteredClaimNames;
namespace Features.Users.Services;
/// <summary>
/// Generates and validates HMAC-SHA256 signed JWTs using <see cref="JsonWebTokenHandler" />.
/// </summary>
public class JwtInfrastructure : ITokenInfrastructure
{
/// <inheritdoc />
/// <remarks>
/// Sets registered claims <c>sub</c> (userId), <c>unique_name</c> (username), <c>iat</c> (current UTC time),
/// <c>exp</c> (expiry), and <c>jti</c> (a newly generated GUID).
/// </remarks>
/// <param name="secret">The symmetric secret used to sign the token, encoded as UTF-8 bytes.</param>
public string GenerateJwt(Guid userId, string username, DateTime expiry, string secret)
{
JsonWebTokenHandler handler = new();
byte[] key = Encoding.UTF8.GetBytes(secret);
List<Claim> claims = new()
{
new Claim(JwtRegisteredClaimNames.Sub, userId.ToString()),
new Claim(JwtRegisteredClaimNames.UniqueName, username),
new Claim(
JwtRegisteredClaimNames.Iat,
DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString()
),
new Claim(
JwtRegisteredClaimNames.Exp,
new DateTimeOffset(expiry).ToUnixTimeSeconds().ToString()
),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
};
SecurityTokenDescriptor tokenDescriptor = new()
{
Subject = new ClaimsIdentity(claims),
Expires = expiry,
SigningCredentials = new SigningCredentials(
new SymmetricSecurityKey(key),
SecurityAlgorithms.HmacSha256
),
};
return handler.CreateToken(tokenDescriptor);
}
/// <inheritdoc />
/// <remarks>Issuer and audience validation are disabled; only the signature and lifetime are validated.</remarks>
/// <param name="secret">The symmetric secret used to verify the token's signature, encoded as UTF-8 bytes.</param>
/// <exception cref="UnauthorizedException">
/// Thrown when the token is invalid, has no claims identity, is expired, or otherwise fails validation.
/// </exception>
public async Task<ClaimsPrincipal> ValidateJwtAsync(string token, string secret)
{
JsonWebTokenHandler handler = new();
byte[] keyBytes = Encoding.UTF8.GetBytes(secret);
TokenValidationParameters parameters = new()
{
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = true,
IssuerSigningKey = new SymmetricSecurityKey(keyBytes),
};
try
{
TokenValidationResult? result = await handler.ValidateTokenAsync(token, parameters);
if (!result.IsValid || result.ClaimsIdentity == null)
throw new UnauthorizedAccessException();
return new ClaimsPrincipal(result.ClaimsIdentity);
}
catch (Exception)
{
throw new UnauthorizedException("Invalid token");
}
}
}

View File

@@ -3,7 +3,6 @@ using System.Security.Claims;
using Domain.Exceptions;
using Features.Users.Identity;
using Infrastructure.Configuration;
using Infrastructure.Jwt;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Configuration;