Files
the-biergarten-app/web/backend/Infrastructure/Infrastructure.Jwt/JwtInfrastructure.cs
Aaron Po de1e9ac02b Migrate Auth/Brewery/Location repositories to pure Dapper; rename Timer to RowVersion (#237)
* Integrate Spectre.Console into seed application, use async data loading

* Replace stored-procedure repositories with Dapper-based repositories

Removes the USP_* stored procedures for Auth, Location, and Brewery
data access in favour of services built with Dapper.

Adds a new Features.Locations slice and optimistic concurrency support
for the Brewery slice.

* Add new documentation for Dapper migration/database schema

* Update FluentAssertions to most recent open source ver

* Update XML doc comment in backend projects

* Modify brewery location dto to include denormalized data

* - Updated column names from 'Timer' to 'RowVersion' in schema.sql for
- Introduced new BeerPost entity and updated BreweryPost and BreweryPostLocation entities to use 'RowVersion' for concurrency.
- Modified UserAccount, UserCredential, UserVerification, City, StateProvince, and Country entities to use 'RowVersion' instead of 'Timer'.
- Updated tests and command handlers to reflect changes in concurrency token from 'Timer' to 'RowVersion'.
- Adjusted BreweryDto and BreweryDtoMapper to include 'RowVersion' and updated mapping logic accordingly.
- Ensured all repository methods and interfaces are aligned with the new concurrency handling approach.

* updates

* Add BeerStyle entity and related properties; refactor BreweryDto to remove BeerPosts

* Fix Api.Specs test failures: stale Timer column refs and missing test.user fixture

* Remove repository tests, usages of registration dto used in repo

* archive old stored procs/functions

* update stale docs

* Add instructions for generating cryptographically secure base64 secrets on macOS/Linux and Windows
2026-08-24 01:09:47 -04:00

85 lines
3.2 KiB
C#

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 Infrastructure.Jwt;
/// <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");
}
}
}