Skip to content

Authentication Flows

Detailed sequence diagrams and flow explanations for all authentication processes.


Two-Step Login Flow

The login process is split into two steps for security and multi-team support.

┌──────────────┐                  ┌──────────────┐                  ┌──────────────┐
│   CLIENT     │                  │   BACKEND    │                  │    REDIS     │
└──────┬───────┘                  └──────┬───────┘                  └──────┬───────┘
       │                                 │                                 │
       │  POST /auth/login               │                                 │
       │  {email, password, remember_me} │                                 │
       ├─────────────────────────────────>│                                 │
       │                                 │ 1. Verify password              │
       │                                 │ 2. Check is_verified            │
       │                                 │ 3. Load user's teams            │
       │                                 │ 4. Generate pre_auth_token      │
       │                                 ├─────────────────────────────────>│
       │                                 │ SET preauth:{token} = user_id    │
       │                                 │ EX 300 seconds                  │
       │                                 │                                 │
       │  {pre_auth_token, teams: [...]} │                                 │
       │<─────────────────────────────────┤                                 │
       │                                 │                                 │
       │  === USER SELECTS TEAM ===      │                                 │
       │                                 │                                 │
       │  POST /auth/session-exchange    │                                 │
       │  {pre_auth_token, team_id, ...} │                                 │
       ├─────────────────────────────────>│                                 │
       │                                 │ 5. GET preauth:{token}          │
       │                                 ├─────────────────────────────────>│
       │                                 │                                 │
       │                                 │ 6. Check TeamMember exists       │
       │                                 │ 7. Check Team.status allowed    │
       │                                 │ 8. Load Role + Permissions      │
       │                                 │ 9. Check ActiveSession          │
       │                                 │    (concurrent login?)          │
       │                                 │                                 │
       │                                 │ 10. Create JWT tokens           │
       │                                 │     - access (6h)               │
       │                                 │     - refresh (7d/1d)           │
       │                                 │                                 │
       │                                 │ 11. SET refresh:{jti}           │
       │                                 ├─────────────────────────────────>│
       │                                 │     EX with TTL                 │
       │                                 │                                 │
       │                                 │ 12. Check LoginActivity        │
       │                                 │     (suspicious login?)         │
       │                                 │                                 │
       │  Set-Cookie: mbpanel_access     │                                 │
       │  Set-Cookie: mbpanel_refresh    │                                 │
       │  Set-Cookie: mbpanel_csrf       │                                 │
       │<─────────────────────────────────┤                                 │
       │                                 │                                 │

Key Points

  1. Pre-auth token is short-lived (5 min) and single-use
  2. Team selection happens after credentials are verified
  3. Session exchange validates team membership + status
  4. Concurrent login check enforces single active session
  5. Suspicious login detection can block token issuance

Token Refresh Flow

┌──────────────┐                  ┌──────────────┐                  ┌──────────────┐
│   CLIENT     │                  │   BACKEND    │                  │    REDIS     │
└──────┬───────┘                  └──────┬───────┘                  └──────┬───────┘
       │                                 │                                 │
       │  POST /auth/refresh             │                                 │
       │  (with refresh cookie + CSRF)   │                                 │
       ├─────────────────────────────────>│                                 │
       │                                 │ 1. Verify CSRF token            │
       │                                 │ 2. Decode refresh JWT           │
       │                                 │ 3. GET refresh:{jti}            │
       │                                 ├─────────────────────────────────>│
       │                                 │                                 │
       │                                 │ 4. Check not blacklisted        │
       │                                 │ 5. Check team status            │
       │                                 │ 6. Check ActiveSession          │
       │                                 │    (idle timeout?)              │
       │                                 │                                 │
       │                                 │ 7. Create NEW token pair        │
       │                                 │                                 │
       │                                 │ 8. blacklist OLD refresh_jti    │
       │                                 │    SET blacklist:{old_jti}      │
       │                                 ├─────────────────────────────────>│
       │                                 │                                 │
       │                                 │ 9. SET NEW refresh:{new_jti}    │
       │                                 ├─────────────────────────────────>│
       │                                 │                                 │
       │                                 │ 10. UPDATE ActiveSession        │
       │                                 │     last_seen_at = now           │
       │                                 │                                 │
       │  Set-Cookie: mbpanel_access     │                                 │
       │  Set-Cookie: mbpanel_refresh    │                                 │
       │  Set-Cookie: mbpanel_csrf       │                                 │
       │<─────────────────────────────────┤                                 │
       │                                 │                                 │

