Go Passwordless Without Locking Anyone Out
Passkeys remove the password, and with it the “forgot password” link everyone falls back on. This guide covers what replaces that link in Seamless Auth, so a lost phone or a policy change does not leave a user, or every administrator, unable to sign in. It ties together the recovery and safety features that shipped in auth API 0.20.0, then gives a recommended setup for a small team and for a larger organization.
Every setting, route and screen here was checked against seamless-auth-api
v0.20.0, admin dashboard
0.11.0, seamless-cli 0.26.0 and @seamless-auth/react 0.19.0. The versions that go together are
in the Compatibility Matrix.
The Ways Back In
Section titled “The Ways Back In”Plan for two kinds of failure. A user loses the device that holds their only passkey. An administrator changes sign-in policy in a way that refuses the factors people hold, possibly including their own. Each row below covers one of those, and the rest of the page explains each.
| Way back in | Covers | Default | Who acts |
|---|---|---|---|
| A second passkey | A lost or broken device | Allowed | The user |
| Recovery codes | Every passkey lost | On | The user |
| Self-service account recovery | Every factor lost, no codes left | Off | The user, after a delay |
| Admin device replacement | Anything, with identity proofing | Always | An administrator |
| Break-glass admins | A policy change that locks admins out | Not set | A designated admin |
| Staged policy and dry run | Finding the lockout before it happens | Always | An administrator |
None of these needs a password. Recovery codes and the break-glass exemption are not login
methods, so no login_methods change can take them away.
Second Passkeys
Section titled “Second Passkeys”The cheapest protection is a second passkey. A signed-in user can enroll as many as they like with
POST /webauthn/register/start and /webauthn/register/finish (access session), and the API will
not delete the last one: DELETE /users/credentials answers 400 with
You must keep at least one credential. when only one is left.
- Synced passkeys already survive a lost device. A passkey in iCloud Keychain, Google Password
Manager or another password manager comes back on the user’s next device. The default
authenticator_policyallows them (syncedPasskeys: "allow"). - Device-bound keys do not. If you set
syncedPasskeys: "block", or pin an AAGUID allow list, every passkey is tied to one authenticator, and a second one (a hardware security key, for example) is the difference between a lost device and a recovery. See Authenticator Policy. - The React SDK asks for one. The
PasskeyManagementcomponent lists, renames and deletes the user’s passkeys, and tells a user with one passkey to add a second on another device or a security key. See React SDK. - Get people onto a first passkey. For users who still sign in with a code or magic link,
prompt_passkey_enrollmentasks them to enroll one after sign-in, and the dashboard’s Enrollment area invites the rest. See Get Users Onto Passkeys.
The coverage report shows how many active users hold a passkey, and how many credentials are backup eligible (synced). It does not show how many passkeys each user holds, so there is no report of users with only one.
Recovery Codes
Section titled “Recovery Codes”Recovery codes are the user’s own way back in when every passkey is gone. They are on by default.
POST /recovery-codes(access session) returns ten single-use codes in the formXXXXX-XXXXX. This is the only time they are shown. Generating a new set invalidates the old one, and replacing an existing set needs a fresh step-up.GET /recovery-codesreports how many are left (remaining,total,createdAt).POST /recovery-codes/redeem, with the pre-auth token from/login, signs in with one code. It is rate limited like an OTP and is not subject to account lockout. It works whateverlogin_methodsorphishing_resistant_onlysay.- Using a code notifies the account’s verified email and phone, so a code used by someone else does not go unnoticed.
A code does not give a full session. It issues a recovery session (recovery: true in the
response, the access token and GET /users/me). Until the user enrolls a new passkey or an
authenticator app, every access route except enrollment, recovery codes, GET /users/me and
logout answers 403 enrollment_required. The server adapters’ requireAuth refuses a recovery
session too, unless a route passes allowRecoverySession: true. Pass it only on the routes your
app uses to drive that enrollment.
In the React SDK’s bundled screens, a user who enrolls a passkey or authenticator app and has no
codes is shown a new set straight away (they can skip it), the sign-in screen offers “Use a recovery
code”, and RecoveryCodeSettings shows how many are left and generates a new set. Custom screens
use the recovery methods on @seamless-auth/client 0.8.0 or @seamless-auth/react-native 0.4.0.
GET /system-config/public reports recoveryCodes, so a sign-in screen only offers them where
they work.
To turn them off, set recovery_codes to {"enabled": false, "requireAtEnrollment": false} (env
seed RECOVERY_CODES, see System Config).
Self-Service Account Recovery
Section titled “Self-Service Account Recovery”For a user with no passkey and no codes left, self-service recovery proves control of a verified email address or phone number, then makes them wait. It is off by default, because it is a way into an account that does not need any of the account’s factors.
Turn it on with account_recovery (env seed ACCOUNT_RECOVERY):
seamless config set account_recovery '{"enabled":true,"delay":"24h"}'delay is between 1h and 30d (default 24h). Send the whole object; a field left out takes its
default.
How it protects the owner:
POST /recovery/startsends a code to the verified address for the chosen channel, and answers the same whether or not the account has one.POST /recovery/verifyopens the request and starts the delay. Every verified address on the account is told, with a cancel link (${FRONTEND_URL}/recovery/cancel#<token>). If no notice reaches any verified address, the request is voided and the API answers503 recovery_notice_failed. Five wrong codes void the code.POST /recovery/complete, once the delay has passed, signs out every session on the account and issues a recovery session, so the user’s next step is enrolling a new passkey or authenticator app.- A signed-in owner can list open requests with
GET /recovery/requestsand cancel one withDELETE /recovery/requests/:id. Cancelling keeps working even after the feature is turned off.
Things to get right before turning it on:
- Notices must deliver. The delay only protects an owner who hears about the request. Configure
email and SMS delivery. If
your server adapter owns delivery, it must list
account_notice_emailandaccount_notice_smsinx-seamless-auth-delivery-kinds, or every verify answers503. - The cancel link must resolve. With the React SDK, mount
AuthRoutesat the root ofFRONTEND_URLso/recovery/cancelexists. The bundled screens include/account-recoveryand/recovery/cancel. - It is unavailable in
phishing_resistant_onlymode, since it proves only an address.GET /system-config/publicthen reportsaccountRecovery: false. Recovery codes are unaffected.
The full flow, including how a user resumes a request after the delay, is in the auth API’s account recovery guide.
Admin-Assisted Device Replacement
Section titled “Admin-Assisted Device Replacement”When a user has nothing left, an administrator can reset their factors after proving who they are.
On the dashboard’s user detail page, or with seamless users prepare-device-replacement, the admin
records identity proofing (in_person or remote_exception, an evidence reference, and an approver
for a remote exception). By default it revokes the user’s sessions, removes their passkeys and
disables their authenticator app. It needs admin:write and a fresh step-up. See
Device replacement recovery.
It does not touch recovery codes, and it does not sign the user in. Afterwards the user needs a way
to start a session and enroll a new passkey: a login method the policy allows (an email code or a
magic link, for example), or an unused recovery code. Under phishing_resistant_only, a user with no
passkey can start a session only with a recovery code, so check what the user will sign in with
before you remove their passkeys.
Break-glass Admins
Section titled “Break-glass Admins”A bad policy change can lock every administrator out: phishing_resistant_only with no enrolled
keys, a method list that leaves out what admins use, or an authenticator allow list nobody’s key is
on. BREAK_GLASS_EMAILS names admin accounts that sign-in policy cannot lock out. It is an
environment variable, not a system_config key, because anything the API can edit is something a
bad edit can remove.
BREAK_GLASS_EMAILS=breakglass-1@example.com,breakglass-2@example.com- Being listed is not enough. The account must also hold a role that satisfies
admin:write. Removing the role, revoking the account, or removing the address (and restarting) ends the exemption. At boot the API logs how many listed entries are not admin accounts. - What it is exempt from, for its own sign-in:
login_methods,phishing_resistant_only,passkey_login_fallback_enabledandrecovery_codes.enabled, andauthenticator_policywhen it enrolls a passkey. - What still applies. It must sign in with a passkey, an authenticator app or a recovery code.
Email and SMS codes, magic links and OAuth are refused for it (
403 login_method_disabled) even where everyone else may use them, because each proves only an address or an outside identity. Lockout still applies to its authenticator app codes. - Every use is announced. A sign-in or enrollment that relied on the exemption records a
break_glass_sign_inorbreak_glass_enrollmentauth event withseverity: "high"in its metadata, and emails every admin. A sign-in the policy would have allowed anyway is not announced.
Set up each break-glass account while the policy still allows it: enroll a passkey (ideally a
hardware key kept offline) and generate its recovery codes, and keep both where at least two people
can reach them. Neither the dashboard nor the CLI has a recovery code screen, so generate the codes
from your application’s account screens (for example the React SDK’s RecoveryCodeSettings) while
signed in as that account. Use the account only to recover the deployment: sign in, correct the
policy, sign out. After any use, regenerate its codes. The auth API’s
break-glass section
covers rotating it to a different account.
See also Service And Security Secrets.
Staged Policy And Dry Run
Section titled “Staged Policy And Dry Run”The policy keys are the system_config keys that decide who can sign in, and how:
login_methods, passkey_login_fallback_enabled, phishing_resistant_only,
prompt_passkey_enrollment, authenticator_policy, lockout_policy, recovery_codes and
account_recovery. Every change to them is kept as a numbered version, and a change can be staged
as a draft and dry-run before it applies.
On the admin dashboard, the Policy page (titled Sign-in Policy) holds all of it:
- Start Draft, edit, and Save Draft. There is one draft per instance, and nothing changes for users until it is published.
- Run Dry Run with 1 to 30 days of sign-ins. It evaluates the draft, or your unsaved edits, against completed sign-ins in that window, and lists every account the draft would refuse, with the reason and whether the account would have No remaining method. Those accounts are listed first.
- Publish. Publishing needs
admin:writeand a fresh step-up, and is refused with409 policy_version_conflictif someone else published since your draft was started. - Roll back from Version History if it goes wrong. A rollback publishes a copy of the older version as a new version, so history is never rewritten.
The same operations are API routes under /system-config/policy (GET /system-config/policy,
PUT and DELETE /system-config/policy/draft, POST /system-config/policy/publish,
/rollback and /dry-run, and GET /system-config/policy/versions). See the auth API’s
policy changes guide
for request bodies and the full list of dry-run reasons.
Read the dry run with its limits in mind:
- It only sees accounts that signed in during the window. An account that has not signed in for 30 days is not evaluated, however the draft would treat it.
- “No remaining method” ignores recovery codes. It is worked out from the login methods and OAuth identities the account would have left, so a listed account may still hold codes. Treat it as the worst case.
- Authenticator rules apply at enrollment, not sign-in. A passkey the draft’s
authenticator_policywould not admit keeps signing in. The dry run reports it asauthenticator_not_allowedorsynced_passkey_blocked, meaning the user could not enroll that authenticator again, for example on a replacement device. - It evaluates at most 10,000 accounts and says so (
capped) when it reaches the limit. - Break-glass accounts are counted, never listed, since policy does not apply to them.
Staging is optional. A whole-config save on the dashboard’s System page, PATCH /system-config/admin, or seamless config set applies at once, and is recorded as a
config_patch version that can be rolled back like any other. The CLI has no policy draft, dry-run
or rollback commands; seamless config apply --dry-run only previews a config file and does not
evaluate sign-ins.
Recommended Setup For A Small Team
Section titled “Recommended Setup For A Small Team”For a single application with a handful of administrators and users who mostly know each other.
- Keep recovery codes on (the default) and make sure users save them. The React bundled screens offer a set at first enrollment; if you build your own screens, offer one there too.
- Ask for a second passkey, or rely on synced passkeys and keep
syncedPasskeys: "allow". MountPasskeyManagement(or an equivalent) on the account settings page. - Leave self-service recovery off unless you have reliable email delivery and no administrator available to do a device replacement. When an administrator can check who someone is, device replacement is the stronger path.
- List one break-glass account in
BREAK_GLASS_EMAILS, separate from your daily admin account, with a hardware key and recovery codes kept offline. Make sure the daily admin account has an authenticator app, since admin writes need a step-up and the CLI steps up with TOTP. - Dry run before every policy change, with 30 days of sign-ins, and publish only when no account shows “No remaining method”. Keep the previous version number handy so you can roll back.
- Leave
phishing_resistant_onlyoff until the coverage report shows everyone holds a passkey. When you turn it on, recovery codes and device replacement are the only ways back for a user who loses every passkey.
Recommended Setup For A Larger Organization
Section titled “Recommended Setup For A Larger Organization”For many users, several administrators, or a compliance requirement to reach passkey-only sign-in.
- Enroll before you enforce. Turn on
prompt_passkey_enrollment, invite users from the dashboard’s Enrollment area, and track the coverage report until passkey coverage is where you need it. See Get Users Onto Passkeys. - Require backups by policy where you block synced passkeys. With
syncedPasskeys: "block"or an AAGUID allow list, every passkey is device-bound, so make two keys per person part of onboarding. Recovery codes stay on. - Tighten in stages, each one drafted and dry-run. For example: first turn off
passkey_login_fallback_enabled, so users who hold a passkey must use it; then remove the address-basedlogin_methods; then turn onphishing_resistant_only. Before each publish, run the dry run over 30 days, contact every account listed with “No remaining method”, and watch sign-in failures on the dashboard’s Events page afterwards. Roll back if refusals spike. - Decide on self-service recovery deliberately. It is unavailable once
phishing_resistant_onlyis on. Before that, if you enable it, use a longerdelaythan the default so notices have time to reach the owner, and confirm your adapter forwards account notices. Many organizations will prefer device replacement with identity proofing, recorded with an evidence reference such as a ticket number. - Run two or more break-glass accounts, each with its own hardware key and recovery codes,
stored in separate places that need two people to open. Alert on
break_glass_sign_inandbreak_glass_enrollmentevents, treat any unplanned one as an incident, and regenerate codes after every use. - Watch the recovery events.
recovery_code_used,account_recovery_verified,account_recovery_completed,admin_device_replacement_recoveryandpolicy_publishedare the events to review. See Auth Events.