Login methods
The hosted sign-in screens are what /authorize redirects to. They offer four first factors, all
converging on one completion path, with passkeys and TOTP layered on top.
Email one-time codes
Section titled “Email one-time codes”POST /login/email/startContent-Type: application/json
{ "email": "ada@acme.com", "turnstile_token": "…" }{ "ok": true }The response is { "ok": true } whether or not the address belongs to a user. Enumeration-safety is
the point: a login form that says “no such user” is a user-directory lookup for anyone who asks.
POST /login/email/verify
{ "email": "ada@acme.com", "code": "123456", "flow": "<flow id>" }Codes are hashed at rest and valid for 10 minutes. A code is burned after 5 wrong guesses,
so the six-digit space cannot be searched within its lifetime, and a cumulative 15 failures
against one address blocks further attempts regardless of how many fresh codes are requested. amr
becomes ["otp"].
Magic links
Section titled “Magic links”POST /login/link/start{ "email": "ada@acme.com", "flow": "<flow id>" }The emailed link is a GET to /login/link, which deliberately does not consume the token —
the screen it opens does, by POSTing to /login/link/verify. That split exists because mail
scanners and link-preview bots follow URLs in email, and a token consumed by a scanner is a token the
user cannot use.
Tokens are hashed at rest, single-use, and valid for 15 minutes. amr becomes ["link"].
Email and password
Section titled “Email and password”POST /login/password{ "email": "ada@acme.com", "password": "…", "flow": "<flow id>" }Hashing is PBKDF2-SHA256 on WebCrypto, with the iteration count stored per hash so it can be raised over time without invalidating existing passwords.
Policy is checked on set: minimum length, a common-password list, and no containment of the user’s
own email address. A rejected password returns 400 weak_password with the reason.
Failures are capped per email address — ten attempts — as well as per IP. The per-email lockout
engages first and surfaces as invalid_credentials, which is why the password
rate-limit bucket is deliberately roomier than the email one: an attacker
grinding one account should hit the account lockout, not a generic IP throttle that tells them
nothing.
amr becomes ["pwd"].
Changing and resetting
Section titled “Changing and resetting”POST /login/password/change # session required{ "current_password": "…", "new_password": "…" }
POST /login/password/reset/start # enumeration-safe{ "email": "ada@acme.com" }
POST /login/password/reset/confirm{ "token": "…", "new_password": "…" }The reset flow doubles as first-time password set for users provisioned by SCIM or SSO, who have no password yet. Reset tokens are valid for 30 minutes.
Social sign-in
Section titled “Social sign-in”GET /login/social # which providers are configuredGET /login/social/:provider/startGET /login/social/:provider/callbackGoogle, Microsoft, and GitHub, configured at the platform level. The brokered leg uses PKCE and a nonce.
Accounts link on verified email and are recorded in a persistent (provider, subject) identity
map, so a user who signs in with Google and later with a password reaches the same account, and a
provider that changes a user’s email address does not orphan it.
Unknown emails are provisioned just-in-time — except throwaway-inbox domains, which are refused at
signup. amr becomes ["sso"].
One completion path
Section titled “One completion path”Every first factor finishes the same way, which is what makes MFA uniform rather than per-method.
If the user has no active TOTP enrollment, the login completes: a session is created, a
login_success audit event is written, and — when the login was resuming an /authorize flow — the
response carries a continue URL to the consent screen.
{ "ok": true, "user_id": "usr_01J…", "continue": "/u/consent?flow=…" }If the user does have TOTP active, no session is created. Instead a short-lived,
attempt-capped mfa_token is returned, and only /login/mfa/verify can finish the
login:
{ "ok": true, "mfa_required": true, "mfa_token": "mfa_…" }Because this is one path rather than four, adding a second factor covers every first factor at once — there is no method that quietly bypasses it.
Branding
Section titled “Branding”GET /login/brandingReturns the tenant’s display name, logo, and primary colour, resolved from the request host. The
hosted screens use it so a customer’s users see their own employer at sign-in rather than yours. Set
it with PATCH /v1/organizations/:orgId.
What is recorded
Section titled “What is recorded”Each step writes an audit event: login_code_sent, login_link_sent,
login_success, social_login, password_set, password_changed,
password_reset_requested, password_reset. Blocked and challenged attempts write
login_blocked and login_challenged — see bot and abuse protection.
After a successful login, anomaly signals are recorded as login_anomaly without gating the login:
a first-ever device, or travel from the previous login’s location that is not physically possible.
Those are yours to act on.