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¶
- Pre-auth token is short-lived (5 min) and single-use
- Team selection happens after credentials are verified
- Session exchange validates team membership + status
- Concurrent login check enforces single active session
- 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¶
- CSRF required on refresh (double-submit pattern)
- Token rotation: Old JTI blacklisted, new JTI issued
- Idle timeout: Rejected if
now - last_seen_at > threshold - Team revalidation: Status checked on every refresh
- 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¶
- OTP stored hashed in Redis (HMAC-SHA256)
- 5 minute TTL on OTP code
- TrustedDevice persisted for future logins
- Rate limiting on OTP generation and verification
- 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¶
- State machine:
pending→allow|deny - Geo-IP lookup via ip-api.com
- Alert token stored in Redis (short TTL)
- Pending state blocks token reuse until decision
- 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¶
- CSRF required on logout
- Blacklist JTI prevents token reuse
- Delete session state from Redis
- Mark session revoked in database
- 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¶
- 24 hour TTL on verification tokens
- Single-use tokens (marked used after consumption)
- Required before login (returns 422 if not verified)
- 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 hour TTL on reset tokens
- Single-use tokens
- All sessions revoked after password change
- User must re-login after reset
- Email enumeration prevented (always returns success)
Related Documentation¶
- API Reference - Endpoint details
- Architecture Overview - System design
- Security Architecture - Security details