passkeys-web
A skill for implementing passkey in web applications. You MUST use this skill whenever a user asks about passkey registartion, passkey authentication or passkey management. It defines the required database schema, API usage, and security best practices.
System prompt
Passkeys Implementation Guide (Web)
This skill provides a modular guide to implementing passkeys
Implementation Phase
- Registration: Allowing users to create new passkeys.
- Authentication: Logging users in with passkeys.
- Management: Listing, renaming, and deleting passkeys.
- Reauthentication: Logging users back in with passkeys.
Evaluation
- Evaluate Passkey Skills: Evaluate passkey skills.
References
You MUST read the following references when implementing their respective features:
- Conditional Create: Read this before implementing automatic passkey creation or conditional create.
- Conditional Mediation: Read this before implementing form autofill based passkey authentication.
- Signal API: Read this before implementing any
PublicKeyCredential.signal*methods. - AAGUID Resolution: Read this before saving a new credential to the database during registration to map the provider name and icon.
Recommended Libraries
For backend, using a library is recommended. Here's a list of recommended open source libraries per language:
- JavaScript/TypeScript: SimpleWebAuthn
- Python: py_webauthn
- Java: Java WebAuthn Server , WebAuthn4J, LINE FIDO2 Server
- .Net: .Net library for FIDO2, WebAuthn.Net
- Go: WebAuthn Go Library, Passkey Server
- Ruby: WebAuthn Ruby
- PHP: WebAuthn PHP Library, WebAuthn Framework
If you are using one of the libraries above, use the following skills:
- Use [[simplewebauthn]] skill when implementing passkeys using SimpleWebAuthn
Attachments
# Passkey Authentication Implementation
Implement a sign-in flow using passkeys.
## 1. Server-Side
### Generate Options Endpoint
1. Create a `challenge` and store it to the session.
2. Specify an empty array `[]` for `allowCredentials` unless the user is known.
3. Specify `userVerification: "preferred"` unless the user explicitly expects `userVerification: "required"`.
### Verify Response Endpoint
1. Check that the `challenge` matches the one in the session.
2. Do not verify the UV flag unless `userVerification: "required"` is passed at the frontend, but don't include this switch in a query parameter. It should be in recorded in the session at the [[#Generate Options Endpoint|options endpoint]].
3. Make sure the signature is verified against the public key stored to the server.
4. Respond with HTTP error code `404` if the matching public key can't be found in the database.
## 2. Client-Side
### Fetch Options from the Server
1. Choose how to invoke passkey authentication:
1. Trigger authentication when a user presses a "Sign-in with passkey" button. Abort ongoing WebAuthn call if there's one.
2. Invoke authentication conditionally as soon as the page is loaded if the sign-in utilizes form autofill following [[./references/conditional-mediation]].
2. Perform feature detection with `PublicKeyCredential.getClientCapabilities()` and check with `passkeyPlatformAuthenticator` is supported to ensure the browser and device support passkeys.
```javascript
// Check for compatibility
if (window.PublicKeyCredential && PublicKeyCredential.getClientCapabilities) {
const capabilities = await PublicKeyCredential.getClientCapabilities();
if (!capabilities.passkeyPlatformAuthenticator) {
// Platform authenticator is not available.
return;
}
}
```
3. Decode fetched credential JSON object with `PublicKeyCredential.parseRequestOptionsFromJSON()`.
4. Attach `AbortController` in case the authentication request must be terminated.
5. Handle errors from `navigator.credentials.get()` by checking `error.name`:
```javascript
const abortController = new AbortController();
try {
const credential = await navigator.credentials.get({
publicKey,
signal: abortController.signal
});
// ... send to server
} catch (e) {
if (e.name === 'NotAllowedError') {
// The user cancelled the dialog or it timed out.
// Do nothing — allow the user to retry.
} else if (e.name === 'AbortError') {
// The operation has been aborted.
} else {
// For other errors, the browser typically shows its own error dialog.
}
}
```
6. Encode the resulting `AuthenticatorAssertionResponse` with `toJSON()` before sending it to the server for verification.
7. Separate the `navigator.credentials.get()` call from the server verification `fetch` call into distinct try/catch blocks (or use a flag/variable to distinguish which phase failed). **Only call `signalUnknownCredential()` when the server explicitly responds with HTTP status `404` (Credential not found).** Do not call it on other server errors to avoid unintentionally signaling a valid passkey as unknown. Do not call it for WebAuthn API errors (`NotAllowedError` and `AbortError`). The credential ID to pass should come from the encoded credential response (e.g. `encodedCredential.id`).
# Passkey Skills Evaluation
Evaluate the current codebase against the passkey best practices from the `passkeys-web` and `simplewebauthn` skills. For each check, search the codebase for evidence and report **PASS**, **FAIL**, or **N/A** (if the feature area is not yet implemented).
## Instructions
1. Read all relevant source files (server-side auth routes, database layer, client-side HTML/JS).
2. Evaluate each item below by searching the code for the specified pattern or behavior.
3. Report results in a markdown table with columns: `#`, `Check`, `Status`, `Evidence` (file + line or brief note).
4. Summarize totals at the end: PASS / FAIL / N/A counts.
---
## Registration
Source: `passkeys-web/registration.md`, `simplewebauthn/SKILL.md`
### Database Schema
| # | Check | What to look for |
| --- | ---------------------------------------------- | --------------------------------------------- |
| R1 | `id` (Base64URLString) stored | Credential ID stored as string |
| R2 | `passkeyUserId` stored | User ID associated with credential |
| R3 | `credentialPublicKey` (Base64URLString) stored | Public key stored (Base64URL encoded) |
| R4 | `credentialType` stored | e.g. `'public-key'` |
| R5 | `credentialDeviceType` stored | `'singleDevice'` or `'multiDevice'` |
| R6 | `credentialBackedUp` stored | Boolean backup state |
| R7 | `aaguid` stored | AAGUID string |
| R8 | `name` stored (AAGUID-derived) | Provider name is derived from AAGUID registry |
| R9 | `providerIcon` stored | Provider icon is derived from AAGUID registry |
| R11 | `transports` stored | Array of transports |
| R12 | `lastUsedAt` stored | Optional last used epoch time |
| R13 | `registeredAt` stored | Registration timestamp |
### Server — Generate Options Endpoint
| # | Check | What to look for |
| --- | ----------------------------------------------- | -------------------------------------------------------- |
| R14 | Challenge stored in session | `req.session.challenge = options.challenge` or similar |
| R15 | `excludeCredentials` uses existing credentials | Existing user credentials mapped to `excludeCredentials` |
| R16 | `authenticatorAttachment: 'platform'` | Specified for passkey promotion |
| R17 | `authenticatorAttachment` not specified | Not specified for dedicated passkey management page |
| R18 | `residentKey: 'required'` | In `authenticatorSelection` |
| R19 | `requireResidentKey: true` | In `authenticatorSelection` |
| R20 | `userVerification: 'preferred'` or `'required'` | In `authenticatorSelection` |
| R21 | `userID` passed as binary (SimpleWebAuthn) | Pass `credential.id` obtained from |
### Server — Verify Response Endpoint
| # | Check | What to look for |
| --- | ------------------------------------------------- | ------------------------------------------------------- |
| R22 | Challenge verified | `expectedChallenge` from session passed to verification |
| R23 | `requireUserVerification: false` (if `preferred`) | Accepts UV=false |
| R24 | AAGUID lookup from registry | `aaguids[aaguid]` or similar lookup |
| R25 | HTTP 404 on missing public key | Returns 404 if matching public key isn't found in DB |
### Client — Registration Flow
| # | Check | What to look for |
| --- | --------------------------------------------- | --------------------------------------------------- |
| R26 | Feature detection: `getClientCapabilities()` | Check for `passkeyPlatformAuthenticator` capability |
| R27 | `parseCreationOptionsFromJSON()` used | Decodes server options to WebAuthn format |
| R28 | `InvalidStateError` handled | Specific catch for duplicate passkey |
| R29 | `SecurityError` handled | HTTPS / RP ID mismatch error handled |
| R30 | `NotAllowedError` handled | User cancellation handled gracefully |
| R31 | `AbortError` handled | Operation aborted handled |
| R32 | `toJSON()` encoding | `credential.toJSON()` before sending to server |
| R33 | `signalUnknownCredential()` on server failure | Called ONLY when server `fetch` verification fails |
### Conditional Create
Source: `passkeys-web/references/conditional-create.md`
| # | Check | What to look for |
| --- | ------------------------------------------ | ----------------------------------------------------------------------------------------------------------- |
| CC1 | Trigger moment | Invoked immediately after a successful, full sign-in that involved a password (e.g., after 2FA is complete) |
| CC2 | Feature detection | `capabilities.conditionalCreate` checked |
| CC3 | Ongoing Conditional Get aborted | `abortController.abort()` called before create |
| CC4 | Conditional mediation | `mediation: 'conditional'` used in `navigator.credentials.create()` |
| CC5 | Silent exceptions handled | Specific catch and ignore for `InvalidStateError`, `NotAllowedError`, `AbortError` |
| CC6 | Server ignores User Presence conditionally | `requireUserPresence: false` in `verifyRegistrationResponse()` ONLY when triggered from conditional create |
| CC7 | Session check enforced | User session is validated (e.g. `sessionCheck` middleware) before conditional registration |
---
## Authentication
Source: `passkeys-web/authentication.md`
### Server — Generate Options Endpoint
| # | Check | What to look for |
| --- | -------------------------------------------- | ------------------------------------------- |
| A1 | `generateAuthenticationOptions` imported | From `@simplewebauthn/server` |
| A2 | Challenge stored in session | `req.session.challenge = options.challenge` |
| A3 | `allowCredentials: []` (empty, discoverable) | Empty array for passkey authentication |
| A4 | `userVerification: 'preferred'` | In options (unless required by user) |
### Server — Verify Response Endpoint
| # | Check | What to look for |
| --- | ---------------------------------------- | ------------------------------------------------- |
| A5 | `verifyAuthenticationResponse` imported | From `@simplewebauthn/server` |
| A6 | Challenge verified | `expectedChallenge` from session passed |
| A7 | UV flag not verified | Unless `'required'` is passed at frontend |
| A8 | HTTP 404 on missing public key | Returns 404 if matching public key isn't found |
| A9 | `credentialPublicKey` passed (as buffer) | Public key converted from stored format |
| A10 | Counter passed to verification | `authenticator.counter` from DB |
| A11 | Counter updated after verification | `authenticationInfo.newCounter` saved to DB |
| A12 | User signed in after success | Session updated (e.g. `req.session['signed-in']`) |
### Client — Authentication Flow
| # | Check | What to look for |
| --- | ------------------------------------ | ------------------------------------------------------------- |
| A13 | Authentication function exists | Client-side code that calls signin endpoint |
| A14 | Feature detection | `getClientCapabilities()` checks passkeyPlatformAuthenticator |
| A15 | Conditional UI (autofill) support | `mediation: 'conditional'` property parameter |
| A16 | `parseRequestOptionsFromJSON()` used | Decodes server options to WebAuthn format |
| A17 | `AbortController` attached | Used in `navigator.credentials.get({ signal })` |
| A18 | `NotAllowedError` handled | Specific catch |
| A19 | `AbortError` handled | Specific catch |
| A20 | `toJSON()` encoding | Response encoded with `toJSON()` |
| A21 | `signalUnknownCredential()` handled | Called ONLY when server explicitly responds with HTTP 404 |
| A22 | Form annotated for autofill | `autocomplete="username webauthn"` or `password webauthn` |
| A23 | Autofill triggers automatically | `autofocus` attribute present on annotated input |
| A24 | Autofill feature detection | `capabilities.conditionalGet` checked before starting |
---
## Management
Source: `passkeys-web/management.md`
### Server
| # | Check | What to look for |
| --- | --------------------------------------- | ----------------------------------------------------- |
| M1 | Endpoint: list all credentials for user | GET/POST route returning user's credentials |
| M2 | Endpoint: rename credential | Route accepting credential ID + new name, updating DB |
| M3 | Endpoint: delete credential | Route accepting credential ID, removing from DB |
### Client — UI
| # | Check | What to look for |
| --- | ----------------------------- | -------------------------------------- |
| M4 | Passkey list rendered | Credentials displayed in a list |
| M5 | Each item shows provider icon | AAGUID-derived icon displayed |
| M6 | Each item shows passkey name | AAGUID-derived name or custom name |
| M7 | Each item shows date | `registeredAt` formatted and displayed |
| M8 | Rename button per passkey | UI element to trigger rename |
| M9 | Delete button per passkey | UI element to trigger delete |
| M10 | Create passkey button | Button to register a new passkey |
| M11 | Empty state message | Helpful text when no passkeys exist |
### Client — Signal API
| # | Check | What to look for |
| --- | --------------------------------------------- | ------------------------------------------------ |
| M12 | Method feature detection check | e.g. `if (PublicKeyCredential.signalMethodName)` |
| M13 | `signalAllAcceptedCredentials()` on page load | Called when management page loads |
| M14 | `signalAllAcceptedCredentials()` after delete | Called after a passkey is deleted |
| M15 | `signalCurrentUserDetails()` after rename | Called after display name or passkey name change |
| M16 | Base64URL-encoded parameters used | Parameters are string, not BufferSource |
---
## Reauthentication
Source: `passkeys-web/reauthentication.md`
### Server — Generate Options Endpoint
| # | Check | What to look for |
| --- | ------------------------------- | ---------------------------------------------------------------------------- |
| RA1 | `allowCredentials` contains IDs | List of `PublicKeyCredentialDescriptor` objects mapped to `allowCredentials` |
### Server — Verify Response Endpoint
| # | Check | What to look for |
| --- | ----------------------------------- | ---------------------------------------------------------------------- |
| RA2 | Assertion belongs to signed-in user | Server explicitly verifies the assertion belongs to the signed-in user |
### Client — Reauthentication Flow
| # | Check | What to look for |
| --- | --------------------- | ---------------------------------------------------------------- |
| RA3 | Appropriate flow used | Uses button flow (if no form) or conditional mediation (if form) |
---
## Signal API
Source: `passkeys-web/references/signal-api.md`
| # | Check | What to look for |
| --- | ------------------------------------------- | ------------------------------------------------------------------------------- |
| S1 | Feature detection | `if (PublicKeyCredential.signalMethodName)` guard |
| S2 | `signalAllAcceptedCredentials()` parameters | passes `rpId`, `userId`, `allAcceptedCredentialIds` (Base64URL-encoded strings) |
| S3 | `signalCurrentUserDetails()` parameters | passes `rpId`, `userId`, `name`, `displayName` |
| S4 | `signalUnknownCredential()` parameters | passes `rpId`, `credentialId` (Base64URL-encoded strings) |
---
## AAGUID Handling
Source: `passkeys-web/references/determine-passkey-provider-from-aaguid.md`
| # | Check | What to look for |
| --- | ------------------------------------- | ------------------------------------------------------------ |
| G1 | AAGUID registry JSON present | `aaguids.json` or similar file with AAGUID mappings |
| G2 | AAGUID lookup after registration | `registrationInfo.aaguid` used to look up provider |
| G3 | Provider name populated from registry | `provider?.name` or fallback to `'Unknown passkey provider'` |
| G4 | Provider icon populated from registry | `provider?.icon_dark` or `provider?.icon_light` |
| G5 | Zeroed AAGUID handled | Fallback for `00000000-0000-0000-0000-000000000000` |
# Passkey Management Implementation
It is best practice to allow users to manage their registered passkeys (list, rename, delete).
## Requirements
- Allow saving multiple passkeys
- Display a registered passkeys list
- Each passkey item displays:
- Passkey manager icon (Determine based on the AAGUID)
- Passkey name (Determine based on the AAGUID)
- Registered date / time
- Last used date / time
- Rename button
- Delete button
- Rename a registered passkey
- Delete a registered passkey
- Create passkey button
- Send list of passkeys signal to password manager as soon as the page is loaded or a passkey is deleted
- Send passkey user details signal to password manager after renaming a passkey
## 1. Server-side Operations
Ensure your `Credentials` store and server endpoints support:
- Returns all credentials for a user.
- Updates an existing credential (for renaming).
- Deletes a credential.
Example endpoints:
```javascript
// GET or POST — return all credentials for the signed-in user
router.post('/credentials', sessionCheck, (req, res) => {
const credentials = Credentials.findByUserId(res.locals.user.id);
return res.json(credentials);
});
// PUT — rename a credential
router.put('/credential/:credentialId', sessionCheck, async (req, res) => {
const { credentialId } = req.params;
const { name } = req.body;
const credential = Credentials.findById(credentialId);
if (!credential || credential.passkeyUserId !== res.locals.user.id) {
return res.status(404).json({ error: 'Credential not found.' });
}
credential.name = name;
await Credentials.update(credential);
return res.json(credential);
});
// DELETE — delete a credential
router.delete('/credential/:credentialId', sessionCheck, async (req, res) => {
const { credentialId } = req.params;
const credential = Credentials.findById(credentialId);
if (!credential || credential.passkeyUserId !== res.locals.user.id) {
return res.status(404).json({ error: 'Credential not found.' });
}
await Credentials.delete(credentialId);
return res.json({ success: true });
});
```
## 2. Client-Side Logic
### Conditional passkey creation button
Display a "Create passkey" button if the browser supports passkeys. Determine the capability using `PublicKeyCredential.getClientCapabilities()`.
### Display the saved passkey list
When rendering the passkeys on the client, ensure each passkey item displays the following:
- **Passkey manager icon**: An icon representing the passkey provider (determine based on the `AAGUID`).
- **Passkey name**: The name of the passkey provider or a custom name (determine based on the `AAGUID`).
- **Registered date / time**: The date and time when the passkey was registered.
- **Last used date / time**: The date and time when the passkey was last used.
- **Rename button**: A UI element allowing the user to rename the passkey.
- **Delete button**: A UI action allowing the user to remove the passkey.
Display "No passkeys found" message when there are no passkeys.
### Signal on page load and after delete
You MUST read [[references/signal-api]] for exact method signatures before implementing any Signal API calls. Key points:
- All `userId` and credential ID parameters are **base64url-encoded strings**, NOT `Uint8Array` or `BufferSource`
- Always feature-detect before calling (e.g. `if (PublicKeyCredential.signalAllAcceptedCredentials)`)
Signal API calls required:
- Call `PublicKeyCredential.signalAllAcceptedCredentials()` as soon as the page is loaded
- Call `PublicKeyCredential.signalAllAcceptedCredentials()` with a list of all saved credentials when a passkey is deleted
- Call `PublicKeyCredential.signalCurrentUserDetails()` with the updated user details after renaming a passkey
## 3. UI Best Practices
- **Empty State**: Show a helpful message if no passkeys are registered.
# Passkey Reauthentication Implementation
Rely on a Passkey/WebAuthn/FIDO2 library that suit your server language.
## 1. Server-Side
### Generate Options Endpoint
- Pass the list of `PublicKeyCredentialDescriptor` objects containing user credential IDs to `allowCredentials` so authentication is constrained to associated passkeys.
### Verify Response Endpoint
- Perform common logic as the regular authentication.
- Verify that the assertion belongs to the user who is signing in.
## 2. Client-Side
Depending on the reauthentication page, choose from one of two reauthentication methods:
- If there is no sign-in form: use a button flow
- If there is a sign-in form: use conditional mediation
### Button flow
- Follow the flow described in [[./authentication]] except `allowCredentials` contain credential IDs.
### Conditional mediation
- Follow [[./references/conditional-mediation]].
Conditional Create allows you to automatically create passkeys for users at the right moment without requiring any action from them, as long as they already have a password saved for your site. This reduces friction in passkey adoption.
This flow works when the user has a saved password in their default password manager, and the password was used recently (e.g., immediately after a successful password-based sign-in).
## Implementation Steps
### 1. Identify the Right Moment
Use Conditional Create **immediately after a successful, full sign-in that involved a password**. If a second factor is required, you must wait until the user has successfully completed all authentication steps and is fully signed in before performing conditional create.
*Note: Passwordless sign-in methods such as magic links, SMS OTP, or identity federation do not meet the conditional create requirements, as it is gated behind entering a valid password.*
### 2. Abort Ongoing Conditional Get
If your login page uses form autofill (Conditional Get), typically the RP expects the user to sign in with either a passkey or password. If the user chooses password, you must abort the ongoing `navigator.credentials.get()` call before invoking an automatic passkey creation.
Use the `AbortController` from your Conditional Get implementation:
```javascript
// Abort the ongoing Conditional Get call
abortController.abort();
```
### 3. Feature Detection
Determine whether Conditional Create is available by checking `conditionalCreate` within `PublicKeyCredential.getClientCapabilities()`.
```javascript
if (window.PublicKeyCredential && PublicKeyCredential.getClientCapabilities) {
const capabilities = await PublicKeyCredential.getClientCapabilities();
if (capabilities.conditionalCreate) {
// Conditional create is available
}
}
```
### 4. Create the Passkey Conditionally
Invoke `navigator.credentials.create()` but with `mediation: "conditional"`.
Ensure you specify `excludeCredentials` to prevent creating duplicate passkeys if one already exists.
```javascript
try {
const cred = await navigator.credentials.create({
publicKey: options,
mediation: 'conditional'
});
// Send the resulting public key credential to the server to verify and register
} catch (e) {
// Handle exceptions gracefully
}
```
### 5. Ignore Exceptions Gracefully
If automatic passkey creation fails, the browser handles it silently without triggering visible messages to the user. You must catch and ignore these exceptions to avoid confusing the user with error popups:
- `InvalidStateError`: A passkey already exists in the provider (occurs when `excludeCredentials` is matched).
- `NotAllowedError`: Creating a passkey doesn't meet the required conditions (e.g., password sign-in wasn't recent enough).
- `AbortError`: The WebAuthn call is manually aborted.
### 6. Adjust Server-Side Verification
Make sure the user is already signed in. A passkey created with conditional create can't create a new user.
Since user interaction is skipped, the registration response returns both **User Presence** and **User Verified** as `false`. The server must securely ignore these flags **only** during credential verification for conditional create requests, while continuing to enforce strict presence checks for normal registrations.
### 7. Signal Registration Failures
If the passkey is created by the passkey provider but fails to register on your server, the passkey lists become inconsistent, resulting in failing sign-in attempts for the user. To mitigate this risk, use the Signal API to keep credentials synchronized between the passkey provider and the server.
Conditional Mediation is also known as Conditional UI or Conditional Get. This allows users to sign in using their passkeys directly from the browser's autofill suggestions, providing a seamless transition from passwords to passkeys.
Passkey form autofill leverages the `mediation: 'conditional'` property in the WebAuthn `navigator.credentials.get()` call. When implemented, the browser doesn't show a modal immediately. Instead, it waits until the user focuses on an input field annotated with `autocomplete="username webauthn"`.
## Implementation Steps
### 1. Annotate the Sign-in Form
Add `autocomplete="username webauthn"` to your username input field, or `autocomplete="password webauthn"` to your password input field. Also add `autofocus` to the same `input` tag to trigger the autofill prompt immediately on page load.
```html
<form id="signin-form">
<input type="text" name="username" autocomplete="username webauthn" autofocus>
<input type="password" name="password" autocomplete="current-password">
<button type="submit">Sign in</button>
</form>
```
### 2. Feature Detection
Check if the browser supports Conditional Get with `PublicKeyCredential.getClientCapabilities()` and check if `conditionalGet` is supported before making the call.
```javascript
if (window.PublicKeyCredential && PublicKeyCredential.getClientCapabilities) {
const capabilities = await PublicKeyCredential.getClientCapabilities();
if (capabilities.conditionalGet === true) {
// The browser supports passkeys and the conditional mediation.
}
}
```
### 3. Call WebAuthn with Conditional Mediation
Invoke `navigator.credentials.get()` with `mediation: 'conditional'` to activate form autofill.
```javascript
const abortController = new AbortController();
try {
const credential = await navigator.credentials.get({
publicKey: options,
signal: abortController.signal,
mediation: 'conditional'
});
...
```
# Determine the passkey provider from AAGUID
An AAGUID (Authenticator Attestation Globally Unique Identifier) is a 128-bit identifier that represents the model of the authenticator, not a specific instance. It is included in the authenticator data during passkey registration and can be used to determine which passkey provider (e.g. Google Password Manager, iCloud Keychain, 1Password) created a credential.
AAGUID should only be used to help users with passkey management. It can be modified unless cryptographically attested, which platform passkeys currently don't support.
## 1. AAGUID Registry
A community-maintained JSON mapping of AAGUIDs to provider names and icons is available at:
```
https://raw.githubusercontent.com/passkeydeveloper/passkey-authenticator-aaguids/refs/heads/main/combined_aaguid.json
```
Each entry has the following schema:
```json
{
"<aaguid-uuid>": {
"name": "Provider Name",
"icon_light": "data:image/png;base64,...",
"icon_dark": "data:image/png;base64,..."
}
}
```
## 2. Using AAGUID After Registration
After verifying a registration response, read the `aaguid` from the registration result and look it up against the registry to populate the credential's `name` and `providerIcon`:
Before looking up the AAGUID in the registry, check if it equals `'00000000-0000-0000-0000-000000000000'`. If so, skip the registry lookup and set `name` to a fallback (e.g. device name from user-agent, or "Unknown passkey provider") and `providerIcon` to `undefined`. Only look up the registry for non-zeroed AAGUIDs.
```javascript
import aaguids from './aaguids.json' with { type: 'json' };
const { aaguid } = registrationInfo;
if (aaguid === '00000000-0000-0000-0000-000000000000') {
// use the device name as the passkey provider based on
// the information derived from the user agent string,
// or just say "Unknown passkey provider"
} else {
const provider = aaguids[aaguid];
const credential = {
// ...other fields
aaguid,
name: provider?.name || 'Unknown passkey provider',
providerIcon: provider?.icon_light,
};
}
```
## 3. Passkey Management UI
Use the AAGUID-derived name and icon when displaying registered passkeys so users can identify which provider holds each credential. Combine with the registration date and last-used date for additional context.
# Signal API
The Signal API lets relying parties communicate credential state to passkey providers (password managers), keeping passkeys in sync without user interaction.
Always feature-detect before calling any signal method:
```javascript
if (PublicKeyCredential.signalMethodName) {
await PublicKeyCredential.signalMethodName({ /* ... */ });
}
```
## 1. `signalAllAcceptedCredentials()`
Informs the passkey provider of the complete list of valid credential IDs for a user. Passkeys not in the list are hidden (not permanently deleted), allowing restoration if they are included in a future call.
**When to call:**
- As soon as the passkey management page loads
- After a passkey is deleted
**Warning:** Passing an empty `allAcceptedCredentialIds` array hides all passkeys for the user. Only invoke when you are confident the list is complete.
```javascript
await PublicKeyCredential.signalAllAcceptedCredentials({
rpId: "example.com",
// Pass in a Base64URL-encoded user ID
userId: "M2YPl-KGnA8",
allAcceptedCredentialIds: [
// Pass in a Base64URL-encoded credential IDs
"vI0qOggiE3OT01ZRWBYz5l4MEgU0c7PmAA",
],
});
```
## 2. `signalCurrentUserDetails()`
Synchronizes updated username and display name with the passkey provider. The provider may choose not to overwrite values the user has manually edited in their password manager.
**When to call:**
- After the user updates their profile (username or display name)
- Optionally on every sign-in
```javascript
await PublicKeyCredential.signalCurrentUserDetails({
rpId: "example.com",
// Base64URL-encoded user ID
userId: "M2YPl-KGnA8",
// username
name: "new.email@example.com",
// display name
displayName: "J. Doe",
});
```
## 3. `signalUnknownCredential()`
Notifies the passkey provider that a credential no longer exists on the server, so the provider can stop offering it to the user.
**When to call:**
- After passkey **authentication** fails because the credential is not found on the server (HTTP 404) — only when the user is unauthenticated.
- After server-side **registration** verification fails — call regardless of auth state.
```javascript
await PublicKeyCredential.signalUnknownCredential({
rpId: "example.com",
// Base64URL-encoded credential ID
credentialId: "vI0qOggiE3OT01ZRWBYz5l4MEgU0c7PmAA",
});
```
# Passkey Registration Implementation
Rely on a Passkey/WebAuthn/FIDO2 library that suit your server language.
## 1. Database Requirements
Your database should store:
```typescript
export interface SesamePublicKeyCredential {
id: Base64URLString; // Credential ID
passkeyUserId: PasskeyUserId; // User ID for passkeys
credentialPublicKey: Base64URLString; // public key,
credentialType: string; // type of credential,
credentialDeviceType: 'singleDevice' | 'multiDevice';
credentialBackedUp: boolean;
aaguid: string; // AAGUID,
counter?: number // Optional counter
providerIcon: string; // Provider icon determined based on AAGUID
name: string; // Determined based on AAGUID
transports: AuthenticatorTransportFuture[]; // list of transports,
lastUsedAt?: number; // last used epoc time,
registeredAt: number;
}
```
## 2. Server-Side
### Generate Options Endpoint
1. Create a `challenge` and store it to the session.
2. Pass existing credentials to `excludeCredentials` to prevent creating duplicates on the same password manager account.
3. **`authenticatorAttachment` depends on the call site — this is a common mistake:**
- Set `authenticatorAttachment: "platform"` **only** for **passkey promotion** flows: post-signup auto-creation, post-login prompt, or any fire-and-forget passkey offer. "Passkey promotion" means offering passkey creation to a user who just authenticated via a non-passkey method.
- **Do NOT set `authenticatorAttachment`** (omit it entirely) when called from a **dedicated passkey management page** (e.g. Settings → Security). Omitting it allows users to register hardware security keys (YubiKey, etc.) in addition to platform authenticators.
- If a single `/register/options` endpoint serves both flows, accept a `promotion: boolean` field in the request body and conditionally apply `authenticatorAttachment: "platform"` only when `promotion === true`.
4. Specify `requireResidentKey: true` and `residentKey: "required"` to request a discoverable credential.
5. Specify `userVerification: "preferred"` or `userVerification: "required"` to verify the user.
### Verify Response Endpoint
1. Check that the `challenge` matches the one in the session.
2. Accept a UV flag: `false` (if you used `'preferred'` and want to allow UV-less authenticators, though mostly unnecessary for resident keys).
3. The UP flag must be `true` unless this is conditional create.
4. Determine the passkey provider based on the AAGUID. You must read [[./references/determine-passkey-provider-from-aaguid]] .
5. Respond with HTTP error code 404 if the matching public key can't be found in the database so that the frontend can invoke the Signal API.
## 3. Client-Side Logic
### Fetch Options from the Server
1. Perform feature detection with `PublicKeyCredential.getClientCapabilities()` to ensure the browser and device support passkeys. Namely:
- a platform authenticator
```javascript
// Check for compatibility
if (window.PublicKeyCredential && PublicKeyCredential.getClientCapabilities) {
const capabilities = await PublicKeyCredential.getClientCapabilities();
if (!capabilities.passkeyPlatformAuthenticator) {
// Platform authenticator is not available.
return;
}
}
```
2. Decode fetched credential JSON object with `PublicKeyCredential.parseCreationOptionsFromJSON()`.
3. Handle errors from `navigator.credentials.create()` by checking `error.name`:
```javascript
try {
const credential = await navigator.credentials.create({ publicKey });
// ... send to server
} catch (e) {
if (e.name === 'InvalidStateError') {
// A matching credential already exists in the authenticator.
// Inform the user a passkey already exists for this device.
} else if (e.name === 'SecurityError') {
// The RP ID doesn't match the origin, or the page isn't HTTPS.
// Show an error message — this is a configuration problem.
} else if (e.name === 'NotAllowedError') {
// The user cancelled the dialog or it timed out.
// Do nothing — allow the user to retry.
} else if (e.name === 'AbortError') {
// The operation has been aborted.
}
// For other errors, the browser typically shows its own error dialog.
}
```
4. Encode the resulting `AuthenticatorAttestationResponse` with `toJSON()` before sending it to the server for verification.
5. Separate the `navigator.credentials.create()` call from the server verification `fetch` call into distinct try/catch blocks (or use a flag/variable to distinguish which phase failed). Only call `signalUnknownCredential()` when the server verification fails — i.e., when the `fetch()` throws. The credential ID to pass should come from the encoded credential response (e.g. `encodedCredential.id`). Do not call it for WebAuthn API errors (`InvalidStateError`, `NotAllowedError`,` SecurityError` and `AbortError`).