Files
admin/_docs/02_document/modules/services_auth_service.md
T
Oleksandr Bezdieniezhnykh a77b3f8a59 [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>
2026-05-14 09:22:53 +03:00

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)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() 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.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>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.