# Module: Azaion.Services.AuthService ## Purpose Mints short-lived (15 min) ES256 access tokens and resolves the current user from HTTP context claims. > **Cycle 2 (2026-05-14) note (AZ-531 / AZ-532 / AZ-534)** — `CreateToken` was completely reshaped: > - Signing switched from HMAC-HS256 (`JwtConfig.Secret`) to ES256 via `IJwtSigningKeyProvider` (AZ-532). > - Lifetime is now `JwtConfig.AccessTokenLifetimeMinutes` (default 15) instead of the old `TokenLifetimeHours` (default 4). > - Tokens stamp two new claims required by the refresh / logout flow: `sid` (session id) and `jti` (per-token unique id). > - Tokens stamp the RFC 8176 `amr` claim (multi-valued; defaults to `["pwd"]`, becomes `["pwd","mfa"]` after `/login/mfa`, with `"recovery"` appended when a recovery code was used). > - Returns an `AccessToken` record (`Jwt` + `ExpiresAt`) so callers can populate `LoginResponse.AccessExp` directly. ## Public Interface ### IAuthService | Method | Signature | Description | |--------|-----------|-------------| | `GetCurrentUser` | `Task GetCurrentUser()` | Reads `ClaimTypes.Name` from `HttpContext.User`, delegates to `IUserService.GetByEmail`. | | `CreateToken` | `AccessToken CreateToken(User user, Guid sessionId, Guid jti, IEnumerable? amr = null)` | Mints a 15-min ES256 access token bound to `sessionId`/`jti`, with the supplied `amr` values. | ### `record AccessToken(string Jwt, DateTime ExpiresAt)` The token string + its absolute expiry (UTC). `Program.cs` packs this into `LoginResponse.AccessToken` / `LoginResponse.AccessExp`. ## Internal Logic - **CreateToken** builds claims: - `ClaimTypes.NameIdentifier` = `user.Id` - `ClaimTypes.Name` = `user.Email` - `ClaimTypes.Role` = `user.Role.ToString()` - `JwtRegisteredClaimNames.Sid` = `sessionId.ToString()` - `JwtRegisteredClaimNames.Jti` = `jti.ToString()` - One `amr` claim per element of the `amr` parameter (defaults to `["pwd"]`). - Signs with `SigningCredentials(active.SecurityKey, SecurityAlgorithms.EcdsaSha256)` using the active key from `IJwtSigningKeyProvider`. The `kid` JWT header is auto-stamped because `ECDsaSecurityKey.KeyId` is set per loaded key. - Lifetime: `now + JwtConfig.AccessTokenLifetimeMinutes`. - **GetCurrentUser**: reads `ClaimTypes.Name` from `HttpContext.User.Claims` and delegates to `IUserService.GetByEmail` (which is cached). ## Dependencies - `IHttpContextAccessor` — for accessing current HTTP context - `IOptions` — `Issuer`, `Audience`, `AccessTokenLifetimeMinutes` - `IJwtSigningKeyProvider` (cycle 2 — ES256 active key) - `IUserService` — for `GetByEmail` lookup - `System.IdentityModel.Tokens.Jwt` - `Microsoft.IdentityModel.Tokens` ## Consumers - `Program.cs` `/login` (after `UserService.ValidateUser`) → calls `CreateToken` via the shared `IssueDualTokens` helper. - `Program.cs` `/login/mfa` → calls `CreateToken` with `amr` from `MfaService.VerifyForLogin`. - `Program.cs` `/token/refresh` → calls `CreateToken` with `amr` reconstructed from the session's `MfaAuthenticated` flag. - `Program.cs` `/users/current` → calls `GetCurrentUser`. - `MfaService.IssueMfaStepToken` and `MissionTokenService.MintToken` mint their own tokens directly (separate audiences); they bypass `AuthService.CreateToken` on purpose. ## Data Models None. ## Configuration `JwtConfig`: - `Issuer`, `Audience` — claim values - `AccessTokenLifetimeMinutes` (default 15) — access TTL - `KeysFolder`, `ActiveKid` — signing key selection (consumed via `IJwtSigningKeyProvider`) The legacy `JwtConfig.Secret` field is **no longer read** — the codebase keeps the property only as a temporary rollback escape hatch and to avoid breaking any environment that still binds it. ## External Integrations None directly. Signing key material lives on disk in `JwtConfig.KeysFolder` (default `secrets/jwt-keys/`). ## Security - Asymmetric ES256 signing — verifiers hold only the public key set (served at `/.well-known/jwks.json`). A compromised verifier can no longer mint admin tokens. - `ValidAlgorithms = [SecurityAlgorithms.EcdsaSha256]` is pinned in `Program.cs` JwtBearer config to defeat the alg-confusion attack (forging a token with `alg=HS256` using the public key as the HMAC secret). - Every token now carries `sid` and `jti`. `sid` is the AZ-535 logout / family-revocation key; `jti` reserves the option of a per-access denylist if revocation latency ever needs to drop below the verifier-poll interval. - The 15-min access TTL plus refresh-token rotation (AZ-531) constrains the leak-window of a stolen access token to <15 min. ## Tests - `e2e/Azaion.E2E/Tests/RefreshTokenTests.cs` (AC-1, AC-2) — verifies `AccessExp ≈ now + 15m` and that rotation produces a fresh access token. - `e2e/Azaion.E2E/Tests/JwksTests.cs` (AC-1) — verifies `alg=ES256` and `kid` header on issued tokens. - `e2e/Azaion.E2E/Tests/MfaLoginTests.cs` (AC-3) — verifies the `amr` claim ordering across the two-step login. - `e2e/Azaion.E2E/Tests/LogoutTests.cs` — exercises the `sid` claim path.