Files
admin/_docs/02_document/modules/services_refresh_token_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.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_authenticated to 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 IssueForNewLogin creates a new family (family_id == id). Each Rotate inserts a new row in the same family with parent_session_id chained to the previous row, then marks the previous row revoked_reason='rotated'.
  • Reuse detection: if a presented token is found with revoked_reason='rotated', every active row in the same family is set to revoked_reason='reuse_detected' (per OAuth 2.1 §6.1) — even the row that succeeded last cycle stops working.
  • Sliding expiry: each rotation moves expires_at to now + RefreshSlidingHours (default 8 h).
  • Absolute cap: a family older than RefreshAbsoluteHours (default 12 h) since family_started_at is rejected even if every individual rotation stayed within the sliding window.
  • Concurrency: rotation runs in a Serializable transaction so two concurrent refreshes of the same token can't both succeed.

Dependencies

  • IDbFactory — admin connection for inserts/updates
  • IOptions<SessionConfig> — sliding/absolute window TTLs (defined alongside JwtConfig in Azaion.Common/Configs/JwtConfig.cs)
  • Session entity, SessionRevokedReasons constants
  • BusinessException / ExceptionEnum.InvalidRefreshToken
  • System.Security.Cryptography.RandomNumberGenerator + SHA256

Consumers

  • Program.cs /login → calls IssueForNewLogin after UserService.ValidateUser succeeds
  • Program.cs /login/mfa → calls IssueForNewLogin after MFA second factor
  • Program.cs /token/refresh → calls Rotate

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