ADR 0001 — WebAuthn / Passkeys as the Only Authentication Factor
Status: Accepted
Date: 2026-04-21
Deciders: product owner (user), software-architect
Scope: All user-facing authentication in TradeMasterAPI (Raptor + Antlers)
Context
TradeMasterAPI is adding multi-user identity. The product owner has set a hard invariant: the system must never store replayable credentials, and no password, SMS OTP, or email OTP may serve as the authentication factor. Additionally the platform handles money movement (live trading) — phishing resistance is not a "nice to have", it is a pre-condition for shipping live-mode to users.
We need a single decision: what is the authentication factor?
Decision
WebAuthn / FIDO2 passkeys are the sole authentication factor. Both platform authenticators (Face ID, Touch ID, Windows Hello, Android biometric) and roaming authenticators (YubiKey, Titan, SoloKey, etc.) are accepted. The backend stores only public keys and credential metadata, never a secret that could impersonate the user.
The implementation will rely on standard libraries (probably py-webauthn on the backend and @simplewebauthn/browser on the frontend) — specific version pinned during the implementation sub-card, not here.
Account recovery, when the primary passkey is lost, is an email-gated re-attestation flow, not a fallback factor. See docs/architecture/auth.md §5.3.
Consequences
Positive
Phishing-resistant by construction. Credentials are origin-scoped; a phishing page on a look-alike domain cannot harvest anything useful.
No credential database to breach. The worst an attacker can exfiltrate from our DB is a list of public keys — by design insufficient to log in as anyone.
No password-reset support burden. The concept does not exist in our system.
Better UX than U/P + MFA on modern devices (Face ID, one tap).
Aligns with NIST SP 800-63B AAL2/AAL3 phishing-resistant authenticator guidance, and with CISA's 2023 passkey push. Easier to defend to auditors.
Negative / risks
User education burden. Retail traders are not all familiar with passkeys; onboarding must teach the concept.
Browser / OS floor. Requires reasonably modern browsers (Chrome 67+, Safari 14+, Firefox 60+, Edge 18+). We publish a support matrix rather than build fallbacks.
Account recovery is a known weak point. Email is the only fallback channel, and email accounts get compromised. Mitigated by (a) 72-hour cooldown on live-trading after recovery, (b) full session + credential revocation on recovery, (c) visible notification, (d) rate-limiting.
Corporate / enterprise SSO integration is out of scope. If we ever sell to orgs we will need SAML/OIDC in addition to passkeys, not instead of. That is a future ADR.
Shared-device scenarios are awkward (library computer, borrowed laptop). Usernameless + discoverable credentials make this tolerable but not seamless. Acceptable for the individual-trader persona.
Neutral
RP ID must be chosen carefully and is effectively immutable after first user registration (see auth.md §10 open question #2).
Sign-count handling in py-webauthn is permissive; we log rather than block on anomalies in v1.
Alternatives considered
Passwords (hashed with Argon2id)
Rejected. Violates the invariant — even Argon2id hashes are still credentials we store and could (in theory) allow offline cracking after a breach. Password reset flows re-introduce email-OTP as an implicit factor. No.
Magic links only (email as the factor)
Rejected. Turns the email inbox into the single factor for every login, not just recovery. Phishable. Slow. Breaks with aggressive email-link-rewriters (Microsoft Safe Links, Barracuda). Does not meet AAL2.
TOTP / SMS OTP as primary
Rejected. SMS is explicitly out by invariant #3 (email is the only contact channel). TOTP on its own is not an identity — it needs a paired password, which we forbid. TOTP as a second factor on top of passkeys adds zero value because the passkey already requires user verification.
OIDC-only (log in with Google / Apple)
Rejected as the sole strategy. We do not want to make Google Identity a hard dependency for our users, and some traders prefer to avoid it. Passkeys give us provider-independent phishing resistance. We may add "Sign in with Apple / Google" as an additional path in a later ADR, but only if it can be wired through without storing any provider tokens long-term.
SAML-only
Rejected. SAML is an enterprise SSO tool, not a retail-user auth scheme. Overkill and wrong persona.
Client certificates (mTLS) to the browser
Rejected on UX grounds. Managing client certs in a browser is hostile to non-technical users. mTLS remains an option for server-to-server auth in a later ADR.
Compliance checklist
[x] Meets non-negotiable invariant #1 (no stored credentials).