* Remove restating comments in backend * Remove restating comments in frontend stories * Remove restating comments in pipeline * Remove restating comments in pipeline scripts * Restore location-clear comment in BreweryRepository * Remove stale/restating comments found in second pass * Restore mock curated-data branch comment in main.cc * Remove more restating/filler comments in backend * Document account-management endpoints, compose files, and env generator * Squashed commit of the following: commit a8a5b8abd9969859c695092fae6990d03b2135ed Author: Aaron Po <aaron.po97@gmail.com> Date: Sun Aug 23 23:53:56 2026 -0400 Add reverse geocoding for brewery street addresses (#245) * Add geocoding to seed application, remove postal codes from generator Update comments * Update pipeline class diagram for OpenAI generator * Update pipeline activity diagram for brewery coordinates and OpenAI backend * Add mailpit, production, db-only, and minimal environments to deployment diagram * Fix authentication flow diagram for Identity migration * Add account-management flows to authentication diagram * Fix database schema diagram note referencing removed AuthRepository
8.9 KiB
Token validation architecture
Overview
The Core project validates JSON Web Tokens (JWTs) across three token types:
- Access Tokens: Short-lived (1 hour) tokens for API authentication
- Refresh Tokens: Long-lived (21 days) tokens for obtaining new access tokens
- Confirmation Tokens: Short-lived (30 minutes) tokens for email confirmation
Components
Infrastructure layer
ITokenInfrastructure
Low-level JWT operations.
Methods:
GenerateJwt()- Creates signed JWT tokensValidateJwtAsync()- Validates token signature, expiration, and format
Implementation: JwtInfrastructure.cs
- Uses Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler
- Algorithm: HS256 (HMAC-SHA256)
- Validates token lifetime, signature, and well-formedness
Features.Auth slice
ITokenService
Both token generation and validation live on the same slice-internal service (there is no separate validation service/class).
Generation methods:
GenerateAccessToken(UserAccount)- Creates 1-hour access tokenGenerateRefreshToken(UserAccount)- Creates 21-day refresh tokenGenerateConfirmationToken(UserAccount)- Creates 30-minute confirmation token
Validation methods:
ValidateAccessTokenAsync(string token)- Validates access tokensValidateRefreshTokenAsync(string token)- Validates refresh tokensValidateConfirmationTokenAsync(string token)- Validates confirmation tokens
Returns (validation): ValidatedToken record containing:
UserId(Guid)Username(string)Principal(ClaimsPrincipal) - Full JWT claims
Implementation: TokenService.cs
- Reads token secrets from environment variables
- Extracts and validates claims (Sub, UniqueName)
- Throws
UnauthorizedExceptionon validation failure
TokenService is registered by Features.Auth's own AddFeaturesAuth()
extension method, except for the lower-level ITokenInfrastructure (JWT
signing/verification) it depends on, which is registered by the host
(API.Core/Program.cs) since JwtAuthenticationHandler (host-level auth
middleware) also depends on it directly.
Integration points
ConfirmUserHandler
Flow:
- Receives confirmation token from user via
ConfirmUserCommand - Calls
ITokenService.ValidateConfirmationTokenAsync() - Extracts user ID from validated token
- Calls
IAuthRepository.ConfirmUserAccountAsync()to update database - Returns confirmation result
ResendConfirmationEmailHandler
Flow:
- Receives a user ID via
ResendConfirmationEmailCommand - Looks up the user and checks they aren't already verified
- Generates a fresh confirmation token via
ITokenService - Sends
SendResendConfirmationEmailCommandover MediatR, handled by theFeatures.Emailsslice, with no direct project reference between the two slices
RefreshTokenHandler
Flow:
- Receives a refresh token via
RefreshTokenCommand - Delegates to
ITokenService.RefreshTokenAsync(), which validates the token, retrieves the user account viaIAuthRepository.GetUserByIdAsync(), and issues a new access/refresh token pair - Maps the result onto
LoginPayload
AuthController
Endpoints:
POST /api/auth/register- Register new userPOST /api/auth/login- Authenticate userPOST /api/auth/confirm?token=...- Confirm emailPOST /api/auth/confirm/resend?userId=...- Resend the confirmation emailPOST /api/auth/refresh- Refresh access token
Account management (require a valid access token; user ID comes from the token, never the request body):
PATCH /api/auth/username- Change the authenticated user's usernamePATCH /api/auth/email- Change the authenticated user's email address; resetsEmailConfirmedto false, so the user must re-confirm viaPOST /api/auth/confirm/resendPATCH /api/auth/password- Change the authenticated user's password; requires the current passwordPATCH /api/auth/profile- Update the authenticated user's profile (first name, last name, date of birth)DELETE /api/auth/account- Permanently delete the authenticated user's account; blocked while the account still has associated posts, comments, photos, or follows
Validation security
Token secrets
Three independent secrets enable:
- Key rotation - Rotate each secret type independently
- Isolation - Compromise of one secret doesn't affect others
- Different expiration - Different token types can expire at different rates
Environment Variables:
ACCESS_TOKEN_SECRET=... # Signs 1-hour access tokens
REFRESH_TOKEN_SECRET=... # Signs 21-day refresh tokens
CONFIRMATION_TOKEN_SECRET=... # Signs 30-minute confirmation tokens
Validation checks
Each token is validated for:
- Signature Verification - Token must be signed with correct secret
- Expiration - Token must not be expired (checked against current time)
- Claims Presence - Required claims (Sub, UniqueName) must be present
- Claims Format - UserId claim must be a valid GUID
Error handling
Validation failures return HTTP 401 Unauthorized:
- Invalid signature → "Invalid token"
- Expired token → "Invalid token" (message doesn't reveal reason for security)
- Missing claims → "Invalid token"
- Malformed claims → "Invalid token"
Token lifecycle
Access token lifecycle
- Generation: During login (1-hour validity)
- Usage: Included in Authorization header on API requests
- Validation: Validated on protected endpoints
- Expiration: Token becomes invalid after 1 hour
- Refresh: Use refresh token to obtain new access token
Refresh token lifecycle
- Generation: During login (21-day validity)
- Storage: Client-side (secure storage)
- Usage: Posted to
/api/auth/refreshendpoint - Validation: Validated by
ITokenService.RefreshTokenAsync() - Rotation: New refresh token issued on successful refresh
- Expiration: Token becomes invalid after 21 days
Confirmation token lifecycle
- Generation: During user registration (30-minute validity)
- Delivery: Emailed to user in confirmation link
- Usage: User clicks link, token posted to
/api/auth/confirm - Validation: Validated by
ConfirmUserHandler - Completion: User account marked as confirmed
- Expiration: Token becomes invalid after 30 minutes
Testing
All of the following live in Features.Auth.Tests.
Unit tests
Services/TokenServiceValidationTests.cs
- Happy path: Valid token extraction
- Error cases: Invalid, expired, malformed tokens
- Missing/invalid claims scenarios
Services/TokenServiceRefreshTests.cs
- Successful refresh with valid token
- Invalid/expired refresh token rejection
- Non-existent user handling
Commands/RefreshTokenHandlerTests.cs
- Verifies the handler maps
ITokenService.RefreshTokenAsync()'s result ontoLoginPayload
Commands/ConfirmUserHandlerTests.cs
- Successful confirmation with valid token
- Token validation failures
- User not found scenarios
Commands/ResendConfirmationEmailHandlerTests.cs
- Sends a fresh confirmation email when the user exists and is unverified
- No-ops when the user doesn't exist or is already verified
BDD tests (Reqnroll)
TokenRefresh.feature
- Successful token refresh
- Invalid/expired token rejection
- Missing token validation
Confirmation.feature
- Successful email confirmation
- Expired/tampered token rejection
- Missing token validation
AccessTokenValidation.feature
- Protected endpoint access token validation
- Invalid/expired access token rejection
- Token type mismatch (refresh used as access token)
Future enhancements
Stretch goals
-
Middleware for Access Token Validation
- Automatically validate access tokens on protected routes
- Populate HttpContext.User from token claims
- Return 401 for invalid/missing tokens
-
Token Denylisting
- Implement token revocation (for example, on logout)
- Store denylisted tokens in cache/database
- Check the denylist during validation
-
Refresh Token Rotation Strategy
- Detect token reuse (replay attacks)
- Automatically invalidate entire token chain on reuse
- Log suspicious activity
-
Structured Logging
- Log token validation attempts
- Track failed validation reasons
- Alert on repeated validation failures (brute force detection)