mirror of
https://github.com/azaion/admin.git
synced 2026-06-21 17:01: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,20 +1,92 @@
|
||||
# Flow: User Login
|
||||
# Flow: User Login (dual token + MFA)
|
||||
|
||||
> **Cycle 2 (2026-05-14)**: rebuilt around the AZ-531 + AZ-532 + AZ-534 + AZ-536 + AZ-537 stack. Single-token, SHA-384, HS256 path is gone. See `_docs/02_document/system-flows.md` F1 for the full narrative; this file is the canonical sequence diagram.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client
|
||||
participant Mid as RateLimiter (per-IP, AZ-537)
|
||||
participant API as Admin API
|
||||
participant US as UserService
|
||||
participant Sec as Security (Argon2id, AZ-536)
|
||||
participant AL as AuditLog
|
||||
participant Mfa as MfaService
|
||||
participant RT as RefreshTokenService
|
||||
participant Auth as AuthService (ES256, AZ-532)
|
||||
participant SS as SessionService
|
||||
participant DB as PostgreSQL
|
||||
participant Auth as AuthService
|
||||
|
||||
Client->>API: POST /login {email, password}
|
||||
Client->>Mid: POST /login {email, password}
|
||||
Mid->>Mid: per-IP sliding-window check
|
||||
alt no permits
|
||||
Mid-->>Client: 429 + Retry-After
|
||||
end
|
||||
Mid->>API: forward
|
||||
API->>US: ValidateUser(request)
|
||||
US->>DB: SELECT user WHERE email = ?
|
||||
DB-->>US: User record
|
||||
US->>US: Compare password hash (SHA-384)
|
||||
US-->>API: User entity
|
||||
API->>Auth: CreateToken(user)
|
||||
Auth-->>API: JWT string (HMAC-SHA256)
|
||||
API-->>Client: 200 OK {token}
|
||||
US->>DB: SELECT users WHERE email = ?
|
||||
US->>AL: CountRecentFailedLogins(email, window)
|
||||
alt account locked OR per-account threshold exceeded
|
||||
US-->>API: AccountLocked / LoginRateLimited (RetryAfterSeconds)
|
||||
API-->>Client: 423 / 429 + Retry-After
|
||||
end
|
||||
US->>Sec: VerifyPassword(presented, stored)
|
||||
alt VerifyResult.Ok=false
|
||||
US->>AL: RecordLoginFailed
|
||||
US->>DB: UPDATE failed_login_count++; lockout_until = now + LockoutSeconds (if newly over)
|
||||
US-->>API: WrongPassword (or NoEmailFound)
|
||||
API-->>Client: 409
|
||||
end
|
||||
alt VerifyResult.NeedsRehash=true (legacy SHA-384)
|
||||
US->>Sec: HashPassword (Argon2id)
|
||||
US->>DB: UPDATE password_hash (lazy migrate)
|
||||
end
|
||||
US->>AL: RecordLoginSuccess
|
||||
US->>DB: UPDATE failed_login_count = 0, lockout_until = NULL, last_login = now
|
||||
US-->>API: User
|
||||
|
||||
alt user.MfaEnabled
|
||||
API->>Mfa: IssueMfaStepToken(userId)
|
||||
Mfa-->>API: ES256 JWT (mfa_pending=true, audience=mfa-step, ~5 min)
|
||||
API-->>Client: 200 OK MfaRequiredResponse {mfa_required, mfa_token, expires_in: 300}
|
||||
|
||||
Note over Client,API: --- second factor ---
|
||||
Client->>Mid: POST /login/mfa {mfa_token, code}
|
||||
Mid->>Mid: per-IP sliding-window check
|
||||
Mid->>API: forward
|
||||
API->>Mfa: ValidateMfaStepToken(mfa_token) -> userId
|
||||
API->>US: GetById(userId) -> User
|
||||
API->>Mfa: VerifyForLogin(userId, code)
|
||||
Mfa->>DB: TOTP verify decrypted mfa_secret OR consume recovery code
|
||||
Mfa->>AL: RecordMfaLoginSuccess (or MfaRecoveryUsed)
|
||||
Mfa-->>API: amr = ["pwd","mfa"] (+ "recovery" if used)
|
||||
API->>RT: IssueForNewLogin(userId, mfaAuthenticated=true)
|
||||
RT->>DB: INSERT INTO sessions (id, family_id=id, refresh_hash=SHA256(opaque), expires_at, mfa_authenticated=true)
|
||||
RT-->>API: (opaqueRefreshToken, Session)
|
||||
API->>Auth: CreateToken(user, sid=Session.Id, jti, amr=["pwd","mfa"])
|
||||
Auth-->>API: AccessToken (ES256)
|
||||
opt user.Role == CompanionPC
|
||||
API->>SS: RevokeMissionsForAircraft(user.Id)
|
||||
end
|
||||
API-->>Client: 200 OK LoginResponse {AccessToken, AccessExp, RefreshToken, RefreshExp}
|
||||
else
|
||||
API->>RT: IssueForNewLogin(userId, mfaAuthenticated=false)
|
||||
RT->>DB: INSERT INTO sessions (..., mfa_authenticated=false)
|
||||
RT-->>API: (opaqueRefreshToken, Session)
|
||||
API->>Auth: CreateToken(user, sid=Session.Id, jti, amr=["pwd"])
|
||||
Auth-->>API: AccessToken (ES256)
|
||||
opt user.Role == CompanionPC
|
||||
API->>SS: RevokeMissionsForAircraft(user.Id)
|
||||
end
|
||||
API-->>Client: 200 OK LoginResponse {AccessToken, AccessExp, RefreshToken, RefreshExp}
|
||||
end
|
||||
```
|
||||
|
||||
## Related diagrams (cycle 2)
|
||||
|
||||
- `flow_refresh_token.md` *(see system-flows.md F11)*
|
||||
- `flow_logout_revocation.md` *(see system-flows.md F12)*
|
||||
- `flow_mission_token.md` *(see system-flows.md F13)*
|
||||
- `flow_mfa_lifecycle.md` *(see system-flows.md F14)*
|
||||
- `flow_revocation_snapshot.md` *(see system-flows.md F15)*
|
||||
|
||||
These are documented inline in `system-flows.md` rather than as standalone files; this `flow_login.md` is kept as a separate file because it is referenced from multiple ADRs and the security report.
|
||||
|
||||
Reference in New Issue
Block a user