Key Points

  1. CSRF required on refresh (double-submit pattern)
  2. Token rotation: Old JTI blacklisted, new JTI issued
  3. Idle timeout: Rejected if now - last_seen_at > threshold
  4. Team revalidation: Status checked on every refresh
  5. Session info updated: IP, geo, device fingerprint tracked

Device Approval Flow

┌──────────────┐                  ┌──────────────┐                  ┌──────────────┐
│   CLIENT     │                  │   BACKEND    │                  │   POSTMARK   │
└──────┬───────┘                  └──────┬───────┘                  └──────┬───────┘
       │                                 │                                 │
       │  POST /device/challenge         │                                 │
       │  {pre_auth_token, fp, ip, ua}   │                                 │
       ├─────────────────────────────────>│                                 │
       │                                 │ 1. Generate 6-digit OTP         │
       │                                 │ 2. Store in Redis                │
       │                                 │    SET device-otp:{user}:{fp}    │
       │                                 │    EX 300 seconds                │
       │                                 │ 3. Send email via Postmark       │
       │                                 ├─────────────────────────────────>│
       │                                 │                                 │
       │  {"message": "OTP sent"}        │                                 │
       │<─────────────────────────────────┤                                 │
       │                                 │                                 │
       │  === USER ENTERS OTP ===        │                                 │
       │                                 │                                 │
       │  POST /device/approve           │                                 │
       │  {pre_auth_token, fp, otp}      │                                 │
       ├─────────────────────────────────>│                                 │
       │                                 │ 4. Verify OTP from Redis        │
       │                                 │ 5. DELETE OTP key               │
       │                                 │ 6. INSERT TrustedDevice         │
       │                                 │                                 │
       │  {"message": "Device approved"} │                                 │
       │<─────────────────────────────────┤                                 │
       │                                 │                                 │

Key Points

  1. OTP stored hashed in Redis (HMAC-SHA256)
  2. 5 minute TTL on OTP code
  3. TrustedDevice persisted for future logins
  4. Rate limiting on OTP generation and verification
  5. Revoke available via /trusted-devices/revoke

Suspicious Login Detection Flow

┌──────────────┐                  ┌──────────────┐                  ┌──────────────┐
│   CLIENT     │                  │   BACKEND    │                  │   POSTMARK   │
└──────┬───────┘                  └──────┬───────┘                  └──────┬───────┘
       │                                 │                                 │
       │  During session-exchange:       │                                 │
       │  check LoginActivity for:       │                                 │
       │  - New IP?                      │                                 │
       │  - New geo location?            │                                 │
       │  - New device fingerprint?      │                                 │
       │                                 │                                 │
       │  === IF SUSPICIOUS ===          │                                 │
       │                                 │                                 │
       │  {"status": "pending",          │                                 │
       │   "token": "alert-token"}       │                                 │
       │<─────────────────────────────────┤                                 │
       │                                 │                                 │
       │                                 │  Send email with approve/deny   │
       │                                 │  links containing alert-token    │
       │                                 ├─────────────────────────────────>│
       │                                 │                                 │
       │  === USER CLICKS EMAIL LINK === │                                 │
       │                                 │                                 │
       │  GET /login/approve?token=...   │                                 │
       ├─────────────────────────────────>│                                 │
       │                                 │ 1. Validate alert token         │
       │                                 │ 2. SET login:ctx:{jti} = allow  │
       │                                 │                                 │
       │  Redirect: Login successful     │                                 │
       │<─────────────────────────────────┤                                 │
       │                                 │                                 │
       │  === NOW /session-exchange WORKS ===                                 │
       │                                 │                                 │

Key Points

  1. State machine: pendingallow | deny
  2. Geo-IP lookup via ip-api.com
  3. Alert token stored in Redis (short TTL)
  4. Pending state blocks token reuse until decision
  5. Deny revokes ActiveSession and blacklists tokens

Logout Flow

