Practical FIDO: Implementing Passwordless Login
Introduction
Managing passwords for different sites is not only easy to forget, but also vulnerable to theft or reuse. If you can complete registration and login by scanning a fingerprint or using facial recognition — that’s the vision promoted by FIDO (Fast IDentity Online).
With FIDO you get the following benefits:
- The server doesn’t store passwords, so there’s nothing to steal
- Attackers can’t intercept credentials over the network because the private key is never transmitted
- Binding to the site’s Origin and enforcing HTTPS ensures secure connections. Even if a user lands on a phishing site, the browser won’t allow it.
- Biometrics can be used to strengthen the “unlock private key” step
FIDO Overview
Use asymmetric cryptography to authenticate, abandoning usernames and passwords entirely
- Challenge: a one-time random string for each request, used for signing with the private key
- Signature: the operation or result of signing the challenge with the private key
- FIDO Registration: the user generates a public/private key pair; the site stores the public key and the user keeps the private key
- FIDO Login: verify the user possesses the private key and that the response was generated for the current challenge (prevents replay attacks)
- Server generates a challenge
- The user’s device signs the challenge with the private key
- Server verifies the signature using the public key
- Biometrics: a way for the user to unlock the private key locally, e.g., fingerprint
- Passkey: a mechanism to manage private keys. It’s a keychain system built on top of private key technology that makes keys understandable, manageable, and less likely to be lost
How is FIDO possible?
FIDO is possible because browsers provide the Web Authentication API to interact with private keys securely, perform user verification, bind to Origin, and devices.
-
User →
navigator.credentials.create()- OS / hardware help generate the key
- Private key is locked on the device
- Public key is given to the server
-
User →
navigator.credentials.get()- Use private key to sign the challenge
- Return the signature
- Server verifies using the public key
FIDO Flow Diagrams
Registration
Login
Implementation
Registration
-
Phase 1: Begin
- The user, after signing in or registering, requests to bind a Passkey.
- Frontend calls:
POST /auth/passkey/register/begin. - Backend returns:
challenge: a one-time random string.userinfo: includesid,name,displayName.relying party(RP): site domain orrpId.pubKeyCredParams: supported crypto algorithms.
- Frontend runs
navigator.credentials.create():- Generates public/private keys and performs Attestation (proves key origin and legitimacy)
-
Phase 2: Finish
- Frontend sends the result back to backend:
POST /auth/passkey/register/finish. - Backend performs multiple verifications:
- Consistency: whether the challenge matches the one previously issued (prevent replay attacks).
- Legitimacy: whether the Attestation is valid (confirm device and key are trusted).
- Origin: whether Origin /
rpIdis correct (prevent phishing).
- On successful verification, store:
public key,credential id,counter, associated with theuser.
- Completion: the user has successfully bound a Passkey.
- Frontend sends the result back to backend:
Login
-
Phase 1: Begin
- Use FIDO to log in
- Frontend calls:
POST /auth/passkey/login/begin. - Backend returns:
challenge: the challenge string.allowCredentials: the user’s registered Credential IDs (leave empty to use Discoverable Credentials mode).
- Frontend runs
navigator.credentials.get():- User verification: unlock the private key via fingerprint, Face ID, or PIN.
- Signing: sign the challenge with the private key.
-
Phase 2: Finish
- Frontend sends the signature result back to backend:
POST /auth/passkey/login/finish. - Backend verification logic:
- Challenge: whether the challenge matches.
- Signature: verify the signature with the public key from the database.
- Counter: ensure the
counteris greater than the previous value (prevent replay or cloned devices). - Origin: whether Origin /
rpIdis correct.
- On successful verification:
- Update the
counterin the database. - Establish login state (issue Session or JWT).
- Update the
- Completion: achieve secure passwordless login.
- Frontend sends the signature result back to backend:
Discoverable Credentials (Resident Keys)
With traditional Non-Discoverable credentials, the server must know who you are first (you enter an account) so it can look up the Credential ID and send it to the authenticator. The authenticator decrypts and then signs with the private key.
Modern authenticators have enough space. During registration, they not only generate key pairs but also store the private key, user ID, account name, and the site’s domain (RP ID) on the authenticator.
At login, the server doesn’t need to know who you are. It can ask the authenticator: “Do you have credentials for this site (RP ID)?” The authenticator will locally “discover” those credentials and present a chooser letting you pick which account to use, without entering an account name.
Registration
navigator.credentials.create({ publicKey: { // ... Other settings authenticatorSelection: { authenticatorAttachment: "platform", // Requires the creation of discoverable credentials residentKey: "required", userVerification: "required" } }});residentKeyrequired: must create a discoverable credential. If unable, the system returnsNotSupportedError.preferred: RP prefers a discoverable credential but accepts non-discoverable.discouraged: RP prefers non-discoverable credentials but accepts discoverable ones.
- requireResidentKey:
For backward compatibility with the older WebAuthn Level 1 specification, this property is retained. If
residentKeyisrequired, set this to true; otherwise set it to false.
Login
When initiating an authentication request, you don’t need to include specific Credential IDs in allowCredentials.
navigator.credentials.get({ publicKey: { challenge: serverGeneratedChallenge, rpId: "yourdomain.com", // Leave blank or do not provide an allowCredentials array // The authenticator will automatically "discover" all available accounts locally based on the rpId. allowCredentials: [], userVerification: "required" }});Conclusion
The FIDO specification defines clear verification steps to avoid security pitfalls and accounts for differences across browsers and platforms. In practice it’s best to rely on existing libraries to handle the nuances — which is why many FIDO libraries provide functions like verifyRegistrationResponse() and verifyAuthenticationResponse() ready to use.