Ship production-ready passkey authentication without becoming a WebAuthn expert
Project website »
Documentation
·
Quick start
·
Demo
Tip
Use our LLM Agent Skill to supercharge Codex, Claude, Copilot or your coding agent of choice 🤖
- Passlock handles WebAuthn complexity (browser quirks, ceremonies, encoding)
- Your backend authorizes each registration/authentication ceremony and sends a one-time token to the browser
- Your frontend completes the passkey ceremony using a simple JS API, resulting in a code and id_token (JWT)
- Your backend exchanges the code or verifies the JWT using our server library or REST API.
- You stay in control of users, sessions, and authorization
No SDK lock-in. No backend coupling.
This monorepo contains the public browser SDK, server SDK, CLI, and a reference SvelteKit example.
- Developers looking for flexible integration options
- Teams needing to launch quickly, then adopt advanced features as the need arises
- Organizations who don't want to be locked into a product, framework or ecosystem
🔓 No lock-in
Framework agnostic. Standards compliant.
🚀 Zero config passkeys
Works out of the box with sensible defaults.
➡️ Related origins
Migrate user passkeys to a new domain.
📱 Credential management
Manage passkeys on end-user devices.
💪 Powerful
User verification, autofill, roaming authenticators and more.
You can be up and running with a working passkey flow in minutes 🚀
Create a new Passlock tenancy:
npx @passlock/cli initTake a note of your Tenancy ID and API Key.
Passkey registration is a three-step process:
- Authorize registration: Your backend generates a registration token for the user.
- Browser ceremony: The browser asks the user to register a passkey.
- Exchange code: Your backend exchanges the code returned by the browser for a registered passkey.
Tip
You only need to pass tokens (strings) between your backend and the browser, avoiding the need to handle JSON and binary data.
// backend/registration.ts
import { Passlock } from "@passlock/server";
const tenancyId = "myTenancyId";
const apiKey = "myApiKey";
const passlock = new Passlock({ tenancyId, apiKey });
const result = await passlock.authorizePasskeyRegistration(
{
rpId: "example.com",
userId: "user_123",
username: "jdoe@gmail.com",
displayName: "Jane Doe",
}
);
if (result.failure) {
// handle the error
throw new Error(result.error.message);
}
// send only this token to your frontend
console.log("registration token: %s", result.value.registrationToken);// frontend/register.ts
import { Passlock } from "@passlock/browser";
const tenancyId = "myTenancyId";
const passlock = new Passlock({ tenancyId });
// call this in a click handler or similar action
// ask your backend for a registration token
const registrationToken = await fetchRegistrationToken();
const result = await passlock.registerPasskey({ registrationToken });
if (result.failure) {
// handle the error
throw new Error(result.error.message);
}
// send result.code or result.id_token to your backend for verification
console.log("code: %s", result.value.code);In your backend, exchange the code to obtain details about the completed registration. We'll use the @passlock/server library for this, but you can also make vanilla REST calls or verify the id_token instead.
// backend/register.ts
import { Passlock } from "@passlock/server";
const tenancyId = "myTenancyId";
const apiKey = "myApiKey";
const passlock = new Passlock({ tenancyId, apiKey });
const result = await passlock.exchangeCode({ code });
if (result.failure) {
// handle the error
throw new Error(result.error.message);
}
// includes details about the completed registration
// link the authenticatorId to a local user account
console.log("user id: %s", result.value.userId);
console.log("passkey id: %s", result.value.authenticatorId);Passkey authentication follows the same backend-authorized pattern as registration:
- Authorize authentication: Your backend decides the authentication policy and generates an authentication token.
- Browser ceremony: The browser asks the user to present a passkey.
- Exchange code: Your backend exchanges the code returned by the browser and looks up the user.
// backend/authorize-authentication.ts
import { Passlock } from "@passlock/server";
const tenancyId = "myTenancyId";
const apiKey = "myApiKey";
const passlock = new Passlock({ tenancyId, apiKey });
const result = await passlock.authorizePasskeyAuthentication({
rpId: "example.com",
discoverable: true,
});
if (result.failure) {
// handle the error
throw new Error(result.error.message);
}
// send only this token to your frontend
console.log("authentication token: %s", result.value.authenticationToken);// frontend/authenticate.ts
import { Passlock } from "@passlock/browser";
const tenancyId = "myTenancyId";
const passlock = new Passlock({ tenancyId });
// call this in a button click handler or similar action
// ask your backend for an authentication token
const authenticationToken = await fetchAuthenticationToken();
const result = await passlock.authenticatePasskey({ authenticationToken });
if (result.failure) {
// handle the error
throw new Error(result.error.message);
}
// send result.code or result.id_token to your backend for verification
console.log("code: %s", result.value.code);Tip
To authenticate against a known account, authorize with userId, allowCredentials, or both. For discoverable login, set discoverable: true; for autofill, also set mediation: "conditional".
In your backend, exchange the code and look up the user by userId or authenticatorId ...
// backend/authenticate.ts
import { Passlock } from "@passlock/server";
const tenancyId = "myTenancyId";
const apiKey = "myApiKey";
const passlock = new Passlock({ tenancyId, apiKey });
const result = await passlock.exchangeCode({ code });
if (result.failure) {
// handle the error
throw new Error(result.error.message);
}
// lookup the user based on their userId or authenticatorId
console.log("user id: %s", result.value.userId);
console.log("passkey id: %s", result.value.authenticatorId);Tip
Not using a JS backend? The examples in this README use our @passlock/server server library, but this is not required. Passlock works similarly to OAuth2/OpenID Connect, so you can make vanilla HTTP calls or use any suitable JWT library to verify an id_token (JWT).
Please see the tutorial and documentation
If Passlock saved you time or helped you ship passkeys faster, a ⭐ on GitHub helps more than you think.