mirror of
https://github.com/azaion/admin.git
synced 2026-06-21 12:51:09 +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.1 KiB
4.1 KiB
Module: Azaion.Services.RefreshTokenService
Purpose
Issues, rotates, and validates opaque refresh tokens for interactive sessions. Implements OAuth 2.1 §6.1 reuse-detection: presenting an already-rotated refresh token kills the entire session family.
Added in cycle 2 (2026-05-14) by AZ-531 (Epic AZ-529, Auth Mechanism Modernization). Foundation for AZ-535 (logout/revocation) and AZ-534 (MFA — pins
mfa_authenticatedto the session so refresh rotation inherits the original AMR strength).
Public Interface
IRefreshTokenService
| Method | Signature | Description |
|---|---|---|
IssueForNewLogin |
Task<(string OpaqueToken, Session Session)> IssueForNewLogin(Guid userId, bool mfaAuthenticated = false, CancellationToken ct = default) |
Mint a fresh refresh token at login; starts a new session family. The opaque token is returned to the caller; only its SHA-256 hash is persisted. mfaAuthenticated is pinned to the session row so rotation preserves AMR strength. |
Rotate |
Task<(string OpaqueToken, Session Session)> Rotate(string opaqueToken, CancellationToken ct = default) |
Rotate the supplied refresh token. On success returns a new opaque token + the new session row. Throws BusinessException(InvalidRefreshToken) on bad/expired/revoked input; on reuse-detection (already-rotated token presented again) the entire session family is revoked first. |
Internal Logic
- Token format: 32 random bytes (256 bits) base64url-encoded → 43-char string (no padding). Persisted as the SHA-256 hex digest in
sessions.refresh_hash. The opaque value is never logged. - Family semantics: each
IssueForNewLogincreates a new family (family_id == id). EachRotateinserts a new row in the same family withparent_session_idchained to the previous row, then marks the previous rowrevoked_reason='rotated'. - Reuse detection: if a presented token is found with
revoked_reason='rotated', every active row in the same family is set torevoked_reason='reuse_detected'(per OAuth 2.1 §6.1) — even the row that succeeded last cycle stops working. - Sliding expiry: each rotation moves
expires_attonow + RefreshSlidingHours(default 8 h). - Absolute cap: a family older than
RefreshAbsoluteHours(default 12 h) sincefamily_started_atis rejected even if every individual rotation stayed within the sliding window. - Concurrency: rotation runs in a
Serializabletransaction so two concurrent refreshes of the same token can't both succeed.
Dependencies
IDbFactory— admin connection for inserts/updatesIOptions<SessionConfig>— sliding/absolute window TTLs (defined alongsideJwtConfiginAzaion.Common/Configs/JwtConfig.cs)Sessionentity,SessionRevokedReasonsconstantsBusinessException/ExceptionEnum.InvalidRefreshTokenSystem.Security.Cryptography.RandomNumberGenerator+SHA256
Consumers
Program.cs/login→ callsIssueForNewLoginafterUserService.ValidateUsersucceedsProgram.cs/login/mfa→ callsIssueForNewLoginafter MFA second factorProgram.cs/token/refresh→ callsRotate
Data Models
Operates on the Session entity via AzaionDb.Sessions table.
Configuration
SessionConfig (bound from appsettings.json section SessionConfig):
RefreshSlidingHours(default 8)RefreshAbsoluteHours(default 12)
External Integrations
PostgreSQL via IDbFactory.
Security
- Refresh tokens are opaque random strings, never JWTs — verifiers cannot decode or alter them.
- The plaintext token leaves the server only at issue/rotation; the DB stores only the SHA-256 hash.
- Reuse-detection is the primary defence against stolen-refresh-token attacks: the legitimate user's next refresh will be rejected and they'll be forced to re-authenticate, but the attacker's token also dies.
- Rotation is transactional (
Serializable) so concurrent refresh races cannot leak two valid descendants.
Tests
e2e/Azaion.E2E/Tests/RefreshTokenTests.cs— covers AC-1 (login dual tokens), AC-2 (rotation invalidates old), AC-3 (reuse kills family), AC-4 (sliding + absolute expiry), AC-5 (opaque, not JWT).