diff --git a/docs/superpowers/specs/2026-04-26-password-reset-mfa-design.md b/docs/superpowers/specs/2026-04-26-password-reset-mfa-design.md index dfdfc95..047cb5f 100644 --- a/docs/superpowers/specs/2026-04-26-password-reset-mfa-design.md +++ b/docs/superpowers/specs/2026-04-26-password-reset-mfa-design.md @@ -56,9 +56,19 @@ POST /api/experience/submit -> redirect to sign-in page ``` +### Security Notification Email + +After a successful password reset, Logto sends a confirmation redirect — but no security awareness email. The SaaS backend sends a separate **security notification email** to the user's address with: + +- Subject: "Your Cameleer password was reset" +- Body: confirms the password was changed, states that **MFA was not required for this change**, and recommends enabling MFA if not already enrolled +- Includes a timestamp and "If this wasn't you, contact your administrator immediately" + +This is triggered by the custom sign-in UI after the Experience API `submit` succeeds — a `POST /api/password-reset-notification` call to the SaaS backend with the user's email. The backend sends the email via the configured SMTP connector. The endpoint is unauthenticated (the user hasn't signed in yet) but rate-limited and accepts only email addresses that exist in Logto. + ### Backend Changes -None. This is entirely between the custom sign-in UI and Logto's Experience API. The `ForgotPassword` email template is already configured in `EmailConnectorService`. +Minimal. The password reset flow itself is entirely between the custom sign-in UI and Logto's Experience API. The `ForgotPassword` email template is already configured in `EmailConnectorService`. The only new backend work is the security notification endpoint above. ## 2. MFA Configuration (Logto Bootstrap) @@ -121,7 +131,7 @@ After password verification + identification, `POST /api/experience/submit` retu New mode `mfaVerify` — shown when submit returns `mfa_factor_not_satisfied`: - 6-digit TOTP code input (reuse existing OTP input pattern) -- "Use backup code instead" link — switches to a single text input for the backup code +- **Prominent backup code fallback** — not a subtle link. Below the TOTP input, a clearly visible secondary action: "Lost your device? Use a backup code" styled as a distinct button or card (not a footnote-sized link). Users in a panic skip small text. The backup code view shows a single text input with clear instructions: "Enter one of your 10 backup codes" ### Experience API Flow @@ -244,15 +254,23 @@ A Spring Security filter that runs after JWT validation: 1. Extract `mfa_enrolled` claim from JWT 2. Look up the user's tenant -> check `settings.mfaRequired` 3. If tenant requires MFA and `mfa_enrolled` is `false`: - - Return `403` with `{ "error": "MFA_ENROLLMENT_REQUIRED", "message": "Your organization requires multi-factor authentication" }` + - Return `403` with a **distinct application error code** to avoid collision with generic Spring Security 403s (which get caught by global error handlers): + ```json + { + "error": "APP_MFA_REQUIRED", + "code": "mfa_enrollment_required", + "message": "Your organization requires multi-factor authentication" + } + ``` + - The response includes a custom header `X-Cameleer-Error: APP_MFA_REQUIRED` as a belt-and-suspenders signal — frontends can check either the body or the header - **Exempt paths**: `/api/tenant/mfa/*` (enrollment endpoints), `/api/config`, `/api/me` -4. Frontend catches 403 `MFA_ENROLLMENT_REQUIRED` -> redirects to Settings page with MFA section highlighted +4. Frontend intercepts 403 responses and checks for `APP_MFA_REQUIRED` specifically (not all 403s) -> redirects to Settings page with MFA section highlighted and an inline banner explaining the requirement ### Cameleer-Server Enforcement (via handoff doc) - Read `mfa_enrolled` from JWT (same token, same parsing as `roles` claim) - Query tenant MFA policy: SaaS exposes `GET /api/tenant/{slug}/mfa-policy` returning `{ "mfaRequired": true/false }` — server caches with 5-minute TTL -- Same enforcement logic: if required and not enrolled -> 403 +- Same enforcement logic: if required and not enrolled -> 403 with `APP_MFA_REQUIRED` error code (same format as SaaS backend) ### Token Refresh After Enrollment @@ -302,15 +320,16 @@ A separate spec file to be delivered alongside this design, covering: | Component | Changes | |-----------|---------| -| `ui/sign-in/src/SignInPage.tsx` | New modes: `forgotPassword`, `mfaVerify` | -| `ui/sign-in/src/experience-api.ts` | New functions: `forgotPassword()`, `verifyTotp()`, `verifyBackupCode()` | -| `ui/src/pages/SettingsPage.tsx` | MFA enrollment section, MFA requirement toggle | +| `ui/sign-in/src/SignInPage.tsx` | New modes: `forgotPassword`, `mfaVerify` (with prominent backup code fallback) | +| `ui/sign-in/src/experience-api.ts` | New functions: `forgotPassword()`, `verifyTotp()`, `verifyBackupCode()`, `notifyPasswordReset()` | +| `ui/src/pages/SettingsPage.tsx` | MFA enrollment section, MFA requirement toggle, `APP_MFA_REQUIRED` 403 interceptor | | `ui/src/pages/TeamPage.tsx` | "Reset MFA" action for team members | | `docker/logto-bootstrap.sh` | MFA factors in Phase 8c, `mfa_enrolled` claim in Phase 7b | | `TenantPortalController.java` | MFA endpoints (setup, verify, backup codes, status, remove) | | `TenantPortalService.java` | MFA business logic proxying to `LogtoManagementClient` | | `LogtoManagementClient.java` | New methods for MFA Management API calls | -| `SecurityConfig.java` or new filter | MFA enforcement filter (check JWT claim + tenant setting) | +| `SecurityConfig.java` or new filter | MFA enforcement filter (`APP_MFA_REQUIRED` error code + `X-Cameleer-Error` header) | +| New: password reset notification | `POST /api/password-reset-notification` — security email after reset (unauthenticated, rate-limited) | | New: server handoff doc | MFA API contract + UX + enforcement spec for cameleer-server team | ## Out of Scope