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.
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.
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.
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.
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
import { PasskeyProvider } from "@open-passkey/react"; <PasskeyProvider provider="locke-gateway" rpId="example.com"> <App /> </PasskeyProvider>
Or vanilla JavaScript
<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 | ||||||
| ML-DSA-65 (PQ only) | -49 |
NIST FIPS 204 | ||||||
| ES256 (ECDSA P-256) | -7 |
Generally Available |
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.
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.
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.
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.
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
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.
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.
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.
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.
import { providePasskey } from "@open-passkey/angular"; export const appConfig = { providers: [ providePasskey({ provider: "locke-gateway", rpId: "example.com" }), ], };
Components
<!-- 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.
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.
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- ES256 + none attestation
- Packed self-attestation
- Packed full x5c chain
- Backup flags (BE/BS)
- RP ID / challenge / origin mismatch
- Invalid attestation statement
- + more failure cases
ES256 Auth
12 vectors- Valid ES256 signature
- User verification required
- Sign count validation
- Tampered signature
- Sign count rollback
- RP ID / challenge mismatch
- + more failure cases
Hybrid PQ Auth
6 vectors- ML-DSA-65-ES256 composite
- RP ID mismatch
- Challenge mismatch
- ML-DSA component tampered
- ECDSA component tampered
- Wrong ceremony type
# 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
Contributing
Strict TDD. To add a new test case:
- Add a vector to
spec/vectors/(or updatetools/vecgen/main.goand regenerate) - Run tests in all languages, the new vector should fail
- Implement the fix in each language
- 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.