diff --git a/docs/superpowers/specs/2026-04-26-server-mfa-handoff.md b/docs/superpowers/specs/2026-04-26-server-mfa-handoff.md new file mode 100644 index 0000000..b0d19f1 --- /dev/null +++ b/docs/superpowers/specs/2026-04-26-server-mfa-handoff.md @@ -0,0 +1,152 @@ +# Cameleer-Server MFA Handoff Document + +**Date:** 2026-04-26 +**For:** cameleer-server team +**Context:** The SaaS platform now supports TOTP MFA with backup codes. This document specifies what the server team needs to implement for MFA enrollment in the server UI. + +## 1. JWT Claim: `mfa_enrolled` + +Every access token now includes an `mfa_enrolled: boolean` claim, set by the Logto Custom JWT script. The server already parses JWT claims for the `roles` field — `mfa_enrolled` works identically. + +**Example decoded JWT payload:** + +```json +{ + "sub": "user-id-123", + "roles": ["server:admin"], + "mfa_enrolled": true, + "aud": "https://api.cameleer.local", + "scope": "tenant:manage tenant:view" +} +``` + +## 2. Enforcement + +### When to enforce + +Check whether the tenant requires MFA: + +- **Endpoint:** `GET /platform/api/tenant/{slug}/mfa-policy` +- **Auth:** M2M token (same as existing server -> SaaS API calls) +- **Response:** `{ "mfaRequired": true/false }` +- **Cache:** 5-minute TTL recommended + +### How to enforce + +On authenticated requests, if `mfaRequired` is `true` and the JWT `mfa_enrolled` claim is `false`: + +**Response:** + +``` +HTTP 403 +X-Cameleer-Error: APP_MFA_REQUIRED +Content-Type: application/json + +{ + "error": "APP_MFA_REQUIRED", + "code": "mfa_enrollment_required", + "message": "Your organization requires multi-factor authentication" +} +``` + +**Exempt paths:** MFA enrollment endpoints (below), health checks, public assets. + +The server UI should intercept 403 responses with `X-Cameleer-Error: APP_MFA_REQUIRED` and redirect to the MFA enrollment page. + +## 3. MFA Enrollment API + +The server needs to call Logto's Management API to manage MFA for users. Use the existing M2M token for authentication. + +### Get MFA status + +``` +GET https://{logto-endpoint}/api/users/{userId}/mfa-verifications +Authorization: Bearer {m2m_token} + +Response: [ + { "id": "ver-123", "type": "Totp", "createdAt": "..." }, + { "id": "ver-456", "type": "BackupCode", "createdAt": "..." } +] +``` + +### Generate TOTP secret + +Generate a 20-byte random secret, Base32-encode it, and create a QR code URI: + +``` +otpauth://totp/Cameleer:{userEmail}?secret={base32Secret}&issuer=Cameleer +``` + +Show the QR code to the user. After they scan and provide a 6-digit code, verify it server-side using the TOTP algorithm (RFC 6238, HMAC-SHA1, 30-second window, +/-1 step drift). + +### Bind TOTP to user + +After successful verification: + +``` +POST https://{logto-endpoint}/api/users/{userId}/mfa-verifications +Authorization: Bearer {m2m_token} +Content-Type: application/json + +{ "type": "Totp", "secret": "{base32Secret}" } + +Response: { "type": "Totp", "secret": "...", "secretQrCode": "..." } +``` + +### Generate backup codes + +After TOTP is bound: + +``` +POST https://{logto-endpoint}/api/users/{userId}/mfa-verifications +Authorization: Bearer {m2m_token} +Content-Type: application/json + +{ "type": "BackupCode" } + +Response: { "type": "BackupCode", "codes": ["abc123", "def456", ...] } +``` + +Display the 10 codes once. User must acknowledge saving them before dismissing. + +### Remove MFA (admin action) + +``` +DELETE https://{logto-endpoint}/api/users/{userId}/mfa-verifications/{verificationId} +Authorization: Bearer {m2m_token} +``` + +Remove all verifications (TOTP + BackupCode) to fully reset MFA for a user. + +## 4. UX Requirements + +### Enrollment flow + +1. User clicks "Set up MFA" in settings +2. Show QR code (200x200px) with the TOTP secret URI +3. User scans with authenticator app +4. User enters 6-digit verification code +5. On success -> show 10 backup codes in a 2-column monospace grid +6. "Copy all" and "Download .txt" buttons +7. Checkbox: "I've saved my backup codes" — must be checked before dismissing +8. After dismissal, force token refresh to get `mfa_enrolled: true` in JWT + +### Enrolled state + +- Show "Authenticator app configured" with green status badge +- "Regenerate backup codes" button +- "Remove MFA" button with confirmation dialog + +### Backup code fallback (sign-in) + +This is handled by the SaaS custom sign-in UI, not the server. No server changes needed for the sign-in flow. + +## 5. Error States + +| Scenario | Response | +|----------|----------| +| User already has TOTP enrolled | 422 — "TOTP already configured" | +| Invalid TOTP code during setup | Show error, let user retry | +| Backup code already used (sign-in) | Handled by SaaS sign-in UI | +| All backup codes exhausted | Admin removes MFA via team page | +| Remove MFA while enforcement active | User will be prompted to re-enroll on next request |