Skip to content

passlock-dev/passlock

Repository files navigation

Frictionless passkey authentication in under 30 minutes

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 🤖

How Passlock works (in 60 seconds)

  1. Passlock handles WebAuthn complexity (browser quirks, ceremonies, encoding)
  2. Your backend authorizes each registration/authentication ceremony and sends a one-time token to the browser
  3. Your frontend completes the passkey ceremony using a simple JS API, resulting in a code and id_token (JWT)
  4. Your backend exchanges the code or verifies the JWT using our server library or REST API.
  5. 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.

Who Passlock is for

  • 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

Key features

🔓 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.

Quick start

You can be up and running with a working passkey flow in minutes 🚀

Create a new Passlock tenancy:

npx @passlock/cli init

Take a note of your Tenancy ID and API Key.

Register a passkey

Passkey registration is a three-step process:

  1. Authorize registration: Your backend generates a registration token for the user.
  2. Browser ceremony: The browser asks the user to register a passkey.
  3. 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);

Authenticate a passkey

Passkey authentication follows the same backend-authorized pattern as registration:

  1. Authorize authentication: Your backend decides the authentication policy and generates an authentication token.
  2. Browser ceremony: The browser asks the user to present a passkey.
  3. 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).

More information

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.

About

Passkey authentication for Astro, Sveltekit and other frameworks

Topics

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors