8.2 KiB
Token Validation Architecture
Overview
The Core project validates 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
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 RefreshTokenService
- 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 Blacklisting
- Implement token revocation (e.g., on logout)
- Store blacklisted tokens in cache/database
- Check blacklist 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)