
Simplewebauthn
- 1 installs
- Updated June 16, 2026
- csc3213-2026-group-b/agent-skills
Helps with ai & agent building tasks.
About
simplewebauthn is a Claude Code skill for ai & agent building. It helps you ship faster with AI-assisted development.
- simplewebauthn
- AI & Agent Building
- AI-coding skill
Simplewebauthn by the numbers
- 1 all-time installs (skills.sh)
- Ranked #14,102 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Jul 7, 2026 (Skillselion catalog sync)
npx skills add https://github.com/csc3213-2026-group-b/agent-skills --skill simplewebauthnAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1 |
|---|---|
| Last updated | June 16, 2026 |
| Repository | csc3213-2026-group-b/agent-skills ↗ |
What it does
Helps with ai & agent building tasks.
Files
SimpleWebAuthn Skill
Use this skill for any WebAuthn or passkey implementation built on SimpleWebAuthn. The docs in this folder are the source of truth:
Use this skill for WebAuthn/passkey implementation work based on the SimpleWebAuthn docs in this folder. The goal: give concise, actionable recipes the model can use when asked to implement or debug server and browser-side WebAuthn flows.
- Philosophy
Docs in this folder (source of truth):
- Intro
- Philosophy
- Server guide
- Browser guide
- Types
- Passkeys guidance
- Custom challenges
- Supported devices
- Browser quirks
- Example project
- @simplewebauthn/server
- @simplewebauthn/browser
Use this skill when code imports from @simplewebauthn/server or @simplewebauthn/browser. Use when user asks about "WebAuthn", "passkeys", "FIDO2", or "conditional create/UI".
- Custom Challenges
- Supported Devices
- Browser Quirks
- Example Project
Core Workflow
SimpleWebAuthn splits WebAuthn into two halves:
1. The server generates options and verifies responses. 2. The browser calls startRegistration() or startAuthentication() and forwards the authenticator response back to the server.
The library handles the JSON and ArrayBuffer conversion details for you. Prefer its helpers over hand-rolling WebAuthn encoding or decoding.
Registration
Use generateRegistrationOptions() to create options, then send them to startRegistration().
After the browser returns a response, verify it with verifyRegistrationResponse() and store the resulting credential data.
Persist at least:
- credential
id publicKeycountertransportscredentialDeviceTypecredentialBackedUp- the WebAuthn user ID used to create the credential
Use discoverable credentials (residentKey required/preferred) and user verification preferences to enable passkey UX. For passkey-first login, set allowCredentials: [] so the browser can present any discoverable credential. Store transports, credentialDeviceType, and credentialBackedUp to understand adoption and enable cross-device UX.
attestationType: 'none'authenticatorSelection.residentKey: 'preferred'for general passkey support, or'required'for discoverable credentials
Persist challenge between options generation and verification (session cookie, Redis keyed by sessionID). Delete the stored challenge after verification to prevent replay. For advanced uses, expectedChallenge can be a function (or async) that validates embedded data. If you need browser hints, preferredAuthenticatorType overrides authenticatorSelection.authenticatorAttachment.
Node crypto or algorithm errors: older Node runtimes can mishandle OKP/Ed25519; if necessary, exclude -8 from supportedAlgorithmIDs and re-register. If verification throws algorithm/OKP errors in Firefox, re-generate credentials without Ed25519. Use generateAuthenticationOptions() to create options, then pass them to startAuthentication().
Endpoint: GET /generate-registration-options Endpoint: POST /verify-registration Endpoint: GET /generate-authentication-options Endpoint: POST /verify-authentication Persistent store for challenges DB table for passkeys with id, publicKey, counter, transports, webauthnUserID For passkey-friendly flows, favor:
If you want narrower guidance (server-only, browser-only, or passkey-only), I can create separate SKILL.md files for simplewebauthn-server, simplewebauthn-browser, and simplewebauthn-passkeys that focus on their respective recipes and triggers.
- user verification when appropriate
- storing transports for future authentications
- handling multi-device credentials and backed-up credentials as first-class cases
On the server, passkeys usually mean a platform-authenticator or synced-credential flow, not a separate API.
Browser Guidance
@simplewebauthn/browser is the preferred front-end integration point.
Use:
startRegistration()for credential creationstartAuthentication()for sign-in
It also supports:
- conditional create / auto register
- browser autofill / conditional UI
- feature detection for WebAuthn support
Operational Notes
- WebAuthn requires a secure context.
localhostis acceptable for local development. - Multiple origins and multiple RP IDs can be verified by passing arrays to the server helpers.
- If you need custom challenge logic, use the
expectedChallengehook rather than bypassing verification. - Keep the challenge value stable between generation and verification and delete it after use to avoid replay.
Troubleshooting
Watch for environment-specific crypto issues, especially older Node versions and Ed25519 / OKP compatibility problems. If verification fails with algorithm support errors, exclude -8 from supportedAlgorithmIDs and re-register affected authenticators.
Implementation Default
When asked to build or debug a SimpleWebAuthn feature, prefer a minimal ceremony with:
- one endpoint to generate options
- one endpoint to verify responses
- persistent challenge storage
- durable passkey records in the database
- counter updates after successful authentication
If more detail is needed, consult the package docs above before proposing custom logic.
Safari
When adding support for WebAuthn, special considerations must be made for the Safari browser on both iOS and macOS:
- @simplewebauthn/browser's `startRegistration()` and `startAuthentication()` must be called in a native click listener (user gesture detection). Some JS UI libraries may not use native click handling without the use of framework-specific functionality.
- Safari only supports the use of XHR and
fetchwithin native click handlers for requesting WebAuthn registration and authentication options.fetchand XHR wrappers like ofetch or axios might cause problems with user gesture detection. To avoid problems with user gesture detection, it's best to have a single nativefetchor XHR call and not use any other async operations (promises, timeouts, ...) before callingstartRegistration()/startAuthentication(). - Websites viewed in Safari running in iOS 17.4 and macOS 14.4 and later are free to invoke WebAuthn as needed. There is an internal rate limiter within Safari to prevent abuse of WebAuthn, but "well-behaving" Relying Parties should no longer have user gesture requirements to contend with anymore (see below.)
- Versions of Safari running in iOS 17.3 and macOS 14.3 and earlier...
- ...may experience issues with session replay tools such as Sentry Session Replay or LogRocket causing problems with user gesture detection.
- ...allow websites to make one call to
navigator.credentials.create()ornavigator.credentials.get()(via `startRegistration()` or `startAuthentication()` respectively) for every browser navigation event without requiring a user gesture, e.g. to support Conditional UI. When using a Single-Page Application (SPA) this limit only gets reset after reloading the page.
Microsoft Edge
The Microsoft Edge browser refers to two different browsers: the original release from 2015 (now called "Microsoft Edge Legacy"), and the Chromium-based version from June 2020 that inherited the name "Microsoft Edge".
When adding support for WebAuthn, special considerations must be made for Microsoft Edge Legacy:
- The browser global `TextEncoder` is not supported. This means @simplewebauthn/browser will not work in this browser without a polyfill for this API. MDN includes a spec-compliant polyfill that can be copied into your project. Various browser polyfill libraries exist on NPM as well.
Firefox
WebAuthn responses from security keys that generate keypairs using Ed25519 (i.e. -8) can fail response verification during registration due to a bug in the browser itself. This can manifest as the following error message from @simplewebauthn/server methods:
Error: Leftover bytes detected while parsing authenticator dataThese responses may also fail to be verified by verifyRegistrationResponse(), even when the same server setup and security key are used in a different browser:
const verifiedFirefox = await verifyRegistrationResponse({ ... });
console.log(verifiedFirefox.verified); // falseThe issue was caused by a bug in authenticator-rs (mozilla/authenticator-rs#292), and according to Mozilla (Bugzilla Bug 1852812) should be resolved as of Firefox 119.
Introduction
What follows is a more in-depth look at the example project available in the repo. A single-file Express server and a few HTML files have been combined with the packages in this project to demonstrate what it takes to get up and running with WebAuthn. This is intended to be a practical reference for implementing the SimpleWebAuthn libraries to add WebAuthn-based Two-Factor Authentication (2FA) support to your website.
Before going any further, though, it's worth noting that the WebAuthn browser API by itself isn't very useful. Developers that want to leverage this API and these libraries are required to have a server with a few things already up and running:
- A stateful server capable of temporarily persisting values
- A database that can store information linked to individual users
_tip_: Don't fret if you don't already have a setup like this! The example project mocks out enough of this functionality to offer you a simple WebAuthn sandbox to play around with before you dive in further.
Getting started
The example server is a Node server, so you'll need the following available on your machine:
- Node.js
- Install the current LTS release if you're new to all of this
- Bun includes a package manager; use
bun install/bun addandbun runto manage dependencies and scripts
Downloading the code
First and foremost, get the SimpleWebAuthn code downloaded to your machine. You can click here to download a ZIP file containing a current snapshot of the codebase, or clone it with git:
$> git clone https://github.com/MasterKale/SimpleWebAuthn.gitAfter unzipping or cloning the codebase, cd to it in a terminal before continuing:
$> cd SimpleWebAuthn
./SimpleWebAuthn/ $>Installing dependencies
First, navigate to the example project directory:
./SimpleWebAuthn/ $> cd example
./example/ $>Next, install dependencies with bun:
./example/ $> bun installStarting the server
Once the two files above are in-place, you can start the server:
./example/ $> bun run startThe example server should now be available at http://localhost:8000!
Setting up HTTPS support
_info_: Setting up HTTPS support will enable you to access the example project from other devices on your intranet, including smartphones, so that you can test WebAuthn across multiple environments from the safety of your own network, no internet access needed!
</br>
_caution_: The following steps assume you own a custom domain and have full access to its DNS configuration. You must be able to create DNS A and CNAME records for your domain/subdomain to complete the steps below. For the steps below, replace dev.example.com with your own domain/subdomain. To clarify, this setup will _not_ expose this server to the internet.
</br>
_tip_: Below are a suggested list of steps to host the example project over HTTPS from a development machine. They are not the only way to accomplish this task, so feel free to deviate as you see fit. The end goal is what's important here, not necessarily how you get there.
WebAuthn must be run from a secure context, that is from either http://localhost or from an https:// address using a valid SSL certificate. While the example project works fine over localhost right out of the box, additional work is needed to get it running over HTTPS:
1. Determine your intranet IP address. This will likely be an address in the 192.168.1.\* range
2. Create an A record for dev.example.com in your domain's DNS configuration pointing to the IP address above.
3. Install EFF's certbot according to your local OS 1. Select "None of the Above" and then your OS to see instructions 2. Stop once you complete the "Install Certbot" step
4. Run the following certbot command and follow its instructions to generate SSL certificates for dev.example.com:
_info_: These SSL certificates are only good for 90 days. To renew your local SSL certificates after 90 days, simply re-run the command above.
$> sudo certbot --manual -d dev.example.com --preferred-challenges dns certonly5. Copy the resulting /etc/letsencrypt/live/dev.example.com/fullchain.pem to ./example/dev.example.com.crt
6. Copy the resulting /etc/letsencrypt/live/dev.example.com/privkey.pem to ./example/dev.example.com.key
7. Create a .env file and add the following environment variable:
```env title="example/.env" ENABLE_HTTPS=true
8. Open **index.ts** and update `rpID`:
const rpID = 'dev.example.com';
9. Once everything is in place, start the server:
./example $> bun run start
Assuming everything is in place, the server will then be accessible at [URL](https://dev.example.com).
Additionally, since the **dev.example.com** DNS A record points to your local machine's intranet IP address, any device connected to your intranet will be able to access the example project at [URL](https://dev.example.com) to test the device's support for WebAuthn.
Introduction
The FIDO® Alliance offers a suite of tests known as the "Conformance Self-Validation Testing". These tests enable developers to verify their Relying Party's (RP) adherence to FIDO specifications.
The WebAuthn API is built on top of FIDO2, and so implementations of WebAuthn can also become FIDO conformant. WebAuthn implementers that go this extra mile add an additional level of validation of the authenticators that interact with their RP.
As of v0.7.0, the @simplewebauthn/server package is FIDO conformant! Support for this additional level of authenticator scrutiny is opt-in - see usage instructions for `MetadataService` for more information.
Validating FIDO conformance
It is important that the SimpleWebAuthn project remains FIDO conformant. To enable others to validate conformance and keep the project honest, a step-by-step list of instructions for running FIDO conformance tests against the library are included below.
Downloading FIDO Conformance Tools
FIDO conformance testing requires downloading the FIDO Conformance Tools application. If you don't already have it, you can submit a download request by filling out the Test Tool Access Request form.
_note_: For the "FIDO Specification" dropdown, select "FIDO2"
After submitting the form, an email response will (eventually) arrive with a download link and username and password. Navigate to the link, enter the username and password, and then download and install the latest release version for your OS from the Desktop UAF FIDO2 U2F/ directory.
You can now verify FIDO conformance of SimpleWebAuthn by following these steps:
Setting up the Example Project
Follow the instructions in the Example Project's Getting Started section.
_important_: Make sure the example project is available at http://localhost:8000 before continuing!
Activating additional routes
Create a .env file and add the following environment variable:
```env title="example/.env" ENABLE_CONFORMANCE=true
This will add additional routes on the `/fido` route.
### Loading metadata statements
You will next need to load "metadata statements" from the FIDO Conformance Tools to ensure that all required tests pass.
Open up the FIDO Conformance Tools and click the **Run** button on the **FIDO2 Tests** card:
<img alt="FIDO2 Tests card" src={useBaseUrl('img/docs/fido-conformance/fido2-run.png')} />
Next, scroll down and click the **DOWNLOAD SERVER METADATA** button on the right-hand column, under **TESTS CONFIGURATION**:
<img alt="FIDO2 Metadata download button" src={useBaseUrl('img/docs/fido-conformance/metadata.png')} />
This will download a **metadata.zip** folder to your computer. Unzip the JSON files within and place them into the **example/fido-conformance-mds/** directory:
<img alt="Code editor showing placement of metadata JSON files" src={useBaseUrl('img/docs/fido-conformance/editor-metadata-json.png')} />
### Starting the server
Start the server once everything is in place:
./example/ $> bun run start
An API request will be triggered by the activation of the conformance routes that pulls in additional metadata information required for the **Metadata Service Tests**. When the example server is ready for testing, you should see the following console output:
🚀 Server ready at http://0.0.0.0:8000 ℹ️ Initializing metadata service with 20 local statements 🔐 FIDO Conformance routes ready
### Initiating conformance tests
To start conformance testing, open up the FIDO Conformance Tools and click the **Run** button on the **FIDO2 Tests** card:
<img alt="FIDO2 Tests card" src={useBaseUrl('img/docs/fido-conformance/fido2-run.png')} />
On the right-hand column, under **SELECT TESTS TO RUN**, check the box next to **Server Tests**:
<img alt="Showing selected FIDO2 server tests" src={useBaseUrl('img/docs/fido-conformance/server-tests.png')} />
Next, scroll down and look for the **Server URL** text box on the right-hand column, under **TESTS CONFIGURATION**:
<img alt="FIDO2 Metadata download button" src={useBaseUrl('img/docs/fido-conformance/metadata.png')} />
Enter the following URL into this text box:
> [URL](http://localhost:8000/fido)
It's finally time! Click the **Run** button on the bottom-right corner of the window to start conformance testing:
<img alt="FIDO2 Tests run button" src={useBaseUrl('img/docs/fido-conformance/run-button.png')} />
### Confirming results
When the tests are completed, results for **FIDO Conformance Tools v1.3.4** should look like this:
<img alt="FIDO2 test results showing 160 passes and zero failures in approximately 19 seconds" src={useBaseUrl('img/docs/fido-conformance/results.png')} />
## Troubleshooting
Below are errors you may see while trying to run tests, and potential solutions to them:
### "Failed to fetch"
You may see a series of "Failed to fetch" errors:
<img alt="FIDO2 test failed to fetch error" src={useBaseUrl('img/docs/fido-conformance/error-failed-to-fetch.png')} />
**Solution:** Make sure the server is available at [http://localhost:8000](http://localhost:8000), and that you've [activated the additional FIDO Conformance-specific routes](#activating-additional-routes).
### "Unexpected token < in JSON at position 0"
You may see a series of "Unexpected token" errors:
<img alt="FIDO2 test failed to fetch error" src={useBaseUrl('img/docs/fido-conformance/error-unexpected-token.png')} />
**Solution:** Make sure that you've [activated the additional FIDO Conformance-specific routes](#activating-additional-routes).
### "Unlisted aaguid...in TOC"
You may see a series of "Unlisted aaguid" errors:
<img alt="FIDO2 test failed to fetch error" src={useBaseUrl('img/docs/fido-conformance/error-unlisted-aaguid.png')} />
**Solution:** Make sure that you've [loaded the metadata statements](#loading-metadata-statements) from the FIDO Conformance Tools
Introduction
Passkeys represent a compelling WebAuthn-based alternative to the timeless combination of "password + second-factor" that we all suffer through.
Passkeys are phishing-resistant, are unique across every website, and can help users maintain account access after device loss.
Additionally, passkeys generated by the three main platform authenticator vendors (Apple, Google, and Microsoft) are automatically synchronized across a user's devices by their respective cloud account. That means there's finally an easy way for users to regain account access if they happen to lose or trade in their device. There's never been a better time to update your authentication to use WebAuthn.
The following options are ones you should set when calling SimpleWebAuthn's methods to ensure that your site is ready for passkeys.
Prerequisites
Below are the three platform authenticator vendors and known minimum versions of their software needed for full passkey support:
Apple: iOS 16, macOS 13 (Ventura, Safari-only)
Google: Android 9+
Microsoft: TBD
FIDO2 security keys are unaffected. They will continue to produce credentials that are hardware bound, and most already support discoverable credentials.
Note that "passkey support" is still "WebAuthn support". Passkeys do not represent a breaking change in how WebAuthn is invoked. Rather they are WebAuthn credentials that can have their private key material synchronized across devices outside the influence of the WebAuthn API. The options below are optimizations for those Relying Parties that want to aim for passkey support as they become more common.
Server
The high-level strategy here is to instruct the authenticator to do the following during registration and authentication:
1. Generate a discoverable credential. The authenticator must generate and internally store a credential mapped to (rpID + userID). 2. Perform user verification. The authenticator must provide two authentication factors within a single authenticator interaction.
generateRegistrationOptions()
import { generateRegistrationOptions } from '@simplewebauthn/server';
const options = await generateRegistrationOptions({
// ...
authenticatorSelection: {
// "Discoverable credentials" used to be called "resident keys". The
// old name persists in the options passed to `navigator.credentials.create()`.
residentKey: 'required',
userVerification: 'preferred',
},
});User verification is "preferred" here because it smooths out potential frictions if a user attempts to use passkeys on a device without a biometric sensor. See "A note about user verification" on passkeys.dev for more context. The actual enforcement of user verification being required for proper passwordless support happens below during response verification.
verifyRegistrationResponse()
const verification = await verifyRegistrationResponse({
// ...
requireUserVerification: false,
});caution A word of caution about user verification
requireUserVerificationis set tofalseabove because many websites can be just fine using passkeys without user verification! The phishing resistant properties of WebAuthn elevates passkeys to a higher level of protection than username+password+2fa, and thus passkeys are not necessarily beholden to the same "multiple factors of auth" rules that came before them.
However _some websites_, for various regulatory reasons, may require multiple factors of authentication to be provided. If you are a developer of such a website then you should set userVerification: 'required' when calling generateRegistrationOptions(), and specify requireUserVerification: true when calling verifyRegistrationResponse().
Make sure to save the transports value returned from @simplewebauthn/browser's startRegistration() method too. Advanced WebAuthn functionality like cross-device auth (i.e. authenticating into a website displayed in Chrome on Windows by using your iPhone) is hard to design good UX around. You can use the browser to figure out when it is available by including each credential's transports in the allowCredentials array passed later into generateAuthenticateOptions(). They will help the browser figure out when a credential might enable a user to log in using new technology that wasn't available before.
Signs that a passkey were created include the following values returned from this method:
verification.registrationInfo.credentialDeviceType:'multiDevice'means the credential will be backed up for use on multiple devices within the same platform vendor cloud ecosystem (e.g. only for use on Apple devices sharing an iCloud account)verification.registrationInfo.credentialBackedUp:truemeans the private key material has been backed up to the user's cloud account. For all intents and purposes this will always betruewhencredentialDeviceTypeabove is'multiDevice'
These values can be stored in the database for a given credential for later reference, to help with understanding rate of adoption of passkeys by your users and adjust your UX accordingly.
generateAuthenticationOptions()
const options = await generateAuthenticationOptions({
// ...
userVerification: 'preferred',
allowCredentials: [],
});userVerification is "preferred" here because it smooths out potential frictions if a user attempts to use passkeys on a device without a biometric sensor. See "A note about user verification" on passkeys.dev for more context. The actual enforcement of user verification being required for proper passwordless support happens below during response verification.
allowCredentials can also be set to [] to allow the user to choose from any discoverable credentials they have for the site when calling startAuthentication() after the user clicks a "Sign in with a passkey" button. This "flips the script" on the authentication ceremony by allowing the user to generate a WebAuthn response with a registered passkey, which then tells the RP which account the user wants to log into without the user needing to provide an account identifier beforehand! After verifying the response and confirming it recognizes the credential then the RP should create a session for the internal user record that is associated with the response's `id` (and/or `userHandle` when available.)
_info_: SettingallowCredentials: []when callinggenerateAuthenticationOptions()is OPTIONAL if you are using @simplewebauthn/browser'sstartAuthentication()method with its second positionaluseBrowserAutofillargument set totrue;startAuthentication()will take care of this for you in this configuration. See the Conditional UI section of the docs for `startAuthentication()` for more information.
verifyAuthenticationResponse()
const authVerify = await verifyAuthenticationResponse({
// ...
requireUserVerification: false,
});_caution_: A word of caution about user verification
requireUserVerificationis set tofalseabove because many websites can be just fine using passkeys without user verification! The phishing resistant properties of WebAuthn elevates passkeys to a higher level of protection than username+password+2fa, and thus passkeys are not necessarily beholden to the same "multiple factors of auth" rules that came before them. However _some websites_, for various regulatory reasons, may require multiple factors of authentication to be provided. If you are a developer of such a website then you should setuserVerification: 'required'when callinggenerateAuthenticationOptions(), and specifyrequireUserVerification: truewhen callingverifyAuthenticationResponse().
Remembering challenges
The generateRegistrationOptions() and generateAuthenticationOptions() methods both return a challenge value that will get signed by an authenticator and returned in the authenticator's response. The goal is for these challenge values to get passed back into the verifyRegistrationResponse() and verifyAuthenticationResponse() methods respectively as their expectedChallenge arguments.
The question then becomes, "how do I keep track of these challenges between creating the options and verifying the response when the user isn't yet logged in?" This is particularly tricky with passkeys and conditional UI. During this "usernameless" authentication the user is encouraged to present _any_ WebAuthn credential they possess for the site, instead of being told the the discrete list of credentials that they're able to use. If the user can't be known ahead of time, then how can the RP tell the user which credentials they can use for authentication?
_caution_: The following advice is high-level and avoids referencing specific frameworks and libraries (beyond SimpleWebAuthn) because every project is different. Please read the suggested course of action below, and then consider how it might be adapted to your project's architecture.
Authentication
One technique for tracking the challenge between options generation and response verification is to start a "session" for any user who views your login page.
First, assign a random sessionID HTTP-only cookie when the page first loads. When the user attempts to authenticate, call server's generateAuthenticationOptions() like normal and temporarily store the challenge somewhere (I like to use Redis' setex() and store challenges for five minutes [300000 ms]) with the sessionID as the key and options.challenge as the value. Return the options to the page and await a response.
When the response comes in, look up the challenge by the sessionID cookie and attempt verification, with challenge passed in as expectedChallenge to verifyAuthenticationResponse(). Make sure to delete the challenge so it can't be reused, even if verification fails, to prevent replay attacks! If the authentication response succeeds then log the user in.
Registration
New user registration is a bit unique in that it requires "bootstrapping" the creation of the user's session, ideally after verifying that the new user is not a bot. RP's are thus recommended to seek verification of ownership of a unique "point of contact" that the new user has signed up for outside of your service (the point of contact can also help with account recovery in case of device loss.)
A "magic link" sent to an email address, then, is a particularly simple solution that can perform double duty:
_info_: Magic Link Example
1. Receiving the magic link confirms that the user is signing up with a valid email account that they have access to, and to which you can send correspondence for account management 2. Clicking the magic link sends back the random, one-time "authorization code" that initiates a registration ceremony
Once the user clicks the link, you can reasonably assume that the user is not a bot and can therefore register an authenticator to their new account.
Challenge management during registration then looks very similar to the steps outlined above in the Authentication section, with one slight change: the "session" would be established just before calling generateRegistrationOptions(), after verifying that the authorization code in the URL is valid and hasn't been used before.
Browser
There isn't a whole lot that changes in how you call the browser methods if you want to support passkeys, as passkeys don't involve any changes in how WebAuthn is ultimately invoked.
startRegistration()
No changes are required.
...Unless you are interested in leveraging automatic passkey registration, a.k.a. "Conditional Create". Conditional Create will attempt to register a passkey with a user's password manager after a successful password authentication, assuming the password manager is also a passkey provider. This will allow the user to use a passkey instead for subsequent authentications.
See the Auto Register section of the docs for `startRegistration()` for more information.
startAuthentication()
No changes are required.
...Unless you are interested in leveraging browser autofill for passkey authentication, a.k.a. "Conditional UI". Conditional UI gives the browser a chance to find and suggest to the user credentials that they can select to then present to you for authentication, all via the browser's native autofill API.
If this interests you, then please see the Browser Autofill section of the docs for `startAuthentication()` as there is a bit of setup required to get it all working.
_danger_: Proceed with extreme caution. Use of WebAuthn'sprfextension dangerously ties vital encryption information to a user's passkey. If a user inadvertently deletes their passkey, they will lose all access to theirminformation that your website chose to encrypt with that passkey's PRF seed. There is nothing that can be done with SimpleWebAuthn to fix this problem if it occurs to you. If your use case can benefit from use of theprfextension then it is worthwhile to invest in learning the ins and outs of it directly from the WebAuthn spec, and to implement the logic yourself so that full liability rests on your shoulders. If you read the following and believe it to be an unsatisfactory explanation of how to use PRF then that is intentional. There are no plans to make PRF simpler to use using SimpleWebAuthn because of the footgun described above. Passkeys are an authentication technology first and foremost, and SimpleWebAuthn prioritizes simplifying these use cases over any other use of WebAuthn.
WebAuthn's pseudo-random function extension (prf) can be used to reliably request a sequence of sufficiently random bytes after a WebAuthn auth ceremony that are strongly associated to a user's passkey. Useful encryption use cases, like end-to-end encryption, can be driven by this "PRF seed."
The seed is generated from the combined hashing of server-controlled bytes (a.k.a. "salt") and authenticator-controlled bytes stored with the passkey private key. When hashed together, these bytes can become useful input for things like an "HMAC-based Key Derivation Function" (HKDF).
The prf extension inputs during registration and authentication are of type BufferSource in the WebAuthn spec. These are mapped to the ArrayBuffer type in JavaScript.
Server: Bytes to Base64URL
After generating options using @simplewebauthn/server's `generateRegistrationOptions()` or `generateAuthenticationOptions()`, the following helper can be imported to add the prf extension directly to the generated options. This will allow the salt to be sent to the browser as a base64url-encoded string along with the rest of the generated options:
import { isoBase64URL } from '@simplewebauthn/server/helpers';
const prfSaltBase64URL: string = isoBase64URL.fromBuffer(prfSaltBytes);Browser: Base64URL to Bytes
In the browser, you can then import the following helper from @simplewebauthn/browser to convert the salt back into an ArrayBuffer before passing the options with the prf extension into startRegistration() or startAuthentication():
import { base64URLStringToBuffer } from '@simplewebauthn/browser';
const prfSaltBytes: ArrayBuffer = base64URLStringToBuffer(prfSaltBase64URL);More Info
To learn more about how to use PRF, please consult the WebAuthn spec.
_caution_: The following functionality is opt-in and is not required for typical use! SimpleWebAuthn remains focused on simplifying working with the WebAuthn API, and the functionality covered in Packages > @simplewebauthn/server will serve the majority of developers' use cases.
Introduction
Some Relying Parties have highly specialized requirements that require greater control over how options challenges are generated, and how challenges in authenticator responses are verified. SimpleWebAuthn supports some of these use cases with the following capabilities:
Use a custom string for challenge
It is possible to specify custom strings for the challenge option when calling generateRegistrationOptions() and generateAuthenticationOptions(). These strings will be treated as UTF-8 bytes when preparing them for use as challenges:
import { generateRegistrationOptions } from '@simplewebauthn/server';
const options = await generateRegistrationOptions({
// ...
challenge: 'simplewebauthn',
});Authenticators will sign over the base64url-encoded challenge as per the WebAuthn spec. This must be accounted for when specifying the same custom string as the expectedChallenge option when calling verifyRegistrationResponse() and verifyAuthenticationResponse():
import { verifyRegistrationResponse } from '@simplewebauthn/server';
import { isoBase64URL } from '@simplewebauthn/server/helpers';
const verification = await verifyRegistrationResponse({
// ...
expectedChallenge: isoBase64URL.fromString('simplewebauthn'),
});Customize challenge verification logic with expectedChallenge
Pulling from URL
A small amount of arbitrary information can be signed over during registration and authentication by overloading the challenge value with a stringified complex value. Call generateRegistrationOptions() or generateAuthenticationOptions() like usual, then override the challenge value to add in additional data in a way that is suitable for the use case:
import { generateAuthenticationOptions } from '@simplewebauthn/server';
import { isoBase64URL } from '@simplewebauthn/server/helpers';
const options = await generateAuthenticationOptions({ ... });
// Remember the plain challenge
setCurrentChallenge(unauthenticatedSessionID, options.challenge);
// Add a simple amount of data to be signed using whatever
// data structure is most appropriate
options.challenge = isoBase64URL.fromString(JSON.stringify({
actualChallenge: options.challenge,
arbitraryData: 'arbitraryDataForSigning',
}));After the WebAuthn ceremony is completed, call verifyRegistrationResponse() or verifyAuthenticationResponse() and pass in a function for expectedChallenge that accepts a challenge string and returns a boolean. This will need to perform the reverse of the logic used above to ensure that "actualChallenge" is the expected challenge:
const expectedChallenge = getCurrentChallenge(unauthenticatedSessionID);
const verification = await verifyAuthenticationResponse({
// ...
expectedChallenge: (challenge: string) => {
const parsedChallenge = JSON.parse(base64url.decode(challenge));
return parsedChallenge.actualChallenge === expectedChallenge;
},
});_info_: expectedChallenge can also be an asynchronous function to support e.g. making a network request to retrieve data needed to complete challenge verification.Once the response is successfully verified then use the decodeClientDataJSON() helper to retrieve the arbitrary data:
import { decodeClientDataJSON } from '@simplewebauthn/server/helpers';
const { challenge } = decodeClientDataJSON(body.response.clientDataJSON);
const parsedChallenge = JSON.parse(base64url.decode(challenge));
console.log(parsedChallenge.arbitraryData); // 'arbitraryDataForSigning'_caution_: The following functionality is opt-in and is not required for typical use! SimpleWebAuthn remains focused on simplifying working with the WebAuthn API, and the functionality covered in Packages > @simplewebauthn/server will serve the majority of developers' use cases.
Introduction
Some Relying Parties may wish to use their own format of user IDs instead of allowing generateRegistrationOptions() to generate a unique, random, WebAuthn-specific identifier to maximize user privacy.
In SimpleWebAuthn v9 it was possible to use such string values directly by simply assigning them to userID when calling generateRegistrationOptions(). As of SimpleWebAuthn v10, though, RP's now must account for the library-orchestrated use of base64url string encoding to get user.id bytes to the browser during registration, and userHandle bytes to the server during authentication.
_caution_: This is not a guide on how to use e.g. a user's email as their WebAuthn ID. Values used foruserIDMUST NOT contain personally-identifying information (PII)! A value like"WA1234567890ABC"is okay, but a value like"iamkale@simplewebauthn.dev"is not!. If you don't want to worry about any of this then consider skipping the rest of this guide and revisiting the general guidance offered in Packages > @simplewebauthn/server.
Registration
Use the isoUint8Array helper to convert the custom user identifier UTF-8 string into a Uint8Array:
import { isoUint8Array } from '@simplewebauthn/server/helpers';
const options = generateRegistrationOptions({
// ...
userID: isoUint8Array.fromUTF8String('customUserIDHere'),
});The value options.user.id will be the base64url-encoded UTF-8 bytes. When passed into **@simplewebauthn/browser**'s `startRegistration()` the bytes will be decoded back to the raw UTF-8 bytes and passed to the authenticator.
Authentication
**@simplewebauthn/browser**'s `startAuthentication()` method will encode the raw credential.response.userHandle bytes out of the WebAuthn response to make it easy to send them to the back end.
Back on the server, the isoBase64URL helper can be used to convert userHandle back into a recognizable UTF-8 string:
import { isoBase64URL } from '@simplewebauthn/server/helpers';
const credential = await receiveFromBrowser();
console.log(
isoBase64URL.toUTF8String(credential.response.userhandle) // 'customUserIDHere'
);Error: String values for \userID\ are no longer supported
@simplewebauthn/server's `generateRegistrationOptions()` will throw this error when a string value is passed in for the userID argument. To fix the problem, review the Registration section above for guidance on refactoring your code to massage your string identifier into a Uint8Array.
_caution_: The following functionality is opt-in and is not required for typical use! SimpleWebAuthn remains focused on simplifying working with the WebAuthn API, and the functionality covered in Packages > @simplewebauthn/server will serve the majority of developers' use cases.
Introduction
Metadata statements maintained by the FIDO Alliance can be referenced during registration to cross-reference additional information about authenticators used with SimpleWebAuthn. These statements contain cryptographically-signed "guarantees" about authenticators and what they are capable of, according to their manufacturer.
@simplewebauthn/server includes support for the FIDO Alliance Metadata Service (version 3.0) API via its MetadataService:
import { MetadataService } from '@simplewebauthn/server';This singleton service contains all of the logic necessary to interact with the MDS API, including signed data verification and automatic periodic refreshing of metadata statements.
_info_: Use of MetadataService is _not_ required to use @simplewebauthn/server! This is opt-in functionality that enables a more strict adherence to FIDO specifications and may not be appropriate for your use case.
initialize()
Simply call initialize() to enable MetadataService configured to use the official MDS API:
import { MetadataService } from '@simplewebauthn/server';
MetadataService.initialize().then(() => {
console.log('🔐 MetadataService initialized');
});MetadataService can also be initialized with optional URLs to other MDS-compatible servers, any local metadata statements you may maintain, or both:
import { MetadataService, MetadataStatement } from '@simplewebauthn/server';
const statements: MetadataStatement[] = [];
// Load in statements from JSON files
try {
const mdsMetadataPath = './metadata-statements';
const mdsMetadataFilenames = fs.readdirSync(mdsMetadataPath);
for (const statementPath of mdsMetadataFilenames) {
if (statementPath.endsWith('.json')) {
const contents = fs.readFileSync(`${mdsMetadataPath}/${statementPath}`, 'utf-8');
statements.push(JSON.parse(contents));
}
}
} catch (err) {
// pass
}
MetadataService.initialize({
mdsServers: ['https://mds-compatible-server.example.com'],
statements: statements,
}).then(() => {
console.log('🔐 MetadataService initialized');
});Once MetadataService is initialized, verifyRegistrationResponse() will reference MDS metadata statements and error out if it receives authenticator responses with unexpected values.
_caution_: Make sure to setattestationTypeto"direct"when callinggenerateRegistrationOptions()to leverage the full power of metadata statements!
verifyMDSBlob()
Some projects that wish to use MetadataService may have restrictions preventing runtime network requests to FIDO MDS for fresh data. In these cases it becomes necessary to pull down the MDS data (a "blob" in MDS parlance), verify its integrity, then cache the metadata statements within for loading later as the statements argument when MetadataService is initialized.
To support these projects, the verifyMDSBlob() helper can be used to verify the integrity of an MDS blob and then extract its contents. These contents can then be cached and loaded into MetadataService without making any subsequent MDS-related network calls:
1. Manually cache MDS data
Use verifyMDSBlob() in a runtime that can make external network requests:
import { verifyMDSBlob } from '@simplewebauthn/server/helpers';
// A JWT downloaded from an MDS server (e.g. https://mds3.fidoalliance.org)
const blob: string = manuallyFetchMDSBlob();
// Network requests will be made here for things like CRL checks
const { statements, parsedNextUpdate, payload } = await verifyMDSBlob(blob);
// Store the array of JSON objects in whatever way is appropriate
await writeStatementsToDisk(statements);parsedNextUpdate is a parsed Date instance of the YYYY-MM-DD-formatted nextUpdate string in the MDS blob payload. You should use this to remind yourself to check for a new version of MDS data after that date.
payload is the raw MDS blob payload. This _may_ be useful to certain projects wanting to e.g. further filter the FIDO2 metadata statements by some arbitrary business logic.
2. Load cached data
Load the cached statements into MetadataService that is running in a runtime that cannot make external network requests:
import { MetadataService } from '@simplewebauthn/server';
// Read the stored array of JSON objects in whatever way is appropriate
const savedStatements = await readStatementsFromDisk();
// No network requests nor validation happen here because the statements
// are assumed trusted
await MetadataService.initialize({
// Do not query any MDS servers
mdsServers: [],
// Load the cached statements
statements: savedStatements,
});Introduction
If you've made it here then you probably know what Secure Payment Confirmation (SPC) is! If not, you can read more about it here: URL
SPC responses are almost identical to WebAuthn responses, save for a slightly different value in their type value within clientDataJSON. Fortunately it's easy to verify such SPC responses using @simplewebauthn/server.
Specify a custom expectedType
Secure Payment Confirmation requests can be supported by SimpleWebAuthn by setting the expectedType argument to "payment.get" when calling @simplewebauthn/server's verifyAuthenticationResponse():
import { verifyAuthenticationResponse } from '@simplewebauthn/server';
const authVerify = await verifyAuthenticationResponse({
// ...
expectedType: 'payment.get',
});If desired, a single call to verifyAuthenticationResponse() can support verification of both WebAuthn and Secure Payment Confirmation responses (i.e. output from @simplewebauthn/browser's startAuthentication() method) by specifying the following array of possible values:
import { verifyAuthenticationResponse } from '@simplewebauthn/server';
const authVerify = await verifyAuthenticationResponse({
// ...
expectedType: ['webauthn.get', 'payment.get'],
});_caution_: The following functionality is opt-in and is not required for typical use! SimpleWebAuthn remains focused on simplifying working with the WebAuthn API, and the functionality covered in Packages > @simplewebauthn/server will serve the majority of developers' use cases.
Introduction
The SettingsService singleton offers methods for customizing @simplewebauthn/server's functionality.
setRootCertificates()
Some registration response attestation statements can be validated via root certificates prescribed by the company responsible for the format. It is possible to use SettingsService to register custom root certificates that will be used for validating certificate paths in subsequent registrations with matching attestation formats:
import { SettingsService } from '@simplewebauthn/server';
// A Uint8Array, or PEM-formatted certificate string
const appleCustomRootCert: Uint8Array | string = '...';
SettingsService.setRootCertificates({
identifier: 'apple',
certificates: [appleCustomRootCert],
});The following values for identifier are supported:
"android-key" | "android-safetynet" | "apple" | "fido-u2f" | "packed" | "tpm" | "mds"If root certificates have not been registered for an attestation statement format (or you set an empty array to one [e.g. []]) then certificate path validation will not occur.
_info_: This method can come in handy when an attestation format requires use of a root certificate that SimpleWebAuthn has not yet been updated to use.
SimpleWebAuthn includes known root certificates for the following such attestation formats:
"android-key""android-safetynet""apple""mds"(for use withMetadataServiceto validate MDS BLOBs)
getRootCertificates()
This method returns existing root certificates for a specific identifier:
import { SettingsService } from '@simplewebauthn/server';
const appleCerts: string[] = SettingsService.getRootCertificates({ identifier: 'apple' });The returned certificates will be PEM-formatted strings.
The excellent passkeys.dev maintains a comprehensive list of devices, operating systems, and browsers that support WebAuthn and, by extension, passkeys. Check it out here:
Current version
The content below should be accurate for @simplewebauthn/browser@^13.0.0.
Installation
This package can be installed from [NPM](https://www.npmjs.com/package/@simplewebauthn/browser) and [JSR](https://jsr.io/@simplewebauthn/browser):
Node LTS 20.x and higher
bun add @simplewebauthn/browserBun v1.0 and higher
bun add @simplewebauthn/browserDeno v1.43 and higher
deno add jsr:@simplewebauthn/browserUMD
This package can also be installed via unpkg by including the following script in your page's <head> element. The library's methods will be available on the global `SimpleWebAuthnBrowser` object.
_info_: The only difference between the two packages below is that the ES5 bundle includes some polyfills for older browsers. This adds some bundle size overhead, but does enable use of browserSupportsWebAuthn() in older browsers to show appropriate UI when WebAuthn is unavailable.</br>
_warning_: Use a subresource integrity checksum
If you are using a UMD bundle in production, it is highly recommend that you...
1. Visit the URL in either <script> tag below to get the exact-version URL that it redirects you to. 2. Enter that versioned URL into the SRI Hash Generator to create a version of that script tag that includes a subresource integrity checksum, to ensure you are always getting the exact, unmodified version of that file that you requested.
ES2021
If you only need to support modern browsers, include the ES2021 version:
<script src="https://unpkg.com/@simplewebauthn/browser/dist/bundle/index.umd.min.js"></script>ES5
If you need to support WebAuthn feature detection in deprecated browsers like IE11 and Edge Legacy, include the ES5 version:
<script src="https://unpkg.com/@simplewebauthn/browser/dist/bundle/index.es5.umd.min.js"></script>Types
This package exports almost all of its types for TypeScript projects to import. For example:
import type { WebAuthnCredential } from '@simplewebauthn/browser';import { ..., type WebAuthnCredential } from '@simplewebauthn/browser';See the auto-generated API docs for this project on JSR for a comprehensive list of available imports.
Methods
The following methods are exported from @simplewebauthn/browser:
startRegistration()
"Registration" is analogous to new account creation. The front end uses the following methods from this package to accomplish this:
import { startRegistration } from '@simplewebauthn/browser';The front end's primary job during registration is the following:
1. Get registration options from the Relying Party (your server)
- See @simplewebauthn/server's `generateRegistrationOptions()`
2. Submit registration options to the authenticator 3. Submit the authenticator's response to the Relying Party for verification
- See @simplewebauthn/server's `verifyRegistrationResponse()`
Below is all of the front end JavaScript needed to fulfill these three steps using this package:
_info_: The code below is a basic implementation written in plain JavaScript for placement in a plain HTML document. @simplewebauthn/browser is installed following the "UMD" installation method mentioned above.
That said, this general sequence of events should be easily adaptable to the front end framework of your choice (React/VueJS/Svelte/etc...) for use in projects that follow the above bun add / bun install installation method.
<script>
const { startRegistration } = SimpleWebAuthnBrowser;
// <button>
const elemBegin = document.getElementById('btnBegin');
// <span>/<p>/etc...
const elemSuccess = document.getElementById('success');
// <span>/<p>/etc...
const elemError = document.getElementById('error');
// Start registration when the user clicks a button
elemBegin.addEventListener('click', async () => {
// Reset success/error messages
elemSuccess.innerHTML = '';
elemError.innerHTML = '';
// GET registration options from the endpoint that calls
// @simplewebauthn/server -> generateRegistrationOptions()
const resp = await fetch('/generate-registration-options');
const optionsJSON = await resp.json();
let attResp;
try {
// Pass the options to the authenticator and wait for a response
attResp = await startRegistration({ optionsJSON });
} catch (error) {
// Some basic error handling
if (error.name === 'InvalidStateError') {
elemError.innerText =
'Error: Authenticator was probably already registered by user';
} else {
elemError.innerText = error;
}
throw error;
}
// POST the response to the endpoint that calls
// @simplewebauthn/server -> verifyRegistrationResponse()
const verificationResp = await fetch('/verify-registration', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(attResp),
});
// Wait for the results of verification
const verificationJSON = await verificationResp.json();
// Show UI appropriate for the `verified` status
if (verificationJSON && verificationJSON.verified) {
elemSuccess.innerHTML = 'Success!';
} else {
elemError.innerHTML = `Oh no, something went wrong! Response: <pre>${JSON.stringify(
verificationJSON
)}</pre>`;
}
});
</script>Auto Register (Conditional Create)
In supported browsers, calling startRegistration({ ..., useAutoRegister: true }) after a successful non-passkey authentication can trigger the silent creation of a passkey. If the password manager that was just used for password autofill is also a passkey provider then the browser will attempt to work with it to register a passkey without any modal UI shown to the user:
<!-- completedPasswordLogin.html -->
<body>
<!-- Typical landing page the user ends up on after login goes here -->
<script>
const { startRegistration } = SimpleWebAuthnBrowser;
/**
* Get options then call `startRegistration()`. Errors should be ignored
* as auto upgrade is considered "opportunistic" and can fail more easily
* than other such WebAuthn calls if the browser is not satisfied that all
* requirements have been met.
*/
fetch('/generate-registration-options')
.then((resp) => resp.json())
.then((optionsJSON) => {
// Note the `useAutoRegister: true` argument here
startRegistration({ optionsJSON, useAutoRegister: true })
.then((regResp) => sendToServerForVerification)
.catch((err) => handleError);
});
</script>
</body>If successful, startRegistration({ ..., useAutoRegister: true }) will resolve with a typical registration response.
_info_: Theup(User Presence) bit in this response is likely to be false! Be sure to specifyrequireUserPresence: falsewhen passing this response into @simplewebauthn/server's `verifyRegistrationResponse()` to account for this.
Successful use of Auto Register can be considered "upgrading" a user to be able to use their new passkey for subsequent authentications.
startAuthentication()
"Authentication" is analogous to existing account login. Authentication in the front end uses the following methods from this package:
import { startAuthentication } from '@simplewebauthn/browser';The front end's primary job during authentication is the following:
1. Get authentication options from the Relying Party (your server)
- See @simplewebauthn/server's `generateAuthenticationOptions()`
2. Submit authentication options to the authenticator 3. Submit the authenticator's response to the Relying Party for verification
- See @simplewebauthn/server's `verifyAuthenticationResponse()`
Below is all of the front end JavaScript that is needed to fulfill these three steps using this package:
_info_: The code below is a basic implementation written in plain JavaScript for placement in a plain HTML document. @simplewebauthn/browser is installed following the "UMD" installation method mentioned above.
That said, this general sequence of events should be easily adaptable to the front end framework of your choice (React/VueJS/Svelte/etc...) for use in projects that follow the above bun add / bun install installation method.
<script>
const { startAuthentication } = SimpleWebAuthnBrowser;
// <button>
const elemBegin = document.getElementById('btnBegin');
// <span>/<p>/etc...
const elemSuccess = document.getElementById('success');
// <span>/<p>/etc...
const elemError = document.getElementById('error');
// Start authentication when the user clicks a button
elemBegin.addEventListener('click', async () => {
// Reset success/error messages
elemSuccess.innerHTML = '';
elemError.innerHTML = '';
// GET authentication options from the endpoint that calls
// @simplewebauthn/server -> generateAuthenticationOptions()
const resp = await fetch('/generate-authentication-options');
const optionsJSON = await resp.json();
let asseResp;
try {
// Pass the options to the authenticator and wait for a response
asseResp = await startAuthentication({ optionsJSON });
} catch (error) {
// Some basic error handling
elemError.innerText = error;
throw error;
}
// POST the response to the endpoint that calls
// @simplewebauthn/server -> verifyAuthenticationResponse()
const verificationResp = await fetch('/verify-authentication', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(asseResp),
});
// Wait for the results of verification
const verificationJSON = await verificationResp.json();
// Show UI appropriate for the `verified` status
if (verificationJSON && verificationJSON.verified) {
elemSuccess.innerHTML = 'Success!';
} else {
elemError.innerHTML = `Oh no, something went wrong! Response: <pre>${JSON.stringify(
verificationJSON
)}</pre>`;
}
});
</script>Browser Autofill (Conditional UI)
Calling startAuthentication({ ..., useBrowserAutofill: true }) will set up support for credential selection via the browser's native autofill popup:
<head>
<!-- ... -->
<script>
const { startAuthentication } = SimpleWebAuthnBrowser;
fetch('/generate-authentication-options')
.then((resp) => resp.json())
.then((optionsJSON) => {
// Note the `useBrowserAutofill: true` argument here
startAuthentication({ optionsJSON, useBrowserAutofill: true })
.then((authResp) => sendToServerForVerificationAndLogin)
.catch((err) => handleError);
});
</script>
</head>
<body>
<!-- ... -->
<label for="username">Username</label>
<input type="text" name="username" autocomplete="webauthn" />
</body>_info_: The"webauthn"value in theautocompleteattribute is required for autofill to work.startAuthentication()will error out if an<input>with this autocomplete value cannot be found on the page.
"webauthn" can be combined with other typical autocomplete values, including "username" and "current-password", but must appear at the end to consistently trigger conditional UI across browsers. The following are all valid:
autocomplete="webauthn"
autocomplete="username webauthn"
autocomplete="current-password webauthn"_info_: If you absolutely know that a suitable<input>element as described above exists somewhere in the DOM (e.g. inside a web component's shadow DOM), butstartAuthentication()raises an error that such an element cannot be found, then you can also specifyverifyBrowserAutofillInput: falsewhen calling this method to bypass the error:
startAuthentication({
optionsJSON,
useBrowserAutofill: true,
verifyBrowserAutofillInput: false,
})
.then(...)_caution_: Guidance from platform vendors indicates that it is important to initialize WebAuthn's Conditional UI experience as soon as possible. Placing this logic in <head> should give browsers enough time to query authenticators for any discoverable credentials to display to the user. It may also work to delay painting UI For N milliseconds to achieve the same thing in something like a single-page app that would trigger this experience some time after page load. It's still early days for this capability, though, and different browsers may have different quirks while the feature evolves.This new "pending" WebAuthn request will be automatically cancelled on a subsequent execution of startAuthentication() in, for example, a click handler that triggers the browser's typical "modal" WebAuthn experience.
Conditional UI is still a nascent capability, but this method should be pretty reliable since the API is largely settled. This new logic has been successfully tested in both Chrome Canary v103 and Safari 16.0 in the macOS Ventura beta.
When supported, users can quickly authenticate by selecting a credential when interacting with the <input>:
Chrome Canary v103:
<img alt="Conditional UI prompt in Chrome Canary v103" src={useBaseUrl('img/docs/conditional-ui/chrome-canary-103.png')} />
Safari 16.0:
<img alt="Conditional UI prompt in Chrome Canary v103" src={useBaseUrl('img/docs/conditional-ui/safari-macos-ventura.jpg')} />
The selection of a credential from this popup will resolve the Promise returned by startAuthentication(), at which point the response can be submitted to the server for verification like usual. If verification succeeds, then the user can be logged in.
browserSupportsWebAuthn()
This helper method is included in this package to help preemptively check for the browser's ability to make WebAuthn API calls:
import { browserSupportsWebAuthn } from '@simplewebauthn/browser';startRegistration() and startAuthentication() both call this method internally. In some scenarios, though, it may be more desirable to hide UI when the page loads and a call to browserSupportsWebAuthn() returns false:
<script>
const elemBegin = document.getElementById('btnBegin');
const elemSuccess = document.getElementById('success');
const elemError = document.getElementById('error');
const { browserSupportsWebAuthn } = SimpleWebAuthnBrowser;
if (!browserSupportsWebAuthn()) {
elemBegin.style.display = 'none';
elemError.innerText = 'It seems this browser does not support WebAuthn...';
return;
}
// ...snip...
</script>browserSupportsWebAuthnAutofill()
This helper method checks for a browser's support for "Conditional UI". When this feature is present, the browser is capable of presenting a list of the user's discoverable credentials in the browser's native autofill prompt.
import { browserSupportsWebAuthnAutofill } from '@simplewebauthn/browser';This method is automatically called by startAuthentication() when true is passed as a second argument, but it may still be independently useful in, for example, single-page applications that may seek to initiate authentication after page load.
platformAuthenticatorIsAvailable()
"Platform authenticators" are known by most users by their brand names: Touch ID, Face ID, Windows Hello...this asynchronous method helps you identify opportunities in which these types of authenticators can be used by your users:
import { platformAuthenticatorIsAvailable } from '@simplewebauthn/browser';These advanced types of authenticators are typically embedded into a user's computer or phone and offer quick confirmation of a user's identity via biometric scan or PIN fallback. As a result of their convenience, you may wish to prioritize user registration of such authenticators when one is available for use:
<script>
const { platformAuthenticatorIsAvailable } = SimpleWebAuthnBrowser;
(async () => {
if (await platformAuthenticatorIsAvailable()) {
/**
* Prompt the user to use Touch ID/Windows Hello/etc... or security keys to register or
* authenticate
*
* How to decide which name to show for the device's platform authenticator is an exercise
* left up to the developer (your best bet is User Agent analysis)
*/
} else {
/**
* Only prompt the user to use security keys to register or authenticate
*/
}
})();
</script>Helpers
Below are various methods and classes intended to help Relying Parties work with this library. These methods are not typically needed when using this library, but they have been made available nonetheless for those projects that may need it.
base64URLStringToBuffer()
This method helps convert a base64url-encoded string into a Uint8Array:
import { base64URLStringToBuffer } from '@simplewebauthn/browser';
const valueBytes: ArrayBuffer = base64URLStringToBuffer('...');bufferToBase64URLString()
This method helps convert a Uint8Array into a base64url-encoded string:
import { bufferToBase64URLString } from '@simplewebauthn/browser';
const valueBase64URL: string = bufferToBase64URLString(new Uint8Array(...));WebAuthnAbortService
startRegistration() and startAuthentication() coordinate cancellation of existing WebAuthn calls via an internal WebAuthnAbortService class singleton. Whenever either of these methods is called, any preceding call to WebAuthn triggered by this library is automatically cancelled to avoid issues with browsers that may object to a page attempting to make multiple, simultaneous calls to WebAuthn.
However, this singleton has been exposed as a helper to enable certain types of projects, like Single-Page Applications (SPA), to manually trigger cancellation of a pending WebAuthn request as needed (e.g. client-side routing away from a login page needs to cancel any pending WebAuthn calls):
import {
startAuthentication,
WebAuthnAbortService,
WebAuthnError,
} from '@simplewebauthn/browser';
// On mount, the component triggers conditional UI
useEffect(() => {
try {
const response = await startAuthentication({ ..., useBrowserAutofill: true });
} catch (err) {
if (err instanceof WebAuthnError && err.code === 'ERROR_CEREMONY_ABORTED') {
// Error can safely be ignored
} else {
// Something else went wrong
}
}
}, []);
function handleLeavePage() {
// Cancel the pending WebAuthn request before leaving the login page
WebAuthnAbortService.cancelCeremony();
}WebAuthnError
The WebAuthnError class is used to wrap errors that are raised when calling WebAuthn via startRegistration() and startAuthentication(). Projects can import this error to perform instanceof checks when catching errors to discern SimpleWebAuthn library errors from other errors that may arise.
An instance of this error has the same name property as the WebAuthn error it is wrapping, but has a custom message property that tries to better communicate what may have caused the error. A corresponding code property is also set on it to programmatically work with library-identified error causes.
Finally, the cause (MDN) property is set to the original Error raised by the call to navigator.credentials.create() or navigator.credentials.get() so that Relying Party developers can directly access the raw WebAuthn error.
import { startRegistration, WebAuthnError } from '@simplewebauthn/browser';
try {
const response = await startRegistration({ ... });
} catch (err) {
if (err instanceof WebAuthnError) {
// See `name`, `message`, `code`, and `cause` for more info
} else {
// Something else went wrong
}
}Troubleshooting
startRegistration() unexpectedly errors out with NotAllowedError after scanning QR code
When a call to startRegistration() results in scanning a QR code with a mobile device, scanning the QR code displayed in Google Chrome may result in a NotAllowedError in the browser and an unexpected error on the mobile device:
NotAllowedError: The operation either timed out or was not allowed. See: https://www.w3.org/TR/webauthn-2/#sctn-privacy-considerations-client.
at new r (index.es5.umd.min.js:2:3304)
at index.es5.umd.min.js:2:8932
at index.es5.umd.min.js:2:10161
at index.es5.umd.min.js:2:1934
at Object.throw (index.es5.umd.min.js:2:2039)
at s (index.es5.umd.min.js:2:808)
Caused by: DOMException: The operation either timed out or was not allowed. See: https://www.w3.org/TR/webauthn-2/#sctn-privacy-considerations-client.This can be caused by an empty value string value (i.e. "") for user.displayName in the options passed to startRegistration().
To fix this, set user.displayName to the same value as user.name and try again. If you are using @simplewebauthn/servers's generateRegistrationOptions() method then you can set the userDisplayName argument to the same value as the userName argument to achieve the same result.
This is confirmed Chrome-specific bug that affects Chrome up through 127. A bug report for this issue has been logged as https://issues.chromium.org/issues/346835891. These docs will be updated when this issue is resolved and a new version of Chrome improves this behavior.
TypeError: Cannot read properties of undefined (reading 'challenge')
As of SimpleWebAuthn v11.0.0 both the startRegistration() and startAuthentication() accept a single object argument, with various options specified as properties in that argument. If you see this error, or were otherwise directed here after calling either of these methods, it is likely caused by passing in options directly (the old way), without wrapping them in an object and specifying them as optionsJSON as expected.
To fix this, update method calls to pass in options within an object instead:
// Before
const response = await startRegistration(options, ...);
// After
const response = await startRegistration({ optionsJSON: options, ... });// Before
const response = await startAuthentication(options, ...);
// After
const response = await startAuthentication({ optionsJSON: options, ... });import useBaseUrl from '@docusaurus/useBaseUrl';
Current version
The content below should be accurate for @simplewebauthn/server@^13.0.0.
Installation
This package can be installed from [NPM](https://www.npmjs.com/package/@simplewebauthn/server) and [JSR](https://jsr.io/@simplewebauthn/server):
Node LTS 20.x and higher
bun add @simplewebauthn/serverBun v1.0 and higher
bun add @simplewebauthn/serverDeno v1.43 and higher
deno add jsr:@simplewebauthn/serverTypes
This package exports almost all of its types for TypeScript projects to import. For example:
import type { WebAuthnCredential } from '@simplewebauthn/server';import { ..., type WebAuthnCredential } from '@simplewebauthn/server';See the auto-generated API docs for this project on JSR for a comprehensive list of available imports.
Additional data structures
Documentation below will refer to the following TypeScript types. These are intended to be inspirational, a simple means of communicating the shape of the values you'll need to be capable of persisting in your database:
import type {
AuthenticatorTransportFuture,
CredentialDeviceType,
Base64URLString,
} from '@simplewebauthn/server';
type UserModel = {
id: any;
username: string;
};
/**
* It is strongly advised that credentials get their own DB
* table, ideally with a foreign key somewhere connecting it
* to a specific UserModel.
*
* "SQL" tags below are suggestions for column data types and
* how best to store data received during registration for use
* in subsequent authentications.
*/
type Passkey = {
// SQL: Store as `TEXT`. Index this column
id: Base64URLString;
// SQL: Store raw bytes as `BYTEA`/`BLOB`/etc...
// Caution: Node ORM's may map this to a Buffer on retrieval,
// convert to Uint8Array as necessary
publicKey: Uint8Array;
// SQL: Foreign Key to an instance of your internal user model
user: UserModel;
// SQL: Store as `TEXT`. Index this column. A UNIQUE constraint on
// (webAuthnUserID + user) also achieves maximum user privacy
webauthnUserID: Base64URLString;
// SQL: Consider `BIGINT` since some authenticators return atomic timestamps as counters
counter: number;
// SQL: `VARCHAR(32)` or similar, longest possible value is currently 12 characters
// Ex: 'singleDevice' | 'multiDevice'
deviceType: CredentialDeviceType;
// SQL: `BOOL` or whatever similar type is supported
backedUp: boolean;
// SQL: `VARCHAR(255)` and store string array as a CSV string
// Ex: ['ble' | 'cable' | 'hybrid' | 'internal' | 'nfc' | 'smart-card' | 'usb']
transports?: AuthenticatorTransportFuture[];
};Below is a sample database schema showing how a passkeys table can track WebAuthn-specific user IDs while still allowing use of typical internal database user IDs through the rest of the site (click here for an interactive version of the schema):
<img alt="FIDO2 Metadata download button" src={useBaseUrl('img/docs/packages/server-sample-db-schema.png')} />
Keep this table structure in mind as you proceed through the following sections.
Identifying your RP
Start by defining some constants that describe your "Relying Party" (RP) server to authenticators:
/**
* Human-readable title for your website
*/
const rpName = 'SimpleWebAuthn Example';
/**
* A unique identifier for your website. 'localhost' is okay for
* local dev
*/
const rpID = 'simplewebauthn.dev';
/**
* The URL at which registrations and authentications should occur.
* 'http://localhost' and 'http://localhost:PORT' are also valid.
* Do NOT include any trailing /
*/
const origin = `https://${rpID}`;These will be referenced throughout registrations and authentications to ensure that authenticators generate and return credentials specifically for your server.
Registration
"Registration" is analogous to new account creation. Registration uses the following exported methods from this package:
import {
generateRegistrationOptions,
verifyRegistrationResponse,
} from '@simplewebauthn/server';Registration occurs in two steps:
1. Generate registration options for the browser to pass to a supported authenticator 2. Verify the authenticator's response
Each of these steps need their own API endpoints:
1. Generate registration options
One endpoint (GET) needs to return the result of a call to generateRegistrationOptions():
// (Pseudocode) Retrieve the user from the database
// after they've logged in
const user: UserModel = getUserFromDB(loggedInUserId);
// (Pseudocode) Retrieve any of the user's previously-
// registered authenticators
const userPasskeys: Passkey[] = getUserPasskeys(user);
const options: PublicKeyCredentialCreationOptionsJSON =
await generateRegistrationOptions({
rpName,
rpID,
userName: user.username,
// Don't prompt users for additional information about the authenticator
// (Recommended for smoother UX)
attestationType: 'none',
// Prevent users from re-registering existing authenticators
excludeCredentials: userPasskeys.map((passkey) => ({
id: passkey.id,
// Optional
transports: passkey.transports,
})),
// See "Guiding use of authenticators via authenticatorSelection" below
authenticatorSelection: {
// Defaults
residentKey: 'preferred',
userVerification: 'preferred',
// Optional
authenticatorAttachment: 'platform',
},
});
// (Pseudocode) Remember these options for the user
setCurrentRegistrationOptions(user, options);
return options;These options can be passed directly into **@simplewebauthn/browser**'s `startRegistration()` method.
Guiding use of authenticators via authenticatorSelection
generateRegistrationOptions() also accepts an authenticatorSelection option that can be used to fine-tune the registration experience. When unspecified, defaults are provided according to passkeys best practices. These values can be overridden based on Relying Party needs, however:
`authenticatorSelection.residentKey`:
'discouraged'- Won't consume discoverable credential slots on security keys, but also won't generate synced passkeys on Android devices.
'preferred'- Will always generate synced passkeys on Android devices, but will consume discoverable credential slots on security keys.
'required'- Same as
'preferred'
`authenticatorSelection.userVerification`:
'discouraged'- Won't perform user verification if interacting with an authenticator won't automatically perform it (i.e. security keys won't prompt for PIN, but interacting with Touch ID on a macOS device will always perform user verification.) User verification will usually be
false. 'preferred'- Will perform user verification when possible, but will skip any prompts for PIN or local login password when possible. In these instances user verification can sometimes be
false. 'required'- Will always provides multi-factor authentication, at the expense of always requiring some users to enter their local login password during auth. User verification should never be
false.
`authenticatorSelection.authenticatorAttachment`:
'cross-platform'- Browsers will guide users towards registering a security key, or mobile device via hybrid registration.
'platform'- Browser will guide users to registering the locally integrated hardware authenticator.
Fine-tuning the registration experience with preferredAuthenticatorType
WebAuthn hints allow a Relying Party to further refine the registration experience compared to specifying a value for authenticatorSelection.authenticatorAttachment as detailed above. SimpleWebAuthn enables use of hints via the preferredAuthenticatorType argument that can be passed into generateRegistrationOptions():
`preferredAuthenticatorType`:
'securityKey'- A security key, like a YubiKey 5, Feitian K40, and other such FIDO2-compatible USB tokens
'localDevice'- Typically the platform authenticator available on the device that they are logging in from
'remoteDevice'- A platform authenticator with access to a valid passkey but that is not on the device the user is logging in from (a.k.a. "hybrid auth" a.k.a. "the one that shows a QR code")
When this value is specified, browsers that support hints will try to tailor their experience to encourage registration of an authenticator of the specified type. Hints are a suggestion, though, and browsers will often allow a user to ultimately choose a different type of authenticator to register. RPs should be prepared for this possibility when using this.
_important_: Setting a value forpreferredAuthenticatorTypewill overwrite any value that may have been specified forauthenticatorSelection.authenticatorAttachment! This is to help maintain backwards compatibility with browsers that may not yet know about hints.
1a. Supported Attestation Formats
If attestationType is set to "direct" when generating registration options, the authenticator will return a more complex response containing an "attestation statement". This statement includes additional verifiable information about the authenticator.
Attestation statements are returned in one of several different formats. SimpleWebAuthn supports all current WebAuthn attestation formats, including:
- Packed
- TPM
- Android Key
- Android SafetyNet
- Apple
- FIDO U2F
- None
_info_: Attestation statements are an advanced aspect of WebAuthn. You can ignore this concept if you're not particular about the kinds of authenticators your users can use for registration and authentication.
2. Verify registration response
The second endpoint (POST) should accept the value returned by **@simplewebauthn/browser**'s `startRegistration()` method and then verify it:
const { body } = req;
// (Pseudocode) Retrieve the logged-in user
const user: UserModel = getUserFromDB(loggedInUserId);
// (Pseudocode) Get `options.challenge` that was saved above
const currentOptions: PublicKeyCredentialCreationOptionsJSON =
getCurrentRegistrationOptions(user);
let verification;
try {
verification = await verifyRegistrationResponse({
response: body,
expectedChallenge: currentOptions.challenge,
expectedOrigin: origin,
expectedRPID: rpID,
});
} catch (error) {
console.error(error);
return res.status(400).send({ error: error.message });
}
const { verified } = verification;_tip_: Support for multiple origins and RP IDs
SimpleWebAuthn optionally supports verifying registrations from multiple origins and RP IDs! Simply pass in an array of possible origins and IDs forexpectedOriginandexpectedRPIDrespectively.
When finished, it's a good idea to return the verification status to the browser to display appropriate UI:
return { verified };3. Post-registration responsibilities
Assuming verification.verified is true then RP's must, at the very least, save the credential data in registrationInfo to the database:
const { registrationInfo } = verification;
const { credential, credentialDeviceType, credentialBackedUp } =
registrationInfo;
const newPasskey: Passkey = {
// `user` here is from Step 2
user,
// Created by `generateRegistrationOptions()` in Step 1
webAuthnUserID: currentOptions.user.id,
// A unique identifier for the credential
id: credential.id,
// The public key bytes, used for subsequent authentication signature verification
publicKey: credential.publicKey,
// The number of times the authenticator has been used on this site so far
counter: credential.counter,
// How the browser can talk with this credential's authenticator
transports: credential.transports,
// Whether the passkey is single-device or multi-device
deviceType: credentialDeviceType,
// Whether the passkey has been backed up in some way
backedUp: credentialBackedUp,
};
// (Pseudocode) Save the authenticator info so that we can
// get it by user ID later
saveNewPasskeyInDB(newPasskey);_info_: Regarding counter, Tracking the "signature counter" allows Relying Parties to potentially identify misbehaving authenticators, or cloned authenticators. The counter on subsequent authentications should only ever increment; if your stored counter is greater than zero, and a subsequent authentication response's counter is the same or lower, then perhaps the authenticator just used to authenticate is in a compromised state.It's also not unexpected for certain high profile authenticators, like Touch ID on macOS, to always return 0 (zero) for the signature counter. In this case there is nothing an RP can really do to detect a cloned authenticator, especially in the context of multi-device credentials.
@simplewebauthn/server knows how to properly check the signature counter on subsequent authentications. RP's should only need to remember to store the value after registration, and then feed it back into startAuthentication() when the user attempts a subsequent login. RP's should remember to update the credential's counter value in the database afterwards. See Post-authentication responsibilities below for how to do so.
Authentication
"Authentication" is analogous to existing account login. Authentication uses the following exported methods from this package:
import {
generateAuthenticationOptions,
verifyAuthenticationResponse,
} from '@simplewebauthn/server';Just like registration, authentication span two steps:
1. Generate authentication options for the browser to pass to a FIDO2 authenticator 2. Verify the authenticator's response
Each of these steps need their own API endpoints:
1. Generate authentication options
One endpoint (GET) needs to return the result of a call to generateAuthenticationOptions():
// (Pseudocode) Retrieve the user record for the account identifier (email, username, etc...) that
// the unauthenticated user entered
const user: UserModel = getUserFromDBByUsername(submittedUsername);
// (Pseudocode) Retrieve any of the user's previously-
// registered authenticators
const userPasskeys: Passkey[] = getUserPasskeys(user);
const options: PublicKeyCredentialRequestOptionsJSON =
await generateAuthenticationOptions({
rpID,
// Require users to use a previously-registered authenticator
allowCredentials: userPasskeys.map((passkey) => ({
id: passkey.id,
transports: passkey.transports,
})),
});
// (Pseudocode) Remember this challenge for this user
setCurrentAuthenticationOptions(user, options);
return options;These options can be passed directly into **@simplewebauthn/browser**'s `startAuthentication()` method.
_tip_: Support for custom challenges, Power users can optionally generate and pass in their own unique challenges aschallengewhen callinggenerateAuthenticationOptions(). In this scenariooptions.challengestill needs to be saved to be used in verification as described below.
2. Verify authentication response
The second endpoint (POST) should accept the value returned by **@simplewebauthn/browser**'s `startAuthentication()` method and then verify it:
const { body } = req;
// (Pseudocode) Retrieve the user record for the account identifier (email, username, etc...) that
// the unauthenticated user entered
const user: UserModel = getUserFromDBByUsername(submittedUsername);
// (Pseudocode) Get `options.challenge` that was saved above
const currentOptions: PublicKeyCredentialRequestOptionsJSON =
getCurrentAuthenticationOptions(user);
// (Pseudocode} Retrieve a passkey from the DB that
// should match the `id` in the returned credential
const passkey: Passkey = getUserPasskey(user, body.id);
if (!passkey) {
throw new Error(`Could not find passkey ${body.id} for user ${user.id}`);
}
let verification;
try {
verification = await verifyAuthenticationResponse({
response: body,
expectedChallenge: currentOptions.challenge,
expectedOrigin: origin,
expectedRPID: rpID,
credential: {
id: passkey.id,
publicKey: passkey.publicKey,
counter: passkey.counter,
transports: passkey.transports,
},
});
} catch (error) {
console.error(error);
return res.status(400).send({ error: error.message });
}
const { verified } = verification;When finished, it's a good idea to return the verification status to the browser to display appropriate UI:
return { verified };_tip_: Support for multiple origins and RP IDs. SimpleWebAuthn optionally supports verifying authentications from multiple origins and RP IDs! Simply pass in an array of possible origins and IDs forexpectedOriginandexpectedRPIDrespectively.
3. Post-authentication responsibilities
Assuming verification.verified is true, then update the user's authenticator's counter property in the DB:
const { authenticationInfo } = verification;
const { newCounter } = authenticationInfo;
saveUpdatedCounter(passkey, newCounter);Troubleshooting
Below are errors you may see while using this library, and potential solutions to them:
DOMException [NotSupportedError]: Unrecognized name
Authentication responses may unexpectedly error out during verification. This appears as the throwing of an "Unrecognized name" error from a call to verifyAuthenticationResponse() with the following stack trace:
DOMException [NotSupportedError]: Unrecognized name.
at new DOMException (node:internal/per_context/domexception:70:5)
at __node_internal_ (node:internal/util:477:10)
at normalizeAlgorithm (node:internal/crypto/util:220:15)
at SubtleCrypto.importKey (node:internal/crypto/webcrypto:503:15)
at importKey (/Users/swan/Developer/simplewebauthn/packages/server/src/helpers/iso/isoCrypto/importKey.ts:9:27)
at verifyOKP (/Users/swan/Developer/simplewebauthn/packages/server/src/helpers/iso/isoCrypto/verifyOKP.ts:57:30)
at Object.verify (/Users/swan/Developer/simplewebauthn/packages/server/src/helpers/iso/isoCrypto/verify.ts:31:21)
at verifySignature (/Users/swan/Developer/simplewebauthn/packages/server/src/helpers/verifySignature.ts:34:20)
at verifyAuthenticationResponse (/Users/swan/Developer/simplewebauthn/packages/server/src/authentication/verifyAuthenticationResponse.ts:206:36)
at async /Users/swan/Developer/simplewebauthn/packages/server/345.ts:26:24This appears to be an issue with some environments running versions of Node prior to v18 LTS.
To fix this, update your call to generateRegistrationOptions() to exclude -8 (Ed25519) from the list of algorithms:
const options = await generateRegistrationOptions({
// ...
supportedAlgorithmIDs: [-7, -257],
});You will then need to re-register any authenticators that generated credentials that cause this error.
Error: Signature verification with public key of kty OKP is not supported by this method
Authentication responses may unexpectedly error out during verification. This appears as the throwing of a "Signature verification with public key of kty OKP is not supported" error from a call to verifyAuthenticationResponse() with the following stack trace:
Error: Signature verification with public key of kty OKP is not supported by this method
at Object.verify (/xxx/node_modules/@simplewebauthn/server/script/helpers/iso/isoCrypto/verify.js:30:11)
at verifySignature (/xxx/node_modules/@simplewebauthn/server/script/helpers/verifySignature.js:25:76)
at verifyAuthenticationResponse (/xxx/node_modules/@simplewebauthn/server/script/authentication/verifyAuthenticationResponse.js:162:66)This is caused by security key responses in Firefox 118 and earlier being incorrectly composed by the browser when the security key uses Ed25519 for its credential keypair.
To fix this, update your call to generateRegistrationOptions() to exclude -8 (Ed25519) from the list of algorithms:
const options = await generateRegistrationOptions({
// ...
supportedAlgorithmIDs: [-7, -257],
});You will then need to re-register any authenticators that generated credentials that cause this error.
ERROR extractStrings is not a function
Registration responses may unexpectedly error out during verification. This appears as the throwing of an "extractStrings is not a function" error from a call to verifyRegistrationResponse() with the following stack trace:
ERROR extractStrings is not a function
at readString (/node_modules/.pnpm/cbor-x@1.5.6/node_modules/cbor-x/dist/node.cjs:520:1)
at read (/node_modules/.pnpm/cbor-x@1.5.6/node_modules/cbor-x/dist/node.cjs:343:1)
at read (/node_modules/.pnpm/cbor-x@1.5.6/node_modules/cbor-x/dist/node.cjs:363:1)
at checkedRead (/node_modules/.pnpm/cbor-x@1.5.6/node_modules/cbor-x/dist/node.cjs:202:1)
at Encoder.decode (/node_modules/.pnpm/cbor-x@1.5.6/node_modules/cbor-x/dist/node.cjs:153:1)
at Encoder.decodeMultiple (/node_modules/.pnpm/cbor-x@1.5.6/node_modules/cbor-x/dist/node.cjs:170:1)
at Object.decodeFirst (/node_modules/.pnpm/@simplewebauthn+server@8.3.5/node_modules/@simplewebauthn/server/script/helpers/iso/isoCBOR.js:30:1)
at decodeAttestationObject (/node_modules/.pnpm/@simplewebauthn+server@8.3.5/node_modules/@simplewebauthn/server/script/helpers/decodeAttestationObject.js:12:1)
at verifyRegistrationResponse (/node_modules/.pnpm/@simplewebauthn+server@8.3.5/node_modules/@simplewebauthn/server/script/registration/verifyRegistrationResponse.js:100:1)
at AuthnService.verifyRegistrationResponse (/home/deploy/mx/modules/authn/authn.service.js:89:1)This is caused by the @vercel/ncc dependency not supporting runtime use of require() within other third-party packages used by the project, like @simplewebauthn/server's use of cbor-x.
To fix this, add CBOR_NATIVE_ACCELERATION_DISABLED=true in your project's env file to disable the use of require() in cbor-x.
Alternatively, the following can be added to your project to inject this value into your project's runtime environment:
function nodeEnvInjection() {
/**
* `@vercel/ncc` does not support the use of `require()` so disable its
* use in the `@simplewebauthn/server` dependency called `cbor-x`.
*
* https://github.com/kriszyp/cbor-x/blob/master/node-index.js#L10
*/
process.env['CBOR_NATIVE_ACCELERATION_DISABLED'] = 'true';
}
// Call this at the start of the project, before any imports
nodeEnvInjection();Error: No data
Calls to verifyAuthenticationResponse() may unexpectedly error out with Error: No data in projects that store credential public keys as Binary data types in MongoDB:
Error: No data
at Module.decodePartialCBOR (file:///path/to/app/node_modules/@levischuck/tiny-cbor/esm/cbor/cbor.js:351:15)
at Module.decodeFirst (file:///path/to/app/node_modules/@simplewebauthn/server/esm/helpers/iso/isoCBOR.js:22:30)
at decodeCredentialPublicKey (file:///path/to/app/node_modules/@simplewebauthn/server/esm/helpers/decodeCredentialPublicKey.js:3:65)
at verifySignature (file:///path/to/app/node_modules/@simplewebauthn/server/esm/helpers/verifySignature.js:17:25)
at verifyAuthenticationResponse (file:///path/to/app/node_modules/@simplewebauthn/server/esm/authentication/verifyAuthenticationResponse.js:154:25)
at async file:///path/to/app/dist/controller/auth.controller.js:202:28To fix this issue, convert the Binary public key bytes into an instance of Uint8Array that verifyAuthenticationResponse() expects:
const verification = await verifyAuthenticationResponse({
// ...
credential: {
// ...
publicKey: new Uint8Array(credentialPublicKey.buffer),
},
});Error: String values for \userID\ are no longer supported
See Advanced Guides > @simplewebauthn/server > Custom User IDs for more information.
TypeError: Cannot read properties of undefined (reading 'counter')
As of SimpleWebAuthn v11.0.0 the verifyAuthenticationResponse() method expects a credential argument of type WebAuthnCredential that contains values like the credential ID, public key, and counter needed to verify the response. In prior versions, this argument was called authenticator of the now defunct type AuthenticatorDevice. If you see this error it is likely caused by authenticator not being refactored into the new credential argument.
To fix this issue, update the call to verifyAuthenticationResponse() to replace authenticator with credential of type WebAuthnCredential:
Before
import { AuthenticatorDevice } from '@simplewebauthn/types';
const authenticator: AuthenticatorDevice = {
credentialID: ...,
credentialPublicKey: ...,
counter: 0,
transports: [...],
};
const verification = await verifyAuthenticationResponse({
// ...
authenticator,
});After
import { WebAuthnCredential } from '@simplewebauthn/server';
const credential: WebAuthnCredential = {
id: ..., // Same as authenticator.credentialID
publicKey: ..., // Same as authenticator.credentialPublicKey
counter: 0, // Same as authenticator.counter
transports: [...], // Same as authenticator.transports
};
const verification = await verifyAuthenticationResponse({
// ...
credential,
});_danger_: Deprecation notice. It is no longer necessary to install this package if you are running v13+ of either @simplewebauthn/browser or @simplewebauthn/server. The types that were maintained in this package will be exported directly from those packages going forward. The last published version of @simplewebauthn/types remains available on NPM and JSR but will no longer be updated.
Before:
import type { WebAuthnCredential } from '@simplewebauthn/types';After:
import { ..., type WebAuthnCredential } from '@simplewebauthn/server';import { ..., type WebAuthnCredential } from '@simplewebauthn/browser';Overview
The SimpleWebAuthn project contains two complimentary libraries to help reduce the amount of work needed to incorporate WebAuthn into a website. The following packages are maintained here:
- @simplewebauthn/server
- @simplewebauthn/browser
WebAuthn is a browser API that empowers us all to secure our accounts with a user-friendly experience powered by public-key cryptography.
Website back ends that wish to leverage this technology must be set up to do two things:
1. Provide to the front end a specific collection of values that the hardware authenticator will understand for "registration" and "authentication". 2. Parse responses from hardware authenticators.
Website front ends have their own part to play in the process:
1. Pass the server-provided values into the WebAuthn API's navigator.credentials.create() and navigator.credentials.get() so the user can interact with their compatible authenticator. 2. Pass the authenticator's response returned from these methods back to the server.
On the surface, this is a relatively straightforward dance. Unfortunately the values passed into the navigator.credentials methods and the responses received from them make heavy use of ArrayBuffer's which are difficult to transmit as JSON between front end and back end. Not only that, there are many complex ways in which authenticator responses must be parsed, and though finalized, the W3C spec is quite complex and is being expanded all the time.
Enter SimpleWebAuthn.
SimpleWebAuthn offers a developer-friendly pair of libraries that simplify the above dance. @simplewebauthn/server exports a small number of methods requiring a handful of simple inputs that pair with the two primary methods exported by @simplewebauthn/browser. No converting back and forth between Uint8Array (or was this supposed to be an ArrayBuffer...?) and String, no worrying about JSON compatibility - SimpleWebAuthn takes care of it all!