# Convex Passkey-auth Passwordless WebAuthn passkey authentication — self-minted JWTs, multi-device support, session management, and React hooks for Convex. This is an upstream package. It has not been adapted or qualified for Bijection. - Package: convex-passkey-auth - Source: https://github.com/TimpiaAI/convex-passkey-auth - npm: https://www.npmjs.com/package/convex-passkey-auth - Directory: https://bijection.com/components/convex-passkey-auth Read the upstream README for setup, license, and compatibility before using it. ## Upstream package documentation Recorded 2026-10-23. This describes convex-passkey-auth; check the Bijection adaptation separately. This component provides complete WebAuthn passkey authentication for Convex apps, handling the full challenge/response flow with self-minted HMAC-SHA256 JWTs. It includes multi-device support, session management, and React hooks for registration and login. The component eliminates passwords entirely while providing secure session tokens compatible with Convex's auth system. ### Use cases • **Replace traditional password auth** with WebAuthn passkeys for better security and user experience • **Build multi-device authentication** where users can authenticate from phones, laptops, and hardware security keys • **Implement passwordless onboarding** with passkey registration and automatic user creation • **Add secure session management** with configurable expiry, refresh tokens, and logout capabilities • **Deploy preview environments** without hardcoded origins since rpId is derived from the client ### How it works The component provides a `PasskeyAuth` class that handles WebAuthn challenge generation and verification through methods like `generateRegistrationOptions` and `verifyAuthentication`. You create Convex mutations that wrap these methods, then use the provided React hooks (`usePasskeyRegister`, `usePasskeyLogin`, `usePasskeyAuth`) to handle the browser WebAuthn APIs. Session tokens are HMAC-SHA256 signed JWTs that integrate directly with Convex's auth system. The `validateSession` method checks token validity and returns user data, while `getOrCreateUser` provides stable user identifiers from passkey credentials. The component stores passkeys, sessions, and challenges in Convex tables with automatic cleanup via cron jobs. ### Upstream README # convex-passkey-auth A [Convex](https://convex.dev) component for passwordless WebAuthn passkey authentication with self-minted JWTs, multi-device support, session management, and React hooks. ## Features - **Passkey registration** - Register new passkeys and associate them with user identifiers - **Challenge/response authentication** - Full WebAuthn challenge/response flow via browser APIs - **Self-minted JWTs** - HMAC-SHA256 signed session tokens compatible with Convex auth - **Session management** - Configurable expiry and automatic refresh strategy - **Multi-device support** - Multiple passkeys per user (phone, laptop, security key) - **`getOrCreateUser` helper** - Stable user identifier from passkey ID - **Preview deployment support** - No hardcoded origins; rpId derived from client - **React hooks** - `usePasskeyRegister`, `usePasskeyLogin`, `usePasskeyAuth` - **Server-side validation** - Validate sessions and get current user from any Convex function - **Session invalidation** - Logout (single session) and logout-all (force everywhere) - **Passkey revocation** - Revoke individual passkeys when devices are lost ## Installation ```bash npm install convex-passkey-auth ``` ## Setup ### 1. Add the component to your Convex app ```ts // convex/convex.config.ts import { defineApp } from "convex/server"; import passkeyAuth from "convex-passkey-auth/convex.config"; const app = defineApp(); app.use(passkeyAuth); export default app; ``` ### 2. Create server-side helpers ```ts // convex/auth.ts import { PasskeyAuth } from "convex-passkey-auth"; import { components } from "./_generated/api"; export const passkeyAuth = new PasskeyAuth(components.passkeyAuth, { rpName: "My App", sessionExpiryMs: 30 * 24 * 60 * 60 * 1000, // 30 days refreshAfterMs: 24 * 60 * 60 * 1000, // refresh daily }); ``` ### 3. Create Convex mutations for the client ```ts // convex/passkeys.ts import { mutation } from "./_generated/server"; import { v } from "convex/values"; import { passkeyAuth } from "./auth"; export const generateRegistrationChallenge = mutation({ args: { identifier: v.string(), displayName: v.optional(v.string()) }, handler: async (ctx, args) => { return await passkeyAuth.generateRegistrationOptions(ctx, args); }, }); export const verifyRegistration = mutation({ args: { identifier: v.string(), credentialId: v.string(), publicKey: v.string(), challenge: v.string(), counter: v.number(), deviceName: v.optional(v.string()), displayName: v.optional(v.string()), }, handler: async (ctx, args) => { return await passkeyAuth.verifyRegistration(ctx, args); }, }); export const generateAuthChallenge = mutation({ args: { identifier: v.optional(v.string()) }, handler: async (ctx, args) => { return await passkeyAuth.generateAuthenticationOptions(ctx, args); }, }); export const verifyAuth = mutation({ args: { credentialId: v.string(), challenge: v.string(), counter: v.number(), }, handler: async (ctx, args) => { return await passkeyAuth.verifyAuthentication(ctx, args); }, }); export const validateSession = mutation({ args: { tokenHash: v.string() }, handler: async (ctx, args) => { return await passkeyAuth.validateSession(ctx, args.tokenHash); }, }); export const logout = mutation({ args: { tokenHash: v.string() }, handler: async (ctx, args) => { return await passkeyAuth.logout(ctx, args.tokenHash); }, }); ``` ### 4. Use React hooks in your app ```tsx // src/App.tsx import { usePasskeyRegister, usePasskeyLogin, usePasskeyAuth } from "convex-passkey-auth/react"; import { useMutation } from "convex/react"; import { api } from "../convex/_generated/api"; function App() { const generateRegChallenge = useMutation(api.passkeys.generateRegistrationChallenge); const verifyReg = useMutation(api.passkeys.verifyRegistration); const generateAuthChallenge = useMutation(api.passkeys.generateAuthChallenge); const verifyAuth = useMutation(api.passkeys.verifyAuth); const validateSessionMutation = useMutation(api.passkeys.validateSession); const logoutMutation = useMutation(api.passkeys.logout); const { register, isRegistering } = usePasskeyRegister({ generateChallenge: generateRegChallenge, verifyRegistration: verifyReg, rpName: "My App", }); const { login, isLoggingIn } = usePasskeyLogin({ generateChallenge: generateAuthChallenge, verifyAuthentication: verifyAuth, }); const { user, isAuthenticated, isLoading, logout } = usePasskeyAuth({ validateSession: validateSessionMutation, invalidateSession: logoutMutation, }); if (isLoading) return
Loading...
; if (isAuthenticated) { return (

Welcome, {user?.userId}

); } return (
); } ``` ## API Reference ### Server-side (`PasskeyAuth` class) | Method | Description | |--------|-------------| | `generateRegistrationOptions(ctx, { identifier, displayName? })` | Generate WebAuthn registration challenge | | `verifyRegistration(ctx, { identifier, credentialId, publicKey, challenge, counter })` | Verify registration and store passkey | | `generateAuthenticationOptions(ctx, { identifier? })` | Generate WebAuthn authentication challenge | | `verifyAuthentication(ctx, { credentialId, challenge, counter })` | Verify authentication and create session | | `validateSession(ctx, tokenHash)` | Validate a session token | | `logout(ctx, tokenHash)` | Invalidate a single session | | `logoutAll(ctx, userId)` | Invalidate all sessions for a user | | `getOrCreateUser(ctx, { identifier, displayName? })` | Find or create user by identifier | | `getUser(ctx, userId)` | Get user info | | `listPasskeys(ctx, userId)` | List passkeys for a user | | `revokePasskey(ctx, credentialId)` | Revoke a specific passkey | | `cleanupExpiredSessions(ctx)` | Delete expired sessions (for cron) | | `cleanupExpiredChallenges(ctx)` | Delete expired challenges (for cron) | ### React hooks | Hook | Returns | |------|---------| | `usePasskeyRegister(options)` | `{ register, isRegistering, error }` | | `usePasskeyLogin(options)` | `{ login, isLoggingIn, error }` | | `usePasskeyAuth(options)` | `{ user, isAuthenticated, isLoading, logout }` | ### Utilities | Function | Description | |----------|-------------| | `hashSessionToken(token)` | Hash a raw session token for server-side validation | ## Cron Jobs Set up cleanup crons to keep the database tidy: ```ts // convex/crons.ts import { cronJobs } from "convex/server"; import { internal } from "./_generated/api"; const crons = cronJobs(); crons.daily("cleanup sessions", { hourUTC: 3, minuteUTC: 0 }, internal.cleanup.expiredSessions); crons.hourly("cleanup challenges", { minuteUTC: 30 }, internal.cleanup.expiredChallenges); export default crons; ``` ## License MIT