mirror of
https://github.com/azaion/admin.git
synced 2026-06-21 21:11:08 +00:00
a77b3f8a59
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>
4.9 KiB
4.9 KiB
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) —
CreateTokenwas completely reshaped:
- Signing switched from HMAC-HS256 (
JwtConfig.Secret) to ES256 viaIJwtSigningKeyProvider(AZ-532).- Lifetime is now
JwtConfig.AccessTokenLifetimeMinutes(default 15) instead of the oldTokenLifetimeHours(default 4).- Tokens stamp two new claims required by the refresh / logout flow:
sid(session id) andjti(per-token unique id).- Tokens stamp the RFC 8176
amrclaim (multi-valued; defaults to["pwd"], becomes["pwd","mfa"]after/login/mfa, with"recovery"appended when a recovery code was used).- Returns an
AccessTokenrecord (Jwt+ExpiresAt) so callers can populateLoginResponse.AccessExpdirectly.
Public Interface
IAuthService
| Method | Signature | Description |
|---|---|---|
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
- CreateToken builds claims:
ClaimTypes.NameIdentifier=user.IdClaimTypes.Name=user.EmailClaimTypes.Role=user.Role.ToString()JwtRegisteredClaimNames.Sid=sessionId.ToString()JwtRegisteredClaimNames.Jti=jti.ToString()- One
amrclaim per element of theamrparameter (defaults to["pwd"]).
- Signs with
SigningCredentials(active.SecurityKey, SecurityAlgorithms.EcdsaSha256)using the active key fromIJwtSigningKeyProvider. ThekidJWT header is auto-stamped becauseECDsaSecurityKey.KeyIdis set per loaded key. - Lifetime:
now + JwtConfig.AccessTokenLifetimeMinutes. - GetCurrentUser: reads
ClaimTypes.NamefromHttpContext.User.Claimsand delegates toIUserService.GetByEmail(which is cached).
Dependencies
IHttpContextAccessor— for accessing current HTTP contextIOptions<JwtConfig>—Issuer,Audience,AccessTokenLifetimeMinutesIJwtSigningKeyProvider(cycle 2 — ES256 active key)IUserService— forGetByEmaillookupSystem.IdentityModel.Tokens.JwtMicrosoft.IdentityModel.Tokens
Consumers
Program.cs/login(afterUserService.ValidateUser) → callsCreateTokenvia the sharedIssueDualTokenshelper.Program.cs/login/mfa→ callsCreateTokenwithamrfromMfaService.VerifyForLogin.Program.cs/token/refresh→ callsCreateTokenwithamrreconstructed from the session'sMfaAuthenticatedflag.Program.cs/users/current→ callsGetCurrentUser.MfaService.IssueMfaStepTokenandMissionTokenService.MintTokenmint their own tokens directly (separate audiences); they bypassAuthService.CreateTokenon purpose.
Data Models
None.
Configuration
JwtConfig:
Issuer,Audience— claim valuesAccessTokenLifetimeMinutes(default 15) — access TTLKeysFolder,ActiveKid— signing key selection (consumed viaIJwtSigningKeyProvider)
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 inProgram.csJwtBearer config to defeat the alg-confusion attack (forging a token withalg=HS256using the public key as the HMAC secret).- Every token now carries
sidandjti.sidis the AZ-535 logout / family-revocation key;jtireserves 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) — verifiesAccessExp ≈ now + 15mand that rotation produces a fresh access token.e2e/Azaion.E2E/Tests/JwksTests.cs(AC-1) — verifiesalg=ES256andkidheader on issued tokens.e2e/Azaion.E2E/Tests/MfaLoginTests.cs(AC-3) — verifies theamrclaim ordering across the two-step login.e2e/Azaion.E2E/Tests/LogoutTests.cs— exercises thesidclaim path.