webauthn-emulator
v1.0.2
Published
[](https://github.com/niranjan94/webauthn-emulator/actions/workflows/ci.yml) [](https://ope
Readme
WebAuthn Emulator
WebAuthn Emulator is a library that provides both a FIDO2/CTAP Authenticator emulator and a WebAuthn API emulator built on top of it. Each component is implemented according to the WebAuthn API and CTAP specifications respectively. This module runs on Node.js and is designed for local integration testing of Passkeys.
For detailed specifications of each emulator, please refer to the following:
- FIDO2/CTAP Authenticator Emulator Detailed Specification for Developers
- WebAuthn API Emulator Detailed Specification for Developers
Usage
npm install webauthn-emulatorBasic usage involves creating a WebAuthnEmulator class and using the create and get methods to emulate navigator.credentials.create and navigator.credentials.get from the WebAuthn API.
Note: All WebAuthn emulator methods are asynchronous and return Promises. Make sure to use await or .then() when calling these methods.
import WebAuthnEmulator from "webauthn-emulator";
const emulator = new WebAuthnEmulator();
const origin = "https://example.com";
await emulator.create(origin, creationOptions);
await emulator.get(origin, requestOptions);You can also use the createJSON and getJSON methods to perform emulation with JSON data based on the WebAuthn API specification.
await emulator.createJSON(origin, creationOptionsJSON);
await emulator.getJSON(origin, requestOptionsJSON);These JSON specifications are defined as standard specification data in the following:
- https://www.w3.org/TR/webauthn-3/#dictdef-authenticationresponsejson
- https://www.w3.org/TR/webauthn-3/#dictdef-registrationresponsejson
The WebAuthnEmulator class emulates the following FIDO2/CTAP Authenticator by default:
- Automatically performs User Verification (sets the
uvflag) - Supports ES256, RS256, and EdDSA algorithms
- Emulates a USB-connected CTAP2 device
- AAGUID is
NID-AUTH-3141592 - Increments the Sign Counter by
1during authentication
These settings can be changed by creating an AuthenticatorEmulator class and passing it to the WebAuthnEmulator class to modify the Authenticator behavior as follows:
import WebAuthnEmulator, { AuthenticatorEmulator } from "webauthn-emulator";
const authenticator = new AuthenticatorEmulator({
algorithmIdentifiers: ["ES256"],
verifications: {
userVerified: false,
userPresent: false,
},
signCounterIncrement: 0,
});
const webAuthnEmulator = new WebAuthnEmulator(authenticator);AuthenticatorEmulator implements the following commands from the FIDO2/CTAP specification:
authenticatorMakeCredential(CTAP2): Creating credentialsauthenticatorGetAssertion(CTAP2): Retrieving credentialsauthenticatorGetInfo(CTAP2): Getting authenticator information
These are typically not used directly, but are called internally by the WebAuthnEmulator class according to the CTAP protocol as follows:
const authenticatorRequest = packMakeCredentialRequest({
clientDataHash: createHash("sha256").update(clientDataJSON).digest(),
rp: options.publicKey.rp,
user: options.publicKey.user,
pubKeyCredParams: options.publicKey.pubKeyCredParams,
excludeList: options.publicKey.excludeCredentials,
options: {
rk: options.publicKey.authenticatorSelection?.requireResidentKey,
uv: options.publicKey.authenticatorSelection?.userVerification !== "discouraged",
},
});
const authenticatorResponse = unpackMakeCredentialResponse(this.authenticator.command(authenticatorRequest));Example with WebAuthn.io
Here's an example of usage with webauthn.io, a well-known WebAuthn demo site. You can find working test code examples in the integration tests.
// Initialize the Origin and WebAuthn API emulator
// Here we use https://webauthn.io as the Origin
const origin = "https://webauthn.io";
const emulator = new WebAuthnEmulator();
const webauthnIO = await WebAuthnIO.create();
const user = webauthnIO.getUser();
// Display Authenticator information
console.log("Authenticator Information", await emulator.getAuthenticatorInfo());
// Create passkey using WebAuthn API Emulator
const creationOptions = await webauthnIO.getRegistrationOptions(user);
const creationCredential = await emulator.createJSON(origin, creationOptions);
await webauthnIO.getRegistrationVerification(user, creationCredential);
// Verify authentication with webauthn.io
const requestOptions = await webauthnIO.getAuthenticationOptions();
const requestCredential = await emulator.getJSON(origin, requestOptions);
await webauthnIO.getAuthenticationVerification(requestCredential);Automated Testing with Playwright
This library is intended for use in Passkeys E2E testing, particularly with Playwright. For Playwright testing, you can easily use the WebAuthn API emulator with the utility class BrowserInjection. Here's how to use it:
import WebAuthnEmulator, {
BrowserInjection,
type PublicKeyCredentialCreationOptionsJSON,
type PublicKeyCredentialRequestOptionsJSON,
} from "webauthn-emulator";
async function startWebAuthnEmulator(page: Page, origin: string, debug = false, relatedOrigins: string[] = []) {
const emulator = new WebAuthnEmulator();
await page.exposeFunction(
BrowserInjection.WebAuthnEmulatorCreate,
async (optionsJSON: PublicKeyCredentialCreationOptionsJSON) => {
const response = await emulator.createJSON(origin, optionsJSON, relatedOrigins);
return response;
},
);
await page.exposeFunction(
BrowserInjection.WebAuthnEmulatorGet,
async (optionsJSON: PublicKeyCredentialRequestOptionsJSON) => {
const response = await emulator.getJSON(origin, optionsJSON, relatedOrigins);
return response;
},
);
await page.exposeFunction(
BrowserInjection.WebAuthnEmulatorSignalUnknownCredential,
async (options: UnknownCredentialOptionsJSON) => {
await emulator.signalUnknownCredential(options);
},
);
await page.exposeFunction(
BrowserInjection.WebAuthnEmulatorSignalAllAcceptedCredentials,
async (options: AllAcceptedCredentialsOptionsJSON) => {
await emulator.signalAllAcceptedCredentials(options);
},
);
await page.exposeFunction(
BrowserInjection.WebAuthnEmulatorSignalCurrentUserDetails,
async (options: CurrentUserDetailsOptionsJSON) => {
await emulator.signalCurrentUserDetails(options);
},
);
}
test.describe("Passkeys Tests", { tag: ["@daily"] }, () => {
test("Passkeys login test", async ({ page }) => {
// Exposed functions defined once per page initially
// Related origins can be specified as needed
const relatedOrigins = ["https://sub.example.com", "https://alt.example.com"];
await startWebAuthnEmulator(page, env, true, relatedOrigins);
await page.goto("https://example.com/passkeys/login");
// Start hooking Passkeys WebAuthn API
// Must be executed after page navigation
await page.evaluate(BrowserInjection.HookWebAuthnApis);
});
});startWebAuthnEmulator uses Playwright's exposeFunction to inject the WebAuthnEmulator's createJSON and getJSON methods into the browser context. This makes the WebAuthnEmulator class's get and create APIs available under the window object in the Playwright test context.
window.webAuthnEmulatorGet: Exposed Function forWebAuthnEmulator.getJSONwindow.webAuthnEmulatorCreate: Exposed Function forWebAuthnEmulator.createJSONwindow.webAuthnEmulatorSignalUnknownCredential: Exposed Function forWebAuthnEmulator.signalUnknownCredentialwindow.webAuthnEmulatorSignalAllAcceptedCredentials: Exposed Function forWebAuthnEmulator.signalAllAcceptedCredentialswindow.webAuthnEmulatorSignalCurrentUserDetails: Exposed Function forWebAuthnEmulator.signalCurrentUserDetails
Additionally, the startWebAuthnEmulator function supports a relatedOrigins parameter. This allows requests from different origins to use the same RP ID. This is useful when using Passkeys in multi-domain environments (such as example.com and sub.example.com). The value of relatedOrigins is the same as the content of /.well-known/webauthn hosted on the domain specified by the RP ID.
These are defined globally per Page, so they need to be defined only once per Page instance.
Next, to hook WebAuthn APIs like navigator.credentials.get, evaluate BrowserInjection.HookWebAuthnApis in the test context.
await page.evaluate(BrowserInjection.HookWebAuthnApis);BrowserInjection.HookWebAuthnApis is a serialized string of a JavaScript function that, when evaluated, performs the following operations:
- Overrides the definition of
navigator.credentials.getto callwindow.webAuthnEmulatorGet - Overrides the definition of
navigator.credentials.createto callwindow.webAuthnEmulatorCreate - Adds the definition of
PublicKeyCredential.signalUnknownCredentialto callwindow.webAuthnEmulatorSignalUnknownCredential
This ensures that the WebAuthnEmulator methods defined earlier with exposeFunction are executed when navigator.credentials.get and navigator.credentials.create are called. These processes include serialization and deserialization of data for communication between the test context and Playwright context.
Custom Credential Storage
By default, the AuthenticatorEmulator stores credentials in memory using PasskeysCredentialsMemoryRepository. For testing scenarios that require persistent storage or database-backed credential management, you can implement the PasskeysCredentialsRepository interface.
Repository Interface
All repository methods are asynchronous and return Promises, allowing you to use database operations:
export interface PasskeysCredentialsRepository {
saveCredential(credential: PasskeyDiscoverableCredential): Promise<void>;
deleteCredential(credential: PasskeyDiscoverableCredential): Promise<void>;
loadCredentials(): Promise<PasskeyDiscoverableCredential[]>;
/**
* Execute operations within a transaction.
* All operations within the transaction function are guaranteed to be atomic.
* This is critical for sign count updates to prevent race conditions.
*/
transaction<T>(fn: (repo: PasskeysCredentialsRepository) => Promise<T>): Promise<T>;
}Transaction Support
The transaction method is essential for atomic operations, particularly for sign counter updates during authentication. The sign counter is a critical security feature in WebAuthn that helps detect cloned credentials. Without atomic updates, concurrent authentication requests could result in race conditions where the sign counter is not properly incremented.
Example of a race condition without transactions:
- Request A reads signCount = 5
- Request B reads signCount = 5 (before A saves)
- Request A saves signCount = 6
- Request B saves signCount = 6 (❌ should be 7!)
With transactions, the emulator ensures:
- Request A: transaction starts → reads 5 → saves 6 → transaction commits
- Request B: waits → transaction starts → reads 6 → saves 7 → transaction commits
Database-Backed Repository Example
Here's an example of implementing a PostgreSQL-backed credential repository:
import { PasskeysCredentialsRepository, PasskeyDiscoverableCredential } from "webauthn-emulator";
import { Pool } from "pg";
class PostgresCredentialsRepository implements PasskeysCredentialsRepository {
constructor(private pool: Pool) {}
async saveCredential(credential: PasskeyDiscoverableCredential): Promise<void> {
const serialized = JSON.stringify(credential);
const id = this.getCredentialId(credential);
await this.pool.query(
`INSERT INTO credentials (id, data) VALUES ($1, $2)
ON CONFLICT (id) DO UPDATE SET data = $2`,
[id, serialized]
);
}
async deleteCredential(credential: PasskeyDiscoverableCredential): Promise<void> {
const id = this.getCredentialId(credential);
await this.pool.query('DELETE FROM credentials WHERE id = $1', [id]);
}
async loadCredentials(): Promise<PasskeyDiscoverableCredential[]> {
const result = await this.pool.query('SELECT data FROM credentials');
return result.rows.map(row => JSON.parse(row.data));
}
async transaction<T>(fn: (repo: PasskeysCredentialsRepository) => Promise<T>): Promise<T> {
const client = await this.pool.connect();
try {
await client.query('BEGIN');
// Create a transactional repository using the same client
const txRepo = new PostgresTransactionalRepository(client);
const result = await fn(txRepo);
await client.query('COMMIT');
return result;
} catch (error) {
await client.query('ROLLBACK');
throw error;
} finally {
client.release();
}
}
private getCredentialId(credential: PasskeyDiscoverableCredential): string {
// Generate unique ID from RP ID and user ID
return `${credential.publicKeyCredentialSource.rpId.value}:${Buffer.from(credential.user.id).toString('base64')}`;
}
}
// Use custom repository with AuthenticatorEmulator
const repository = new PostgresCredentialsRepository(pool);
const authenticator = new AuthenticatorEmulator({
credentialsRepository: repository
});
const emulator = new WebAuthnEmulator(authenticator);Built-in Repository Options
The library provides two built-in repository implementations:
- PasskeysCredentialsMemoryRepository (default): Stores credentials in memory
- PasskeysCredentialsFileRepository: Stores credentials as JSON files on disk
import { AuthenticatorEmulator, PasskeysCredentialsFileRepository } from "webauthn-emulator";
// Use file-based storage
const repository = new PasskeysCredentialsFileRepository("./credentials");
const authenticator = new AuthenticatorEmulator({
credentialsRepository: repository
});Both built-in repositories implement transaction locking using promise-based queuing to ensure atomic operations, even without database-level transaction support.
License
This project is licensed under the terms of the MIT license. See LICENSE.md for more info.
