Multi-Factor Authentication (MFA)¶
Status: ✅ Shipped — TOTP-based MFA (
pyotp+ QR provisioning) with backup codes is live for admin users: setup/verify/check/disable/status/backup-code endpoints, anMFAAttemptaudit trail, and per-userMFASecretstorage. There is no dedicated MFA doc page prior to this one — coverage was previously scattered acrossDECISIONS.md,FINDINGS.md, a fewDATA_MODELSpages, andadmin-ui.md.
MFA adds a second factor (TOTP code from an authenticator app, or a one-time backup code) on top of normal password auth. Endpoints are scoped by the same RBAC chain as the rest of admin — see ./rbac-admin-scopes.md — not a separate MFA-specific permission.
Schema¶
MFASecret(one row per user, unique FK toUser.id):secret(TOTP shared secret),is_verified,backup_codes(list, hashed/stored),created_at,verified_at,last_used.MFAAttempt:user_id,code_entered,success,ip_address,user_agent,attempt_time,failure_reason— every check attempt is logged, success or failure.
Source: services/gameserver/src/models/mfa.py.
Endpoints¶
All under /auth/mfa (mounted via api/api.py). Tip mfa.py gates every listed route with get_current_user (authenticated bearer) — not require_scope(PLAYERS_VIEW). Route contract also lives in ../ARCHITECTURE/auth.md § Multi-factor authentication (TOTP).
| Route | Method | Purpose |
|---|---|---|
/auth/mfa/generate |
POST | Generate a new TOTP secret + QR provisioning URL for the current user. Does not enable MFA yet. |
/auth/mfa/verify |
POST | Confirm setup with a TOTP code; on success, MFA becomes enabled and backup codes are returned once. |
/auth/mfa/check |
POST | Validate a TOTP or backup code (login-flow use — only get_current_user, not scope-gated). Logs an MFAAttempt every call. |
/auth/mfa/status |
GET | Enabled/verified flags, remaining backup-code count, last-used timestamp. |
/auth/mfa/disable |
POST | Disable MFA for the current user. |
/auth/mfa/backup-codes |
GET | List remaining backup codes (404 if MFA not enabled). |
/auth/mfa/regenerate-backup-codes |
POST | Invalidate all existing backup codes and issue a fresh set (404 if MFA not enabled). |
/auth/mfa/attempts |
GET | Recent MFAAttempt rows for the current user (hours query param, default 24) — security-monitoring surface. |
Source: services/gameserver/src/api/routes/mfa.py.
Service behavior¶
MFAService (services/gameserver/src/services/mfa_service.py):
generate_secret— creates apyotpTOTP secret, builds aprovisioning_uri(issuer"Sectorwars2102 Admin") and a QR code data URL (qrcodelibrary). Does not persist as verified.verify_setup— validates the submitted code against the pending secret (valid_window=1, ±1 time-step tolerance). On success: marks the secret verified, generates 10 backup codes, returns them once in the response (not retrievable again except via regenerate).verify_code— the shared check used by both/checkand any internal caller: tries a backup code first, falls back to TOTP; every attempt (success or failure) is written toMFAAttemptwith IP/user-agent.is_mfa_enabled,disable_mfa,get_backup_codes,regenerate_backup_codes,get_recent_attempts— status/management surface backing the endpoints above 1:1._generate_backup_codes— 10 codes, 8 alphanumeric characters each._is_backup_code/_use_backup_code— backup codes are single-use; a consumed code is removed from the stored set.
What this is not¶
Admin-ui remains the setup/verify UI (MFASetup.tsx / MFAVerification.tsx). /auth/mfa/* management is any authenticated user (get_current_user), not admin-scope-only. Player password login is MFA-gated on the gameserver (auth.py _mfa_gate_and_mint_tokens / requires_mfa) and player-client AuthContext — that login gate is documented in ../ARCHITECTURE/auth.md; this page previously claimed player login was unwired.
Source map¶
| Concern | Path |
|---|---|
Models (MFASecret, MFAAttempt) |
services/gameserver/src/models/mfa.py |
| Service | services/gameserver/src/services/mfa_service.py |
| Routes | services/gameserver/src/api/routes/mfa.py |
| Router mount | services/gameserver/src/api/api.py |
Related¶
../ARCHITECTURE/auth.md— loginmfa_code/requires_mfagate +/auth/mfaroute table../rbac-admin-scopes.md— admin RBAC generally (not the livemfa.pydependency)../admin-ui.md— admin-facing UI surfaces.