open-passkey

An open-source library for adding passkey authentication to any app. Built on WebAuthn with hybrid post-quantum signature verification (ML-DSA-65-ES256) across 6 languages and 20+ frameworks.

Core libraries in Go, TypeScript, Python, Java, .NET, and Rust. Server bindings for Express, Next.js, Fastify, Hono, NestJS, Flask, FastAPI, Django, Spring, ASP.NET, Axum, and more. Client SDKs for React, Vue, Svelte, Solid, and Angular. Use with your own backend or with Locke Gateway for free hosted passkey authentication.

View on GitHub arrow_forward MIT License. Available on npm, PyPI, NuGet, crates.io, Maven Central, and Go modules.

Locke Gateway

Locke Gateway is a free hosted passkey server that works with every open-passkey client SDK. Instead of deploying your own backend, point any frontend at the Gateway with your domain and get full passkey authentication in seconds.

speed

No backend required

The Gateway handles credential storage, challenge management, and ceremony orchestration. Your frontend talks directly to it -- no server code to write or deploy.

lock

Sessions included

After authentication, the Gateway issues HMAC-SHA256 signed session cookies. Your client SDK can check session status and logout without any server-side session infrastructure.

shield

Post-quantum ready

The Gateway advertises hybrid ML-DSA-65-ES256 as the preferred algorithm. When browser authenticators add PQ support, your users are automatically protected -- no code changes needed.

Any frontend framework

React
import { PasskeyProvider } from "@open-passkey/react";

<PasskeyProvider provider="locke-gateway" rpId="example.com">
  <App />
</PasskeyProvider>

Or vanilla JavaScript

html
<script src="@open-passkey/sdk/dist/open-passkey.iife.js"></script>
<script>
  const passkey = new OpenPasskey.PasskeyClient({
    provider: "locke-gateway",
    rpId: "example.com",
  });
</script>

The Gateway works with React, Vue, Svelte, Solid, Angular, and plain JavaScript. If you later decide to self-host, change provider: "locke-gateway" to baseUrl: "/passkey", deploy any of the 18 server bindings, and import your credentials via the Gateway export API (on roadmap). Passkeys are bound to your domain, so your users' existing credentials carry over seamlessly.

Hybrid Post-Quantum Support

open-passkey implements ML-DSA-65-ES256 hybrid composite signatures (draft-ietf-jose-pq-composite-sigs), combining a NIST-standardized post-quantum algorithm with classical ECDSA in a single credential. Both signature components must verify independently, if either is broken, the other still protects you.

Algorithm COSE alg Status Go TS Py Java .NET Rust
ML-DSA-65-ES256 (composite) -52 IETF Draft check check check check check check
ML-DSA-65 (PQ only) -49 NIST FIPS 204 check check check check check check
ES256 (ECDSA P-256) -7 Generally Available check check check check check check

How it works: During registration, the server advertises preferred algorithms in pubKeyCredParams. During authentication, the core libraries read the COSE alg field from the stored credential and dispatch to the correct verifier automatically. ES256, ML-DSA-65, or the composite ML-DSA-65-ES256 path. No application code changes needed.

Browser support note: As of early 2026, no major browser authenticator produces ML-DSA-65 or composite signatures natively. Credentials use ES256 until platform support arrives. Both cores verify all three algorithms today so that when authenticators catch up, your deployment is already protected.

Architecture

open-passkey separates concerns into three layers. Core protocol libraries handle pure WebAuthn/FIDO2 verification with no framework dependencies. Server bindings are thin adapters that wire the core into specific frameworks. Client SDKs handle browser-side WebAuthn API calls and server communication. Adding passkey support to a new framework only requires writing an adapter, not reimplementing cryptography.

Repository Layout
open-passkey/
├── spec/vectors/             # 31 shared JSON test vectors
├── packages/
│   ├── core-{go,ts,py,java,dotnet,rust}/    # Core protocol (6 languages)
│   ├── server-go/            # Go HTTP handlers (stdlib)
│   ├── server-ts/            # Shared TS server logic
│   ├── server-{express,fastify,hono,...}/    # TS framework bindings (9)
│   ├── server-{flask,fastapi,django}/       # Python framework bindings
│   ├── server-{spring,aspnet,axum}/         # Java, .NET, Rust bindings
│   ├── sdk-js/               # Browser SDK (PasskeyClient)
│   ├── {react,vue,svelte,solid,angular}/    # Frontend SDKs
│   └── authenticator-ts/     # Software authenticator (testing)
├── examples/                 # 23 working examples
└── tools/vecgen/             # Test vector generation

Code Examples

Server Setup (server-go)

HTTP handlers for the full WebAuthn ceremony. Works with any Go router. Defaults to hybrid ML-DSA-65-ES256 preferred.

go
import "github.com/locke-inc/open-passkey/packages/server-go"

p, _ := passkey.New(passkey.Config{
    RPID:            "example.com",
    RPDisplayName:   "My App",
    Origin:          "https://example.com",
    ChallengeStore:  passkey.NewMemoryChallengeStore(),
    CredentialStore: myDBCredentialStore,
})

