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