# 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 <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:

```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