mux := http.NewServeMux()
mux.HandleFunc("POST /passkey/register/begin",  p.BeginRegistration)
mux.HandleFunc("POST /passkey/register/finish", p.FinishRegistration)
mux.HandleFunc("POST /passkey/login/begin",     p.BeginAuthentication)
mux.HandleFunc("POST /passkey/login/finish",    p.FinishAuthentication)

Pluggable interfaces: ChallengeStore (single-use challenge storage; in-memory default provided) and CredentialStore (credential persistence; you implement for your DB).

Core Verification (core-go)

Lower-level API. Algorithm dispatch is automatic based on the stored COSE key.

go
import "github.com/locke-inc/open-passkey/packages/core-go/webauthn"

// Registration
result, err := webauthn.VerifyRegistration(webauthn.RegistrationInput{
    RPID:              "example.com",
    ExpectedChallenge: challengeB64URL,
    ExpectedOrigin:    "https://example.com",
    ClientDataJSON:    credential.Response.ClientDataJSON,
    AttestationObject: credential.Response.AttestationObject,
})
// result.CredentialID, result.PublicKeyCOSE -- store for future auth

// Authentication: dispatches to ES256, ML-DSA-65, or ML-DSA-65-ES256
result, err := webauthn.VerifyAuthentication(webauthn.AuthenticationInput{
    RPID:                "example.com",
    ExpectedChallenge:   challengeB64URL,
    ExpectedOrigin:      "https://example.com",
    StoredPublicKeyCOSE: storedKeyBytes,
    StoredSignCount:     storedCount,
    ClientDataJSON:      credential.Response.ClientDataJSON,
    AuthenticatorData:   credential.Response.AuthenticatorData,
    Signature:           credential.Response.Signature,
})

Algorithm Constants

go
webauthn.AlgES256                  // -7  (ECDSA P-256)
webauthn.AlgMLDSA65                // -49 (ML-DSA-65 / Dilithium3)
webauthn.AlgCompositeMLDSA65ES256  // -52 (hybrid composite)

Express Server (@open-passkey/express)

One function gives you a full passkey API with registration, authentication, and optional session cookies.

typescript
import express from "express";
import {
  createPasskeyRouter,
  MemoryChallengeStore,
  MemoryCredentialStore,
} from "@open-passkey/express";

const app = express();
app.use(express.json());

app.use("/passkey", createPasskeyRouter({
  rpId:            "example.com",
  rpName:          "My App",
  origin:          "https://example.com",
  challengeStore:  new MemoryChallengeStore(),
  credentialStore: new MemoryCredentialStore(),
}));

app.listen(3000);

Also available for Next.js, Fastify, Hono, NestJS, Nuxt, SvelteKit, Remix, and Astro. All are thin wrappers over the same @open-passkey/server logic.

Core Verification (@open-passkey/core)

Lower-level API for custom integrations. Same 31 spec vectors as all other language cores.

typescript
import { verifyRegistration, verifyAuthentication } from "@open-passkey/core";

const result = await verifyRegistration({
  rpId:              "example.com",
  expectedChallenge: challengeB64URL,
  expectedOrigin:    "https://example.com",
  clientDataJSON:    credential.response.clientDataJSON,
  attestationObject: credential.response.attestationObject,
});

Provider + Hooks (@open-passkey/react)

Wrap your app with a provider, then use hooks for registration, login, and session management.

tsx
import { PasskeyProvider, usePasskeyRegister, usePasskeyLogin } from "@open-passkey/react";

// Use Locke Gateway (free, no backend needed)
function App() {
  return (
    <PasskeyProvider provider="locke-gateway" rpId="example.com">
      <MyApp />
    </PasskeyProvider>
  );
}

function LoginButton() {
  const { authenticate, status, result } = usePasskeyLogin();

  return (
    <button onClick={() => authenticate()} disabled={status === "pending"}>
      Sign in with Passkey
    </button>
  );
}

Also available for Vue (@open-passkey/vue), Svelte (@open-passkey/svelte), Solid (@open-passkey/solid), and Angular (@open-passkey/angular).

Provider Setup (@open-passkey/angular)

Headless components with content projection. You provide the UI and the library handles the WebAuthn ceremony.

typescript - app.config.ts
import { providePasskey } from "@open-passkey/angular";

export const appConfig = {
  providers: [
    providePasskey({ provider: "locke-gateway", rpId: "example.com" }),
  ],
};

Components

html
<!-- Registration -->
<passkey-register [userId]="userId" [username]="username"
                  (registered)="onRegistered($event)"
                  (error)="onError($event)" #reg>
  <button (click)="reg.register()" [disabled]="reg.loading()">
    Register Passkey
  </button>
</passkey-register>

<!-- Authentication -->
<passkey-login [userId]="userId"
               (authenticated)="onAuthenticated($event)"
               (error)="onError($event)" #login>
  <button (click)="login.login()" [disabled]="login.loading()">
    Sign in with Passkey
  </button>
</passkey-login>

The client passes through whatever algorithm the server and authenticator negotiate. No client-side changes needed for PQ support.

