Skip to content

TimpiaAI/convex-passkey-auth

Repository files navigation

convex-passkey-auth

npm version License: MIT TypeScript

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)
  • 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

npm install convex-passkey-auth

Setup

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("user@example.com", "John Doe")}
        disabled={isRegistering}
      >
        {isRegistering ? "Registering..." : "Register Passkey"}
      </button>
      <button
        onClick={() => login("user@example.com")}
        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

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors