mirror of
https://github.com/azaion/admin.git
synced 2026-06-21 15:31:09 +00:00
[AZ-529] [AZ-530] Cycle-2 documentation refresh
Refreshes _docs/02_document/ to reflect the cycle-2 auth-modernization
+ CMMC hardening landings (AZ-531..AZ-538). Authoritative source for
the ripple set is ripple_log_cycle2.md.
Covered:
- architecture.md (section 1 rewritten, ADRs 6-9 added)
- data_model.md (sessions, audit_events, user columns, migrations)
- system-flows.md (F1 rewritten; F11-F17 added; F2/F7/F9 minor)
- module-layout.md (cycle-2 sub-component table)
- diagrams/flows/flow_login.md (dual-token + MFA)
- components/{01_data_layer,03_auth_and_security,05_admin_api}
- modules/ (12 new, 8 modified — full Argon2id/ES256/MFA/refresh
/mission/session/audit/jwks rollup)
- tests/{blackbox,security,traceability-matrix}
Step 13 (Update Docs) output for cycle 2.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,48 +1,81 @@
|
||||
# Module: Azaion.Services.AuthService
|
||||
|
||||
## Purpose
|
||||
JWT token creation and current-user resolution from HTTP context claims.
|
||||
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<User?> GetCurrentUser()` | Extracts email from JWT claims, returns full User entity |
|
||||
| `CreateToken` | `string CreateToken(User user)` | Generates a signed JWT token for the given user |
|
||||
| `GetCurrentUser` | `Task<User?> GetCurrentUser()` | Reads `ClaimTypes.Name` from `HttpContext.User`, delegates to `IUserService.GetByEmail`. |
|
||||
| `CreateToken` | `AccessToken CreateToken(User user, Guid sessionId, Guid jti, IEnumerable<string>? 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
|
||||
- **GetCurrentUser**: reads `ClaimTypes.Name` from `HttpContext.User.Claims`, then delegates to `IUserService.GetByEmail`.
|
||||
- **CreateToken**: builds a `SecurityTokenDescriptor` with claims (NameIdentifier = user ID, Name = email, Role = role), signs with HMAC-SHA256 using the configured secret, sets expiry from `JwtConfig.TokenLifetimeHours`.
|
||||
|
||||
Private method:
|
||||
- `GetCurrentUserEmail` — extracts email from claims dictionary.
|
||||
- **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<JwtConfig>` — JWT configuration
|
||||
- `IOptions<JwtConfig>` — `Issuer`, `Audience`, `AccessTokenLifetimeMinutes`
|
||||
- `IJwtSigningKeyProvider` (cycle 2 — ES256 active key)
|
||||
- `IUserService` — for `GetByEmail` lookup
|
||||
- `System.IdentityModel.Tokens.Jwt`
|
||||
- `Microsoft.IdentityModel.Tokens`
|
||||
|
||||
## Consumers
|
||||
- `Program.cs` `/login` endpoint — calls `CreateToken` after successful validation
|
||||
- `Program.cs` `/users/current` — calls `GetCurrentUser` (the previously listed `/resources/get`, `/resources/get-installer`, `/resources/check` consumers were removed in cycle 2 / by AZ-197 along with their endpoints)
|
||||
|
||||
- `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
|
||||
Uses `JwtConfig` (Issuer, Audience, Secret, TokenLifetimeHours).
|
||||
|
||||
`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.
|
||||
None directly. Signing key material lives on disk in `JwtConfig.KeysFolder` (default `secrets/jwt-keys/`).
|
||||
|
||||
## Security
|
||||
- Token includes user ID, email, and role as claims
|
||||
- Signed with HMAC-SHA256
|
||||
- Expiry controlled by `TokenLifetimeHours` config
|
||||
- Token validation parameters are configured in `Program.cs` (ValidateIssuer, ValidateAudience, ValidateLifetime, ValidateIssuerSigningKey)
|
||||
|
||||
- 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
|
||||
None.
|
||||
- `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.
|
||||
|
||||
Reference in New Issue
Block a user