@passwordless-id/webauthn

repository·main·Indexed 20 days ago

https://github.com/passwordless-id/webauthn

A minimalistic, dependency-free wrapper around the WebAuthn protocol designed to simplify the implementation of WebAuthn and Passkeys for both client and server environments. It provides a `client` submodule for browser-side operations and a `server` submodule for backend verification. Supports NodeJS 19+, Cloudflare Workers, and all major browsers implementing the WebAuthn API.

Tokens
14.7K
Snippets
46
Records
67
Agent score
70%

What's inside @passwordless-id/webauthn

  1. Hybrid Authentication Strategies

    main

    For optimal UX and compatibility, you can combine different authentication methods. Recommended patterns include:

    • Allow list + Discovery: Use one for "Remember Me" functionality and the other for "Sign in with an alternate account".
    • Conditional UI + Allow list: Use this combination to support all browsers and security keys.
    • Discovery + Allow list: Use discovery as a fallback to ensure support for all security keys.

    Example Ideal Workflow:

    1. Sign in as...: Provide a list of stored users/credentials using locally stored credential IDs.
    2. Use another account: Provide a fallback using either Conditional UI (fetching from the server) or Discovery.
  2. How the WebAuthn authentication flow works

    main

    The authentication process follows a four-step sequence involving the User/Authenticator, the Browser, and the Server:

    1. Challenge Request: The Browser requests a unique, random challenge (nonce) from the Server to prevent replay attacks.
    2. Browser Authentication: The Browser triggers client.authenticate(...). The User performs local authentication (biometrics, PIN, etc.) on their device. The device signs the challenge with its private key.
    3. Payload Submission: The Browser sends the signed challenge payload back to the Server.
    4. Server Verification: The Server loads the user's public key (credential key) and verifies the signature against the challenge and expected parameters.
  3. Use Discoverable vs Non-discoverable credentials

    main

    There are two primary ways to trigger authentication:

    1. Discoverable (Default): If no list of allowed credential IDs is provided, the browser shows a native popup allowing the user to select a registered passkey. This requires the credential to be 'discoverable'.
    2. Non-discoverable (Explicit): You can prompt the user for their username first, retrieve a list of allowed credential IDs from your server, and then call authentication with the allowCredentials: [...] option. This typically bypasses the native selection popup and goes straight to user verification.

    Note on Security Keys: Non-discoverable credentials are useful for USB security keys (like Yubikeys) with limited storage ('slots'), as they do not require the key to store and list credentials.

  4. Configure passkeys for 2FA (Two-Factor Authentication)

    main

    To ensure a passkey acts as a form of 2FA, two conditions must be met:

    1. Possession Factor: The credential must be hardware-bound (not synced).
    2. Verification Factor: The userVerification flag must be set to required. This forces the user to provide 'something they are' (biometrics) or 'something they know' (PIN).

    Note: This library sets userVerification to required by default, which is more restrictive than the native WebAuthn protocol's preferred default. This ensures that only authenticators providing user verification (like fingerprint or PIN) can be used.

  5. Understand the @passwordless-id/webauthn module structure

    main

    The webauthn module is a bundle composed of several specialized modules. You can import only the ones you need to reduce your final JS bundle size:

    • client: Used for invoking WebAuthn operations in the browser.
    • server: Used for verifying responses on the server side.
    • parsers: Used to parse encoded data (registration, authentication, etc.) without performing verifications.
    • utils: Various encoding, decoding, challenge generation, and other utilities.
  6. Understand Attestation

    main

    Attestation is a proof of the specific authenticator model being used.

    • When to use: Only if you have stringent security requirements that necessitate allowing only specific hardware devices.
    • Drawbacks: Using attestation can deteriorate UX because the credential is created client-side and then potentially rejected server-side if it doesn't match requirements. Additionally, many platforms and password managers do not provide attestation data, or browsers may replace it with generic data to protect user privacy.
  7. How WebAuthn authentication flows work

    main

    WebAuthn relies on asymmetric cryptography (a public/private key pair).

    1. Registration: The authenticator creates a key pair. The public key is sent to the server, and the private key is stored securely by the authenticator (either hardware-bound or synced in the cloud).
    2. Authentication: The server sends a unique, random challenge to the browser. The authenticator signs this challenge using the private key. The server then verifies the signature using the stored public key.

    Security Note: The challenge must be a cryptographically strong random value generated by the server for every call, consumed upon use, and expired if unused to prevent replay attacks. You can use server.randomChallenge() for this purpose.

  8. Understand `userVerification` behavior across platforms

    main

    The behavior of the userVerification option (e.g., required, preferred, discouraged) depends heavily on the platform and the specific authenticator being used.

    iCloud Keychain

    | Biometrics available | discouraged: ❌ | preferred: ✅ | required: ✅ | | Biometrics not available | discouraged: ❌ | preferred: ❌ | required: ✅ |

    Google Password Manager (Desktop)

    | Biometrics available | discouraged: ❌ | preferred: ✅ | required: ✅ | | Biometrics not available | discouraged: ❌ | preferred: ❌ | required: ✅ |

    Windows Hello

    | Biometrics available | discouraged: ✅ | preferred: ✅ | required: ✅ | | Biometrics not available | discouraged: ✅ | preferred: ✅ | required: ✅ |

    Warning: Many password managers may provide inaccurate userVerified flags in their responses.

  9. How the WebAuthn registration flow works

    main

    The registration process follows these steps:

    1. Request Challenge: The Browser requests registration; the Server generates and sends a random challenge (nonce).
    2. Trigger Browser Registration: The Browser calls webauthn.register(...). The User performs local authentication (PIN, biometrics).
    3. Send Payload: The Browser sends the resulting JSON payload (containing the public key and attestation) to the Server.
    4. Verify: The Server verifies the payload against the original challenge and expected origin.
    5. Store: The Server stores the credential (specifically the public key) for future authentication.
  10. Distinguish between hardware-bound and synced passkeys

    main

    Passkeys (public key credentials) can be either hardware-bound or synced in the cloud.

    • Hardware-bound: The credential is tied to a specific physical device (e.g., a security key or a device's dedicated security chip). These are generally considered more secure because the user must physically possess the device.
    • Synced (Multi-device): The credential is synced with the user's platform account (Apple, Google, Microsoft) or a password manager. These offer higher convenience but delegate security to the software authenticator/account.

    During registration, you can check the credential.synced flag to determine which type was created.

  11. Authenticate a user (Client and Server)

    main

    Authentication follows a four-step process:

    1. Request Challenge: The server generates and stores a random challenge.
    2. Trigger Authentication (Browser): Call client.authenticate(credentialIds, challenge, options). If credentialIds is an empty array [], the platform will show a default UI to select a user (Passkeys/Discoverable credentials).
    3. Load Credential (Server): The server retrieves the stored credentialKey (id, publicKey, algorithm, synced) from the database using the credentialId provided in the authentication payload.
    4. Verify Authentication (Server): Call server.verifyAuthentication(authentication, credentialKey, expected) where expected includes challenge, origin, and optionally userVerified and counter.
    // 1. Browser: Trigger authentication
    import { client } from '@passwordless-id/webauthn'
    const challenge = "56535b13-5d93-4194-a282-f234c1c24500"
    const authentication = await client.authenticate(["3924HhJdJMy_svnUowT8eoXrOOO6NLP8SK85q2RPxdU"], challenge, {
      authenticatorType: "auto",
      userVerification: "required",
      timeout: 60000
    })
    
    // 2. Server: Verify authentication
    import { server } from '@passwordless-id/webauthn' 
    const credentialKey = {
        id: "3924HhJdJMy_svnUowT8eoXrOOO6NLP8SK85q2RPxdU",
        publicKey: "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEgyYqQmUAmDn9J7dR5xl-HlyAA0R2XV5sgQRnSGXbLt_xCrEdD1IVvvkyTmRD16y9p3C2O4PTZ0OF_ZYD2JgTVA==",
        algorithm: "ES256",
        synced: true
    }
    const expected = {
        challenge: "56535b13-5d93-4194-a282-f234c1c24500",
        origin: "http://localhost:8080",
        userVerified: true,
        counter: 123
    }
    const authenticationParsed = await server.verifyAuthentication(authentication, credentialKey, expected)
  12. Trigger authentication via Autocomplete (Conditional Mediation)

    main

    Autocomplete (also known as conditional mediation) allows authentication to be triggered automatically when a user interacts with an input field (e.g., selecting a user from an autocomplete list). This can potentially eliminate the need for separate "Register" and "Login" buttons.

    Implementation:

    1. Use discoverable: 'required' during registration.
    2. Call the authentication method with autocomplete: true when the input element is mounted in the DOM.

    Pros & Cons:

    • Advantages: Streamlined UX where authentication is triggered via input selection.
    • Drawbacks: High complexity; not cross-platform/browser friendly (does not work in Firefox, Opera, or many smaller browsers); does not work with every security key; fills up security key "slots". Note: A standard login button is still recommended for compatibility.
    // 1. Registration: discoverable: 'required'
    // 2. Authentication: autocomplete: true