convex-passkey-auth
v1.0.1
Published
Convex component for passwordless WebAuthn passkey authentication — self-minted JWTs, multi-device support, session management, and React hooks
Maintainers
Readme
convex-passkey-auth
A Convex 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)
getOrCreateUserhelper - 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
npm install convex-passkey-authSetup
1. Add the component to your Convex app
// 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
// 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
// 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
// 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 <div>Loading...</div>;
if (isAuthenticated) {
return (
<div>
<p>Welcome, {user?.userId}</p>
<button onClick={logout}>Logout</button>
</div>
);
}
return (
<div>
<button
onClick={() => register("[email protected]", "John Doe")}
disabled={isRegistering}
>
{isRegistering ? "Registering..." : "Register Passkey"}
</button>
<button
onClick={() => login("[email protected]")}
disabled={isLoggingIn}
>
{isLoggingIn ? "Logging in..." : "Login with Passkey"}
</button>
</div>
);
}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:
// 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