Flask Server (open-passkey-flask)

Register a blueprint for the full passkey API. Also available for FastAPI and Django.

python
from flask import Flask
from open_passkey_flask import create_passkey_blueprint
from open_passkey_server import MemoryChallengeStore, MemoryCredentialStore

app = Flask(__name__)

passkey = create_passkey_blueprint(
    rp_id="example.com",
    rp_name="My App",
    origin="https://example.com",
    challenge_store=MemoryChallengeStore(),
    credential_store=MemoryCredentialStore(),
)

app.register_blueprint(passkey, url_prefix="/passkey")

Core Verification (open-passkey)

Lower-level API. Same 31 spec vectors as all other language cores.

python
from open_passkey import verify_registration, verify_authentication

result = verify_registration(
    rp_id="example.com",
    expected_challenge=challenge_b64url,
    expected_origin="https://example.com",
    client_data_json=credential["clientDataJSON"],
    attestation_object=credential["attestationObject"],
)

Packages

Core Protocol Libraries

Pure WebAuthn verification. All 6 implementations pass the same 31 shared test vectors.

core-go

Go stdlib crypto + circl

core-ts

Node crypto + @noble/post-quantum

core-py

Python + liboqs

core-java

Java + Bouncy Castle

core-dotnet

.NET + ML-DSA support

core-rust

Rust + pqcrypto

Server Bindings

Thin framework adapters. Registration, authentication, sessions, pluggable credential stores.

server-go

stdlib http

Express

@open-passkey/express

Next.js

@open-passkey/nextjs

Fastify

@open-passkey/fastify

Hono

@open-passkey/hono

NestJS

@open-passkey/nestjs

Nuxt

@open-passkey/nuxt

SvelteKit

@open-passkey/sveltekit

Remix

@open-passkey/remix

Astro

@open-passkey/astro

Flask

open-passkey-flask

FastAPI

open-passkey-fastapi

Django

open-passkey-django

Spring

Java

ASP.NET

.NET

Axum

Rust

Client SDKs

Browser-side WebAuthn API calls, base64url encoding, and server communication. Works with any backend or Locke Gateway.

sdk-js

@open-passkey/sdk (vanilla JS)

React

@open-passkey/react

Vue

@open-passkey/vue

Svelte

@open-passkey/svelte

Solid

@open-passkey/solid

Angular

@open-passkey/angular

Cross-Language Testing

All 6 core implementations run against the same 31 JSON test vectors in spec/vectors/. These contain real WebAuthn payloads generated by a software authenticator, covering happy paths and failure modes across all three algorithm families. When a bug is found in any language, a new vector is added and all implementations gain the test case automatically.

Registration

13 vectors
  • checkES256 + none attestation
  • checkPacked self-attestation
  • checkPacked full x5c chain
  • checkBackup flags (BE/BS)
  • closeRP ID / challenge / origin mismatch
  • closeInvalid attestation statement
  • + more failure cases

ES256 Auth

12 vectors
  • checkValid ES256 signature
  • checkUser verification required
  • checkSign count validation
  • closeTampered signature
  • closeSign count rollback
  • closeRP ID / challenge mismatch
  • + more failure cases

Hybrid PQ Auth

6 vectors
  • checkML-DSA-65-ES256 composite
  • closeRP ID mismatch
  • closeChallenge mismatch
  • closeML-DSA component tampered
  • closeECDSA component tampered
  • closeWrong ceremony type
Run tests
# All tests
./scripts/test-all.sh

# Or individually:
cd packages/core-go && go test ./... -v
cd packages/core-ts && npm test
cd packages/server-ts && npm test
cd packages/angular && npm test

Roadmap

check_circle Core protocol libraries in 6 languages (Go, TS, Python, Java, .NET, Rust)
check_circle 31 shared test vectors: all 6 cores passing
check_circle Hybrid PQ: ML-DSA-65-ES256 composite verification in all languages
check_circle 18 server bindings across 20 frameworks
check_circle Client SDKs: React, Vue, Svelte, Solid, Angular, vanilla JS
check_circle HMAC-SHA256 stateless session cookies across all server languages
check_circle Attestation: none + packed (self-attestation and full x5c)
check_circle Sign count rollback detection, backup flags, PRF extension
check_circle 23 working examples across all supported frameworks
radio_button_unchecked Ruby + PHP core libraries
radio_button_unchecked Rails + Laravel server bindings
radio_button_unchecked Additional attestation formats (TPM, Android)
radio_button_unchecked Gateway credential export API (migrate to self-hosted)
radio_button_unchecked Backup flags enforcement policies

Contributing

Strict TDD. To add a new test case:

  1. Add a vector to spec/vectors/ (or update tools/vecgen/main.go and regenerate)
  2. Run tests in all languages, the new vector should fail
  3. Implement the fix in each language
  4. All vectors pass = done

New language implementation: Create packages/core-{lang}/, write a test runner that loads spec/vectors/*.json, implement until all 31 vectors pass.

View on GitHub arrow_forward Locke Security Architecture Post-Quantum at Locke MIT License. Copyright 2025 Locke Identity Networks Inc.