┌──────────────┐                  ┌──────────────┐                  ┌──────────────┐
│   CLIENT     │                  │   BACKEND    │                  │    REDIS     │
└──────┬───────┘                  └──────┬───────┘                  └──────┬───────┘
       │                                 │                                 │
       │  POST /auth/logout              │                                 │
       │  (with CSRF token)              │                                 │
       ├─────────────────────────────────>│                                 │
       │                                 │ 1. Verify CSRF                  │
       │                                 │ 2. Get JTI from refresh token   │
       │                                 │                                 │
       │                                 │ 3. blacklist refresh JTI        │
       │                                 │    SET blacklist:{jti} EX 7d    │
       │                                 ├─────────────────────────────────>│
       │                                 │                                 │
       │                                 │ 4. DELETE refresh:{jti}         │
       │                                 ├─────────────────────────────────>│
       │                                 │                                 │
       │                                 │ 5. UPDATE ActiveSession          │
       │                                 │    revoked_at = now              │
       │                                 │    revoked_reason = logout       │
       │                                 │                                 │
       │  Clear all auth cookies         │                                 │
       │<─────────────────────────────────┤                                 │
       │                                 │                                 │

Key Points

  1. CSRF required on logout
  2. Blacklist JTI prevents token reuse
  3. Delete session state from Redis
  4. Mark session revoked in database
  5. Clear all cookies (access, refresh, CSRF)

Email Verification Flow

┌──────────────┐                  ┌──────────────┐                  ┌──────────────┐
│   CLIENT     │                  │   BACKEND    │                  │   POSTMARK   │
└──────┬───────┘                  └──────┬───────┘                  └──────┬───────┘
       │                                 │                                 │
       │  POST /email/verification/request│                                 │
       │  {email}                         │                                 │
       ├─────────────────────────────────>│                                 │
       │                                 │ 1. Generate token               │
       │                                 │ 2. Store in DB                  │
       │                                 │ 3. Send email                   │
       │                                 ├─────────────────────────────────>│
       │                                 │                                 │
       │  {"message": "Email sent"}      │                                 │
       │<─────────────────────────────────┤                                 │
       │                                 │                                 │
       │  === USER CLICKS EMAIL LINK === │                                 │
       │                                 │                                 │
       │  POST /email/verification/confirm│                                 │
       │  {token}                         │                                 │
       ├─────────────────────────────────>│                                 │
       │                                 │ 4. Validate token               │
       │                                 │ 5. UPDATE user.is_verified = true│
       │                                 │ 6. Mark token used              │
       │                                 │                                 │
       │  {"message": "Verified"}        │                                 │
       │<─────────────────────────────────┤                                 │
       │                                 │                                 │

Key Points

  1. 24 hour TTL on verification tokens
  2. Single-use tokens (marked used after consumption)
  3. Required before login (returns 422 if not verified)
  4. Resend allowed via /email/verification/request

Password Reset Flow

┌──────────────┐                  ┌──────────────┐                  ┌──────────────┐
│   CLIENT     │                  │   BACKEND    │                  │   POSTMARK   │
└──────┬───────┘                  └──────┬───────┘                  └──────┬───────┘
       │                                 │                                 │
       │  POST /password-reset/request    │                                 │
       │  {email}                         │                                 │
       ├─────────────────────────────────>│                                 │
       │                                 │ 1. Generate token               │
       │                                 │ 2. Store in DB                  │
       │                                 │ 3. Send email                   │
       │                                 ├─────────────────────────────────>│
       │                                 │                                 │
       │  {"message": "Email sent"}      │                                 │
       │<─────────────────────────────────┤                                 │
       │                                 │                                 │
       │  === USER SUBMITS NEW PASSWORD ===                                 │
       │                                 │                                 │
       │  POST /password-reset/confirm    │                                 │
       │  {token, new_password}           │                                 │
       ├─────────────────────────────────>│                                 │
       │                                 │ 4. Validate token               │
       │                                 │ 5. Hash new password            │
       │                                 │ 6. UPDATE user.hashed_password   │
       │                                 │ 7. Mark token used              │
       │                                 │ 8. Revoke all sessions          │
       │                                 │                                 │
       │  {"message": "Password reset"}  │                                 │
       │<─────────────────────────────────┤                                 │
       │                                 │                                 │

Key Points

  1. 1 hour TTL on reset tokens
  2. Single-use tokens
  3. All sessions revoked after password change
  4. User must re-login after reset
  5. Email enumeration prevented (always returns success)