Show HN: Open Passkey – open-source passkey auth with free "backendless" host
hackernews
|
|
📰 뉴스
#review
원문 출처: hackernews · Genesis Park에서 요약 및 분석
요약
오픈 소스 라이브러리인 Open Passkey는 하이브리드 포스트 퀀텀 서명 검증을 지원하며, Go, TypeScript, Python 등 다양한 언어와 프레임워크에서 패스키 인증을 쉽게 구현할 수 있게 합니다. 현재 ES256 알고리즘은 프로덕션 환경에서 사용 가능하며, 브라우저 지원이 기다려지는 포스트 퀀텀 알고리즘도 검증이 완료된 상태입니다. 또한 별도의 등록 없이 무료로 사용할 수 있는 Locke Gateway를 통해 백엔드 없이도 클라이언트 사이드 인증 구현이 가능합니다.
본문
An open-source library for adding passkey authentication to any app. Built on WebAuthn with hybrid post-quantum signature verification (ML-DSA-65-ES256). Available for Go, TypeScript, Python, Java, .NET, and Rust. Status: Production-ready for ES256 passkeys. Post-quantum algorithms verified but awaiting browser 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 | Python | Java | .NET | Rust | |---|---|---|---|---|---|---|---|---| | ML-DSA-65-ES256 (composite) | -52 | IETF Draft | Yes | Yes | Yes | Yes | Yes | Yes | | ML-DSA-65 (PQ only) | -49 | NIST FIPS 204 | Yes | Yes | Yes | Yes | Yes | Yes | | ES256 (ECDSA P-256) | -7 | Generally Available | Yes | Yes | Yes | Yes | Yes | Yes | 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. No application code changes needed when PQ support arrives in browsers. Pick your framework and add passkey auth in minutes. Every example is in examples/ . Use Locke Gateway as a free hosted passkey backend with no registration and no API keys. // Any framework, just add your domain new PasskeyClient({ provider: "locke-gateway", rpId: "app.example.com" }) // React // Vue createPasskey({ provider: "locke-gateway", rpId: "app.example.com" }) // Angular providePasskey({ provider: "locke-gateway", rpId: "app.example.com" }) import express from "express"; import { createPasskeyRouter, MemoryChallengeStore, MemoryCredentialStore } from "@open-passkey/express"; const app = express(); app.use(express.json()); app.use("/passkey", createPasskeyRouter({ rpId: "localhost", rpDisplayName: "My App", origin: "http://localhost:3001", challengeStore: new MemoryChallengeStore(), credentialStore: new MemoryCredentialStore(), })); app.listen(3001); import ( "net/http" passkey "github.com/locke-inc/open-passkey/packages/server-go" ) p, _ := passkey.New(passkey.Config{ RPID: "localhost", RPDisplayName: "My App", Origin: "http://localhost:4001", ChallengeStore: passkey.NewMemoryChallengeStore(), CredentialStore: passkey.NewMemoryCredentialStore(), }) 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) http.ListenAndServe(":4001", mux) Handlers are standard http.HandlerFunc — works directly with Chi, Gorilla, net/http, etc. For Echo and Fiber, see the examples for thin adapter wrappers. from fastapi import FastAPI from fastapi.staticfiles import StaticFiles from open_passkey_fastapi import create_passkey_router, PasskeyConfig app = FastAPI() app.include_router( create_passkey_router(PasskeyConfig( rp_id="localhost", rp_display_name="My App", origin="http://localhost:5002", )), prefix="/passkey", ) import { PasskeyProvider, usePasskeyRegister, usePasskeyLogin } from "@open-passkey/react"; function App() { return ( ); } function PasskeyDemo() { const { register, status: regStatus } = usePasskeyRegister(); const { authenticate, status: authStatus, result } = usePasskeyLogin(); return ( <> register("user-1", "Alice")} disabled={regStatus === "pending"}> Register Passkey authenticate("user-1")} disabled={authStatus === "pending"}> Sign In ); } const passkey = new OpenPasskey.PasskeyClient({ baseUrl: "/passkey" }); // Register const reg = await passkey.register("user-1", "Alice"); // Authenticate const auth = await passkey.authenticate("user-1"); // app.config.ts import { providePasskey } from "@open-passkey/angular"; export const appConfig = { providers: [providePasskey({ baseUrl: "/passkey" })], }; // app.component.ts — headless components with content projection @Component({ imports: [PasskeyRegisterComponent, PasskeyLoginComponent], template: ` Register Sign In `, }) export class AppComponent { /* ... */ } See all framework examples below. open-passkey/ ├── spec/vectors/ # 31 shared JSON test vectors ├── packages/ │ ├── core-go/ # Go core protocol (ES256, ML-DSA-65, composite) │ ├── core-ts/ # TypeScript core protocol │ ├── core-py/ # Python core protocol │ ├── core-java/ # Java core protocol │ ├── core-dotnet/ # .NET core protocol │ ├── core-rust/ # Rust core protocol │ ├── server-ts/ # Shared TS server logic (Passkey class) │ ├── server-go/ # Go HTTP bindings (stdlib http.HandlerFunc) │ ├── server-express/ # Express.js │ ├── server-fastify/ # Fastify │ ├── server-hono/ # Hono (edge-compatible) │ ├── server-nestjs/ # NestJS │ ├── server-nextjs/ # Next.js App Router │ ├── server-nuxt/ # Nuxt 3 (Nitro) │ ├── server-sveltekit/ # SvelteKit │ ├── server-remix/ # Remix │ ├── server-astro/ # Astro │ ├── server-py/ # Shared Python server logic (PasskeyHandler) │ ├── server-flask/ # Flask (thin wrapper around server-py) │ ├── server-fastapi/ # FastAPI (thin wrapper around server-py) │ ├── server-django/ # Django (thin wrapper around server-py) │ ├── server-spring/ # Spring Boot │ ├── server-aspnet/ # ASP.NET Core │ ├── server-axum/ # Axum (Rust) │ ├── sdk-js/ # Vanilla JS client (base for all frontend SDKs) │ ├── react/ # React hooks + provider │ ├── vue/ # Vue composables + plugin │ ├── svelte/ # Svelte stores │ ├── solid/ # SolidJS primitives │ ├── angular/ # Angular components + service │ └── authenticator-ts/ # Software authenticator for testing ├── examples/ # Working example for every framework └── tools/vecgen/ # Test vector generator The core protocol is pure WebAuthn/FIDO2 verification logic with no framework dependencies. Server packages (server-ts , server-go , server-py ) contain shared business logic; framework bindings are thin adapters (~50-80 lines). Frontend SDKs all wrap @open-passkey/sdk (PasskeyClient ), which handles the browser WebAuthn API and HTTP calls — framework packages only add framework-specific state management (React hooks, Vue refs, Svelte stores, Angular DI). Adding passkey support to a new framework only requires writing an adapter, not reimplementing cryptography or client logic. Every core library passes the same 31 shared test vectors. Zero framework dependencies. | Package | Language | Crypto (ES256) | PQ (ML-DSA-65) | CBOR | |---|---|---|---|---| core-go | Go | crypto/ecdsa | cloudflare/circl | fxamacker/cbor | core-ts | TypeScript | node:crypto | @noble/post-quantum | cbor-x | core-py | Python | cryptography | oqs (liboqs) | cbor2 | core-java | Java | BouncyCastle | BouncyCastle bcpqc | Jackson CBOR | core-dotnet | C# | System.Security.Cryptography | BouncyCastle.Cryptography | PeterO.Cbor | core-rust | Rust | p256 + ecdsa | fips204 | ciborium | All server bindings expose 4 POST endpoints: | Endpoint | Purpose | |---|---| POST /passkey/register/begin | Start registration ceremony | POST /passkey/register/finish | Complete registration | POST /passkey/login/begin | Start authentication ceremony | POST /passkey/login/finish | Complete authentication | All share @open-passkey/server — a framework-agnostic Passkey class with challenge management and store interfaces. | Package | Framework | Init Pattern | |---|---|---| @open-passkey/express | Express.js | app.use("/passkey", createPasskeyRouter(config)) | @open-passkey/fastify | Fastify | fastify.register(passkeyPlugin, config) | @open-passkey/hono | Hono | app.route("/passkey", createPasskeyApp(config)) | @open-passkey/nestjs | NestJS | PasskeyModule.forRoot(config) | @open-passkey/nextjs | Next.js | createPasskeyHandlers(config) → route handlers | @open-passkey/nuxt | Nuxt 3 | createPasskeyHandlers(config) → Nitro handlers | @open-passkey/sveltekit | SvelteKit | createPasskeyHandlers(config) → +server.ts | @open-passkey/remix | Remix | createPasskeyActions(config) → action functions | @open-passkey/astro | Astro | createPasskeyEndpoints(config) → API routes | Single Go module with standard http.HandlerFunc handlers. Pluggable ChallengeStore and CredentialStore interfaces. Works directly with any framework that accepts http.HandlerFunc (Chi, Gorilla, net/http). For Echo and Fiber, examples show thin adapter wrappers (~5 lines). | Package | Init Pattern | |---|---| server-go | p.BeginRegistration etc. as http.HandlerFunc , or p.Handler() as http.Handler | | Package | Framework | Init Pattern | |---|---|---| server-flask | Flask | app.register_blueprint(create_passkey_blueprint(config)) | server-fastapi | FastAPI | app.include_router(create_passkey_router(config)) | server-django | Django | configure(...) + include(passkey_urls) | | Package | Framework | Init Pattern | |---|---|---| server-spring | Spring Boot | Auto-config via application.properties | server-aspnet | ASP.NET Core | app.MapPasskeyEndpoints(config) | server-axum | Axum (Rust) | passkey_router(config, stores) → Axum Router | Client-side only. All wrap @open-passkey/sdk (PasskeyClient ), which handles the browser WebAuthn API, base64url encoding, PRF extension decoding, and HTTP calls to any open-passkey server. Framework packages add only framework-specific state management. | Package | Framework | API | Wraps | |---|---|---|---| @open-passkey/sdk | Vanilla JS / tag | new PasskeyClient({ baseUrl }) | — (canonical) | @open-passkey/react | React | usePasskeyRegister() , usePasskeyLogin() | PasskeyClient | @open-passkey/vue | Vue 3 | usePasskeyRegister() , usePasskeyLogin() | PasskeyClient | @open-passkey/svelte | Svelte | createPasskeyClient() → stores | PasskeyClient | @open-passkey/solid | SolidJS | createPasskeyRegister() , createPasskeyLogin() | PasskeyClient | @open-passkey/angular | Angular | PasskeyRegisterComponent , PasskeyLoginComponent | PasskeyClient | The SDK also ships an IIFE bundle (dist/open-passkey.iife.js ) for use via tag — all server-only examples (Go, Python, Rust, .NET, Java, Node.js) use this. Every framework binding has a working example in examples/ . Each is a complete passkey registration + authentication demo. # Pick any example: cd examples/express && npm install && npm start cd examples/fiber && go run main.go cd examples/fastapi && pip install -r requirements.txt && python app.py Frontend examples (use Locke Gateway — no server to run): | Example | Framework | Port | Frontend SDK | |---|---|---|---| examples/react | React | 3015 | @open-passkey/react | examples/vue | Vue 3 | 3013 | @open-passkey/vue | examples/angular | Angular | 4200 | @open-passkey/angular | examples/solid | SolidJS | 3011 | @open-passkey/solid | Full-stack examples (self-hosted, in-memory stores): | Example | Framework | Port | Frontend | |---|---|---|---| examples/express | Express | 3001 | SDK ( ) | examples/fastify | Fastify | 3002 | SDK ( ) | examples/hono | Hono | 3003 | SDK ( ) | examples/nestjs | NestJS | 3009 | SDK ( ) | examples/nextjs | Next.js | 3004 | React SDK (@open-passkey/react ) | examples/nuxt | Nuxt 3 | 3005 | Vue SDK (@open-passkey/vue ) | examples/sveltekit | SvelteKit | 3006 | Svelte SDK (@open-passkey/svelte ) | examples/remix | Remix | 3007 | React SDK (@open-passkey/react ) | examples/astro | Astro | 3008 | SDK ( ) | examples/gin | Go (stdlib) | 4001 | SDK ( ) | examples/nethttp | Go net/http | 4002 | SDK ( ) | examples/echo | Go Echo | 4003 | SDK ( ) | examples/fiber | Go Fiber | 4004 | SDK ( ) | examples/chi | Go Chi | 4005 | SDK ( ) | examples/flask | Flask | 5001 | SDK ( ) | examples/fastapi | FastAPI | 5002 | SDK ( ) | examples/django | Django | 5003 | SDK ( ) | examples/spring | Spring Boot | 8080 | SDK ( ) | examples/aspnet | ASP.NET Core | 5000 | SDK ( ) | examples/axum | Axum (Rust) | 3000 | SDK ( ) | - Attestation: none andpacked (self-attestation + full x5c certificate chain) - Backup flags: BE/BS exposed in results, spec conformance enforced (SS6.3.3) - PRF extension: Salt generation, per-credential evaluation, output passthrough - E2E Encrypted Vault: localStorage -style API — see Vault (PRF) below - userHandle: Cross-checked against credential owner in discoverable flow - Sign count: Rollback detection per SS7.2 - Token binding: "present" rejected,"supported" allowed - Algorithm negotiation: ML-DSA-65-ES256 preferred, ML-DSA-65 second, ES256 fallback open-passkey includes an end-to-end encrypted key-value store powered by the WebAuthn PRF extension. The server only ever sees ciphertext — encryption keys are derived on the client from the authenticator's hardware secret and never leave the browser. const passkey = new PasskeyClient({ baseUrl: "/passkey" }); // userId is REQUIRED for vault — see "Why userId is required" below const result = await passkey.authenticate("[email protected]"); const vault = passkey.vault(); await vault.setItem("secret", "hunter2"); const value = await vault.getItem("secret"); // "hunter2" // Persist the encryption key so the vault survives page refreshes await vault.persistKey(); // stores non-extractable CryptoKey in IndexedDB On subsequent page loads, restore the vault without re-authenticating: const vault = await Vault.restore("/passkey"); if (vault) { const value = await vault.getItem("secret"); // works immediately } - During registration, the server generates a random 32-byte PRF salt and stores it with the credential - During authentication, the server sends the salt back in the WebAuthn request options ( extensions.prf.evalByCredential ) - The authenticator evaluates HMAC(credentialSecret, salt) and returns a 32-byte PRF output - The SDK derives an AES-256-GCM key via HKDF-SHA256(prfOutput, salt="open-passkey-vault", info="aes-256-gcm") setItem encrypts with a random 12-byte IV;getItem decrypts. The server stores opaque ciphertext PRF salts must be included in the WebAuthn request options before navigator.credentials.get() is called. To include the correct salt, the server must look up the user's credentials — which requires knowing the userId upfront. When authenticate() is called without a userId (discoverable credentials / OS passkey picker), the server cannot include PRF salts because it doesn't know which credential will be selected. The authentication succeeds, but prf.results.first is undefined and vault() will throw. Rule of thumb: if your app uses the vault, always pass userId to authenticate() . The derived CryptoKey is non-extractable — even JavaScript cannot read the raw key bytes. When stored in IndexedDB via persistKey() , it can only be used for encrypt/decrypt operations through the Web Crypto API. Call Vault.clear() on logout to remove it. | Method | Description | |---|---| vault.persistKey() | Store the derived CryptoKey in IndexedDB | Vault.restore(baseUrl, sessionToken?) | Load a persisted vault (returns null if not found) | Vault.clear() | Remove the pe
Genesis Park 편집팀이 AI를 활용하여 작성한 분석입니다. 원문은 출처 링크를 통해 확인할 수 있습니다.
공유