Skip to content

feat(auth): add a phishing-resistant-only login mode - #355

Merged
Bccorb merged 2 commits into
mainfrom
feat/phishing-resistant-only
Oct 7, 2026
Merged

Bccorb merged 2 commits into
mainfrom
feat/phishing-resistant-only

Conversation

@Bccorb

@Bccorb Bccorb commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Closes #177.

Depends on @seamless-auth/types 0.27.0 (fells-code/seamless-auth-types#89), now published and pinned here.

What changed

phishing_resistant_only (new, default false)

  • normalizeLoginPolicy narrows the effective policy to loginMethods: ['passkey'] and turns fallback off. Only a literal true turns it on.
  • Email and phone code login, magic links, TOTP login and OAuth all answer 403 login_method_disabled (OAuth providers are hidden from start and callback). GET /system-config/public reports loginMethods: ["passkey"].
  • Account bootstrap: the email code that verifies a new account's address still starts one session, so the first passkey can be enrolled (passkey enrollment is on auth: 'access'). An already-verified account cannot use that path. An account that never enrolls has to go through admin recovery.
  • Backstop: issueSessionAndRespond now takes a required method and refuses anything but passkey in this mode, unless the call is the account-verification one. Each endpoint refuses before it gets there, so this only fires if a future endpoint forgets.
  • Env var PHISHING_RESISTANT_ONLY. Docs updated.

Passkey fallback rule is now enforced on the endpoints themselves

passkey_login_fallback_enabled: false used to be applied only when /login built the method list. The continuation endpoints checked whether a method was enabled for the deployment, not whether this account was allowed to use it. A user holding a passkey could still call the email or phone code, magic link, TOTP login or email verification endpoint with the ephemeral token /login issued and get a session. All of those now answer 403 login_method_disabled for that user.

Decoy parity

The decoy responders for unknown identifiers apply both rules to the decoy's derived passkey shape (the same one /login advertised for it), so the new refusals do not reveal whether an account exists. There is a parametrized integration suite covering both rules across the affected endpoints.

Contract

  • New config key (types#89). POST /totp/verify-login gains a 403. Everything else that now refuses already documented 403 login_method_disabled.
  • No new routes, so no adapter passthrough work. Follow-up: the admin dashboard needs a toggle for the new key.

Not changed

  • Step-up still accepts OTP and TOTP. Step-up raises the assurance of an existing session rather than starting one. Whether strict mode should also require a passkey for step-up is worth a separate decision.
  • The default for new deployments stays off. Changing it is a product positioning call.

Verification

npm run typecheck, npm run lint, npm run format:check, npm run build, npm run coverage (1626 passed, thresholds met). Run against a local build of types#89.

Bccorb added 2 commits October 6, 2026 20:37
Adds the phishing_resistant_only config key, which accepts passkeys only
for every account apart from the one session that verifies a new
account's address. The passkey fallback rule is now enforced on each
continuation endpoint rather than only in the /login method list, and
decoys mirror both rules.

Closes #177.
@Bccorb
Bccorb force-pushed the feat/phishing-resistant-only branch from 8369444 to 4e9460e Compare October 7, 2026 00:39
@Bccorb
Bccorb marked this pull request as ready for review October 7, 2026 00:39
@Bccorb
Bccorb merged commit c68a315 into main Oct 7, 2026
7 checks passed
@Bccorb
Bccorb deleted the feat/phishing-resistant-only branch October 7, 2026 01:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

No enforceable phishing-resistant-only login mode, and magic link is on by default

1 participant