You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Add the account/security endpoints the My Account Security tab needs, and make the one-time-code request RESTful. All on the USER plane, owner-scoped, in the Identities aggregate (inside tenants).
This is PR3 of the My Account initiative. Independent of PR2; benefits from PR1 landing first but does not strictly require it.
Why
Research found the Security tab's actions are unbacked today. We add exactly what the design shows — no more (no device list / per-session revoke; the design only has "Sign out" + "Sign out of all").
Scope
1. RESTful one-time codes — POST /v1/verification-codes
Body: { "email": "...", "purpose": "signup" | "password_reset" } → sends a one-time, email-proof code (emailed in prod via Resend; returned in the response in dev, matching existing code behavior).
SupersedesPOST /v1/auth/password/reset-codeand the web-signup use of POST /v1/enrollment-codes.
purpose is bound to the issued code so a password_reset code cannot be replayed against signup (and vice-versa).
Same per-IP rate limiting as the existing code endpoints.
Daemon enrollment stays on /v1/enrollment-codes for now — convergence tracked separately (tech-debt issue).
2. Change password = email-code-exchange (no new mutation endpoint)
The Security-tab "Change password" drives the existing flow: POST /v1/verification-codes {purpose:"password_reset"} → POST /v1/auth/password/reset {code, new_password}. Proving email access is mandatory (no current-password path).
Consistent consequence: reset revokes all sessions; after success the user is bounced to sign-in with the success banner. (No backend change needed beyond WS-H: My Account SPA (site/) + account API #1; this item is mostly a contract note for the SPA + a test that the in-app path works end-to-end.)
3. Connected sign-in methods
GET /v1/me/identities → list the caller's login methods:
GET /v1/auth/oidc/{provider}/start?mode=link — when called while authenticated, carries the tenant through the signed OAuth state; the callback links the verified provider identity to the current account. Guard: reject (409) if that provider-subject is already linked to a different tenant.
DELETE /v1/me/identities/{provider} — unlink. Guarded by the "≥1 login method must always remain" invariant (422/409 if it would remove the last one).
4. Sessions
DELETE /v1/me/sessions — "sign out of all sessions": revoke the whole collection (including the current one), clear the cookie, → client redirects to sign-in. Reuses the existing delete_for_tenant.
Existing POST /v1/auth/logout stays for the current session. No device list / per-session revoke (not in the design).
Acceptance criteria
POST /v1/verification-codes works for both purposes; purpose binding enforced (cross-purpose code rejected); old password/reset-code removed (or aliased) and web-signup migrated to the new resource.
GET /v1/me/identities returns the caller's methods; never leaks secrets/hashes.
OIDC mode=link links to the authenticated tenant; cross-tenant subject collision rejected.
Summary
Add the account/security endpoints the My Account Security tab needs, and make the one-time-code request RESTful. All on the USER plane, owner-scoped, in the Identities aggregate (inside
tenants).This is PR3 of the My Account initiative. Independent of PR2; benefits from PR1 landing first but does not strictly require it.
Why
Research found the Security tab's actions are unbacked today. We add exactly what the design shows — no more (no device list / per-session revoke; the design only has "Sign out" + "Sign out of all").
Scope
1. RESTful one-time codes —
POST /v1/verification-codes{ "email": "...", "purpose": "signup" | "password_reset" }→ sends a one-time, email-proof code (emailed in prod via Resend; returned in the response in dev, matching existing code behavior).POST /v1/auth/password/reset-codeand the web-signup use ofPOST /v1/enrollment-codes.purposeis bound to the issued code so apassword_resetcode cannot be replayed against signup (and vice-versa)./v1/enrollment-codesfor now — convergence tracked separately (tech-debt issue).2. Change password = email-code-exchange (no new mutation endpoint)
POST /v1/verification-codes {purpose:"password_reset"}→POST /v1/auth/password/reset {code, new_password}. Proving email access is mandatory (no current-password path).3. Connected sign-in methods
GET /v1/me/identities→ list the caller's login methods:[ { "provider": "password", "label": "pedro@example.com", "connected_at": "..." }, { "provider": "google", "label": "pedro@gmail.com", "connected_at": "..." } ]GET /v1/auth/oidc/{provider}/start?mode=link— when called while authenticated, carries the tenant through the signed OAuth state; the callback links the verified provider identity to the current account. Guard: reject (409) if that provider-subject is already linked to a different tenant.DELETE /v1/me/identities/{provider}— unlink. Guarded by the "≥1 login method must always remain" invariant (422/409 if it would remove the last one).4. Sessions
DELETE /v1/me/sessions— "sign out of all sessions": revoke the whole collection (including the current one), clear the cookie, → client redirects to sign-in. Reuses the existingdelete_for_tenant.POST /v1/auth/logoutstays for the current session. No device list / per-session revoke (not in the design).Acceptance criteria
POST /v1/verification-codesworks for both purposes;purposebinding enforced (cross-purpose code rejected); oldpassword/reset-coderemoved (or aliased) and web-signup migrated to the new resource.GET /v1/me/identitiesreturns the caller's methods; never leaks secrets/hashes.mode=linklinks to the authenticated tenant; cross-tenant subject collision rejected.DELETE /v1/me/identities/{provider}enforces ≥1-remaining.DELETE /v1/me/sessionsrevokes all sessions + clears cookie; subsequent silent token exchange 401s.References
crates/tenants/src/api/auth.rs,repository/identity.rs,repository/session.rs,api/codes.rs.docs/adr/0009-human-web-authentication.md(the auth/session model).