Account switching
When one person controls multiple email-based accounts (e.g. a personal account and a company-email account from corporate onboarding), they can link them and switch without signing in again. Base path: /api/auth/linked-identities.
Linking (proof of ownership on both sides)
Section titled “Linking (proof of ownership on both sides)”| Method · Route | Purpose |
|---|---|
POST /initiate | Signed-in user A sends { "email": "<B>" }; Axowl emails B a single-use confirm link (valid 15 minutes). An address with no Axowl account gets the same 200 answer and no email, so the response does not reveal whether an account exists. One request per minute per caller and address — a second one answers 400 rate_limited. Other errors: 400 self_link, 400 already_linked. |
GET /confirm?token= | B opens the emailed link and sees a confirmation page with a Link accounts button. Opening the link changes nothing and does not use up the token. |
POST /confirm | Sent by the button on that page with { "token": "…" }: creates the sealed link (A↔B) and uses up the token. The token is the proof; no session is needed. Returns { message, redirectUrl }. Errors (400): invalid_token, expired_token, account_gone, already_linked. |
GET / | List the current user’s active links (linkId, userId, email, displayName, linkedAt) — for an account-switcher menu. |
POST /{id}/revoke | Unlink. Either side can revoke; the link record is marked revoked and kept, never hard-deleted. |
/initiate and /{id}/revoke only work from a sealed device.
Switching (account-takeover-critical)
Section titled “Switching (account-takeover-critical)”POST /switch re-issues the session as the linked identity and swaps the cookie — gated by a fresh passkey step-up of the current session owner. Body: { "targetUserId", "assertionResponseJson" }, where assertionResponseJson is the result of a fresh navigator.credentials.get() for the current account.
targetUserIdmust be a valid id (400invalid_target) and a link must already exist between caller and target, else 404not_linked.- A passkey assertion is required (403
step_up_requiredif absent). - The assertion must verify and belong to the caller’s own account, so a stolen cookie + an attacker’s own passkey cannot pivot (403
step_up_failed). - On success, the response sets the session cookie of the target account and returns its
userId,emailandorganizationId.
Linking requires explicit proof of ownership of both accounts — accounts are never linked automatically.