Magic links
Sign in from an email: a link on the same device, or a code typed on any device. How it works, who can use it and what it protects against.
A magic link signs a user in with nothing but their mailbox. The email carries a link (opened on the device that asked) and a 6-digit code (typed in the app that asked, on any device); either one works, once, for 10 minutes. It is one more first factor, next to a password and providers: a user with a second factor is still asked for it.
It proves the same thing a password reset does (control of the mailbox), so it is as strong as that.
# realm.yaml: needs the email: section, and BACKD_SECRETS_KEY on the instance
magic_link:
enabled: true
signup: false # true: an address with no account may make one (verified) this way
sign_in:
allowed_redirects: [https://app.acme.example, "acme://"]
app_url: https://app.acme.example/signed-in # where a link goes without redirect_to
The flow
sequenceDiagram
participant App as App (the device that asked)
participant B as backd
participant M as Mailbox (any device)
App->>App: make a PKCE verifier and challenge
App->>B: POST /_auth/magic-link { email, redirect_to?, code_challenge }
B-->>App: 202 { request_id } (the same for every address)
B->>M: the email: a link and a 6-digit code
alt the link, on the same device
M->>B: GET /_auth/magic-link?token=… (a page with a button)
M->>B: POST /_auth/magic-link (the button)
B-->>App: 303 redirect_to?backd_code=…
App->>B: POST /_auth/token { code, code_verifier }
else the code, typed in the app
App->>B: POST /_auth/magic-link/verify { request_id, code, code_verifier }
end
B-->>App: the session, or the MFA challenge- Ask.
POST /_auth/magic-linkwith{"email", "code_challenge", "redirect_to"?, "locale"?}.code_challengeis the S256 challenge of a verifier only your app holds, as in the provider flow. The answer is always202 {"request_id"}. - The email has the link and the code. Redeeming either one uses up the request, so the other stops working.
- The link opens a page of
backdwith a button (pages/magic-link/<locale>.html, or the built-in one). Opening it never uses the link up, so a mail scanner that follows links can’t spoil it; only the button does. The button sends the browser toredirect_to(elsesign_in.app_url) with the one-time login code inbackd_code, bound to your app’s PKCE challenge, which the app redeems atPOST /_auth/tokenas for a provider. A link with nowhere to go (noredirect_to, noapp_url) shows a page that says to type the code, and isn’t used up. - The code is typed in the app, which sends it with its
request_idandcode_verifier: the code works only in the app that asked. Five wrong tries end the request.
An app that uses the JavaScript client needs no return route: the client sends the current page as redirect_to and finishes the sign-in when the page loads (see Sign in with providers).
Who can use it
| The address | What happens |
|---|---|
| belongs to a user | the link or code signs them in; an unverified address becomes verified (only the mailbox’s owner could have used it), and the account is claimed: the password, linked providers, second factor and sessions that existed before are removed, since someone who only knew the address may have set them (audited as user.claimed) |
has no account, magic_link.signup: true and the realm’s signup: open | the account is made when the link or code is used, never at request time: verified, with new_user: true, running account.on_signup |
| has no account otherwise | a no-account email (“someone tried to sign in with this address; there is no account”) |
| belongs to a disabled or erased user | nothing is sent |
The answer to a request is the same for all of them, in about the same work, so it says nothing about who is registered. Requests use the email limits (per recipient and per address); a limit that is reached isn’t told either.
Users who sign in by link get an email sign-in method the first time (shown in GET /_auth/me), so the rule that a user always keeps one way to sign in sees it like any other. The account rules apply as at any sign-in: login_networks, disabled users, and the second factor.
Accounts without a password
A user who signs in by link or provider needs no password:
| Endpoint | What it does |
|---|---|
DELETE /_auth/password | Removes the password (needs a fresh proof). 409 last_sign_in_method when nothing else would be left; ends the user’s other sessions and sends a security-notice |
POST /_auth/password | Sets one again (proof needed), or use a password reset; the first password of an account that had none sends a security-notice (password-set) |
GET /_auth/me | sign_in_methods lists what the user signs in with now (password, magic_link, providers), for an account settings page |
- A way in is a provider, or a verified address in a realm with magic links. A user who only has a password in a realm without magic links can’t remove it, and neither can one whose address isn’t verified.
- The same rule covers unlinking a provider: the last way in never goes, for the user or for an administrator.
- A user with no password and no second factor proves themselves for sensitive actions with a code emailed to them (
POST /_auth/reauth/email-code, see Re-authentication).
Errors
Status and code | When |
|---|---|
400 validation_error, 400 invalid_redirect | A bad address, challenge or redirect_to (never about the account) |
400 invalid_code | The code or the verifier is wrong (attempts_left in details) |
400 invalid_magic_link | An unknown, used, expired or exhausted request |
403 signin_refused, 403 signup_closed | The account can’t sign in; sign-up by link isn’t allowed |
403 magic_link_disabled | The realm doesn’t have magic links |
429 | Too many tries from one address |
The button’s redirect carries backd_error=signin_refused or signup_closed instead of a code.