Node.js SDK
@passkeybase/node
The @passkeybase/node package provides a server-side SDK for Node.js. It includes the Management API client, Express authentication middleware with JWKS validation, webhook signature verification, and enrollment token generation.
Installation
Install via npm:
npm install @passkeybase/nodeRequirements: Node.js >= 20. The SDK uses nativefetch (no Axios or node-fetch required) and depends on jose for JWT signing and verification. ESM and CJS builds are both provided.
Quick Start
Initialize the ManagementClientwith your M2M credentials and tenant domain. The SDK handles OAuth2client_credentialstoken acquisition, caching, and automatic refresh.
import { ManagementClient } from '@passkeybase/node';
const mgmt = new ManagementClient({
domain: 'myapp.auth.ing',
clientId: 'YOUR_CLIENT_ID',
clientSecret: 'YOUR_CLIENT_SECRET',
});
// List all users
const users = await mgmt.users.list({ page: 1, perPage: 20 });
for (const user of users.data) {
console.log(`${user.id}: ${user.name}`);
}ManagementClient
The ManagementClient is the main entry point for the Management API. It exposes sub-clients for each resource type:users,credentials,clients,roles,tenant, andlogs.
import { ManagementClient } from '@passkeybase/node';
const mgmt = new ManagementClient({
domain: 'myapp.auth.ing', // Required. Your tenant domain.
clientId: 'YOUR_CLIENT_ID', // Required. M2M OAuth client ID.
clientSecret: 'YOUR_CLIENT_SECRET', // Required. M2M OAuth client secret.
audience: 'https://myapp.auth.ing/api/v1/', // Optional. Custom audience.
timeoutMs: 10_000, // Optional. Request timeout in ms. Default: 10000.
maxRetries: 3, // Optional. Retry transient failures. Default: 3.
});| Option | Type | Default | Description |
|---|---|---|---|
domain | string | -- | Your tenant domain (e.g., myapp.auth.ing). |
clientId | string | -- | Machine-to-machine OAuth client ID from the dashboard. |
clientSecret | string | -- | Machine-to-machine OAuth client secret. |
audience | string | https://{domain}/api/v1/ | Custom audience for token requests. |
timeoutMs | number | 10000 | Request timeout in milliseconds. |
maxRetries | number | 3 | Maximum retries for transient failures (5xx, network errors). |
Authentication Middleware (Express)
The requireAuth middleware validates Bearer JWTs from the Authorizationheader using your tenant's JWKS endpoint. On success, it populatesreq.auth with the verified token payload. JWKS keys are cached automatically.
import express from 'express';
import { requireAuth } from '@passkeybase/node/middleware/express';
const app = express();
// Protect all /api routes with JWT verification
app.use('/api', requireAuth({
domain: 'myapp.auth.ing',
audience: 'https://api.myapp.com',
}));
// req.auth is populated with the verified token payload
app.get('/api/me', (req, res) => {
res.json({
userId: req.auth.sub,
email: req.auth.email,
name: req.auth.name,
permissions: req.auth.permissions,
});
});
app.listen(3000);Middleware Options
requireAuth({
domain: 'myapp.auth.ing', // Required. Tenant domain for JWKS lookup.
audience: 'https://api.myapp.com', // Required. Expected audience claim.
algorithm: 'RS256', // Optional. Signing algorithm. Default: 'RS256'.
clockToleranceSeconds: 5, // Optional. Clock skew tolerance. Default: 5.
});| Option | Type | Default | Description |
|---|---|---|---|
domain | string | -- | Tenant domain used to fetch JWKS from https://{domain}/.well-known/jwks.json. |
audience | string | -- | Expected aud claim in the JWT. |
algorithm | string | 'RS256' | Expected signing algorithm. |
clockToleranceSeconds | number | 5 | Clock skew tolerance for token expiration checks. |
Webhook Verification
Verify incoming webhook signatures using HMAC-SHA256. The function validates the signature header and optionally checks the timestamp for replay protection.
import express from 'express';
import { verifyWebhookSignature } from '@passkeybase/node';
const app = express();
app.post(
'/webhooks/passkeybase',
express.raw({ type: 'application/json' }),
(req, res) => {
// Verify the HMAC-SHA256 signature
verifyWebhookSignature(req.body, req.headers, {
secret: process.env.WEBHOOK_SECRET!,
});
const event = JSON.parse(req.body.toString());
switch (event.type) {
case 'user.created':
console.log('New user:', event.data.email);
break;
case 'credential.registered':
console.log('New passkey for:', event.data.userId);
break;
}
res.sendStatus(200);
},
);Verification Options
verifyWebhookSignature(rawBody, headers, {
secret: 'whsec_...', // Required. Shared webhook secret.
signatureHeader: 'x-passkeybase-signature', // Optional. Default shown.
timestampHeader: 'x-passkeybase-timestamp', // Optional. Default shown.
maxAgeSeconds: 300, // Optional. Replay protection window. Default: 300.
});Important: Use express.raw() to receive the raw request body. If you use express.json(), the body will be parsed and the signature verification will fail.
Management API
Users
Create User
const user = await mgmt.users.create({
email: '[email protected]',
name: 'Jane Doe',
appMetadata: { plan: 'pro' },
});
console.log(`Created user: ${user.id}`);List Users
// List with pagination
const users = await mgmt.users.list({ page: 1, perPage: 20 });
for (const user of users.data) {
console.log(`${user.id}: ${user.name} (${user.email})`);
}
console.log(`Total: ${users.total}`);Get User
const user = await mgmt.users.get('usr_abc123');
console.log(`Name: ${user.name}`);
console.log(`Email: ${user.email}`);
console.log(`Logins: ${user.loginsCount}`);
console.log(`Last login: ${user.lastLogin}`);Update User
const user = await mgmt.users.update('usr_abc123', {
name: 'Jane Smith',
blocked: false,
appMetadata: { tier: 'enterprise' },
});
console.log(`Updated: ${user.name}`);Delete User
Deletes a user and all their associated passkeys.
await mgmt.users.delete('usr_abc123');Get User Roles
const roles = await mgmt.users.getRoles('usr_abc123');
for (const role of roles) {
console.log(`Role: ${role.name} (${role.id})`);
}Credentials
List Credentials
List all passkey credentials for a specific user.
const credentials = await mgmt.credentials.list('usr_abc123');
for (const cred of credentials) {
console.log(`${cred.id}: ${cred.name} (${cred.type})`);
console.log(` Last used: ${cred.lastUsedAt}`);
}Delete Credential
Revoke a specific passkey credential.
await mgmt.credentials.delete('cred_xyz789');Clients
Create Client
const app = await mgmt.clients.create({
name: 'My Web App',
appType: 'spa',
callbacks: ['https://myapp.com/callback'],
allowedOrigins: ['https://myapp.com'],
allowedLogoutUrls: ['https://myapp.com'],
});
console.log(`Client ID: ${app.clientId}`);Rotate Secret
const result = await mgmt.clients.rotateSecret('pk_live_abc123');
console.log(`New secret: ${result.clientSecret}`);Roles
Create Role
const role = await mgmt.roles.create({
name: 'admin',
description: 'Administrator role',
});
console.log(`Role ID: ${role.id}`);Assign and Remove Users
// Assign users to a role
await mgmt.roles.assignUsers('role_abc123', ['usr_abc123', 'usr_def456']);
// List users in a role
const users = await mgmt.roles.listUsers('role_abc123');
for (const user of users) {
console.log(`${user.id}: ${user.name}`);
}
// Remove users from a role
await mgmt.roles.removeUsers('role_abc123', ['usr_def456']);Tenant Settings
// Get tenant settings
const settings = await mgmt.tenant.get();
console.log(`Tenant: ${settings.tenantName}`);
console.log(`Domain: ${settings.domain}`);
console.log(`Environment: ${settings.environment}`);
// Update tenant settings
const updated = await mgmt.tenant.update({
tenantName: 'My App (Production)',
supportEmail: '[email protected]',
sessionLifetime: 86400,
});Audit Logs
// List log entries with filters
const logs = await mgmt.logs.list({
page: 1,
perPage: 50,
userId: 'usr_abc123',
type: 'login_success',
from: '2025-01-01T00:00:00Z',
});
for (const entry of logs.data) {
console.log(`${entry.date}: ${entry.type} from ${entry.ip}`);
}
// Get a single log entry
const entry = await mgmt.logs.get('log_abc123');Enrollment Tokens
Generate tokens for enrolling a passkey for an existing user. The SDK supports two approaches: client-side signed JWTs (no Management API call) and server-side enrollment tickets. See the Enrollment guide for the full flow.
Signed Token (HMAC-SHA256)
Creates a JWT signed locally using the client secret and thejose library. No API call required.
import { createEnrollmentToken } from '@passkeybase/node';
// Create a signed enrollment token (JWT with HMAC-SHA256)
const token = await createEnrollmentToken({
clientId: 'YOUR_CLIENT_ID',
clientSecret: 'YOUR_CLIENT_SECRET',
userId: 'usr_abc123',
email: '[email protected]',
expiresInSeconds: 3600, // 1 hour (default)
});
// Return to the frontend for passkey enrollment
// res.json({ enrollmentToken: token });Server-Side Enrollment Ticket
Creates an enrollment ticket via the Management API with a redirect URL.
// Or create a server-side enrollment ticket via the Management API
const ticket = await mgmt.users.createEnrollmentTicket('usr_abc123');
console.log(`Ticket URL: ${ticket.url}`);
console.log(`Expires at: ${ticket.expiresAt}`);Full Express Example
A complete endpoint that verifies the user's JWT and returns an enrollment token for the frontend SDK.
import express from 'express';
import { requireAuth } from '@passkeybase/node/middleware/express';
import { createEnrollmentToken } from '@passkeybase/node';
const app = express();
app.post(
'/api/enrollment-token',
requireAuth({ domain: 'myapp.auth.ing', audience: 'https://api.myapp.com' }),
async (req, res) => {
const token = await createEnrollmentToken({
clientId: process.env.PASSKEYBASE_CLIENT_ID!,
clientSecret: process.env.PASSKEYBASE_CLIENT_SECRET!,
userId: req.auth.sub,
email: req.auth.email!,
});
res.json({ enrollmentToken: token });
},
);Error Handling
All SDK methods throw typed errors extendingPasskeyBaseError. Use instanceof checks for granular error handling.
import {
ManagementClient,
ApiError,
AuthenticationError,
NotFoundError,
RateLimitError,
PasskeyBaseError,
} from '@passkeybase/node';
const mgmt = new ManagementClient({
domain: 'myapp.auth.ing',
clientId: 'YOUR_CLIENT_ID',
clientSecret: 'YOUR_CLIENT_SECRET',
});
try {
const user = await mgmt.users.get('usr_nonexistent');
} catch (err) {
if (err instanceof NotFoundError) {
console.log(`Not found: ${err.message}`); // "resource '/users/usr_nonexistent' not found"
console.log(`Status: ${err.statusCode}`); // 404
} else if (err instanceof RateLimitError) {
console.log(`Rate limited. Retry after: ${err.retryAfterMs}ms`);
} else if (err instanceof AuthenticationError) {
console.log(`Auth error: ${err.message}`);
} else if (err instanceof ApiError) {
console.log(`API error ${err.statusCode}: ${err.message}`);
}
}| Error Class | Description |
|---|---|
PasskeyBaseError | Base error class. Has code and message properties. |
ApiError | API returned an error response. Has statusCode and body. |
AuthenticationError | Invalid or expired M2M credentials, or JWT verification failure. |
NotFoundError | 404 Not Found -- the requested resource does not exist. |
RateLimitError | 429 Too Many Requests. Includes retryAfterMs in milliseconds. |
Next steps
- JavaScript SDK -- core client-side SDK
- React SDK -- hooks and components for React apps
- Enrollment -- add passkeys to existing auth systems
- API Reference -- full Management API documentation
- Python SDK -- server-side SDK for Python backends