Skip to content

Passkey Setup Guide

This guide covers advanced passkey authentication setup for your application.

  • HTTPS-enabled domain (or localhost for development)
  • Modern browser with WebAuthn support
  • Orbital API credentials

Install the WebAuthn browser library:

Terminal window
npm install @simplewebauthn/browser

Create a registration form:

<form id="register-form">
<input type="email" id="email" placeholder="Email" required />
<input type="text" id="displayName" placeholder="Display Name" required />
<button type="submit">Register with Passkey</button>
</form>

Handle registration:

import { startRegistration } from '@simplewebauthn/browser';
document.getElementById('register-form')?.addEventListener('submit', async (e) => {
e.preventDefault();
const email = (document.getElementById('email') as HTMLInputElement).value;
const displayName = (document.getElementById('displayName') as HTMLInputElement).value;
try {
const invitationCode = 'abc123xyz';
// Step 1: Begin registration with invitation
const beginResp = await fetch(`/api/auth/passkey/begin?invite=${invitationCode}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
});
if (!beginResp.ok) {
throw new Error('Failed to begin registration');
}
const { publicKey, sessionId } = await beginResp.json();
// Step 2: Create credential
const credential = await startRegistration(publicKey);
// Step 3: Complete registration
const completeResp = await fetch('/api/auth/passkey/complete', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'SessionID': sessionId,
},
body: JSON.stringify(credential),
});
if (!completeResp.ok) {
throw new Error('Registration failed');
}
alert('Registration successful! You can now log in.');
} catch (error) {
console.error('Registration error:', error);
alert('Registration failed. Please try again.');
}
});

Create a login button:

<button id="login-btn">Sign in with Passkey</button>

Handle authentication:

import { startAuthentication } from '@simplewebauthn/browser';
document.getElementById('login-btn')?.addEventListener('click', async () => {
try {
// Step 1: Begin authentication
const beginResp = await fetch('/api/auth/passkey/begin', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({}),
});
if (!beginResp.ok) {
throw new Error('Failed to begin login');
}
const { publicKey, sessionId } = await beginResp.json();
// Step 2: Get credential
const credential = await startAuthentication(publicKey);
// Step 3: Complete authentication
const completeResp = await fetch('/api/auth/passkey/complete', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'SessionID': sessionId,
},
body: JSON.stringify(credential),
credentials: 'include',
});
if (!completeResp.ok) {
throw new Error('Authentication failed');
}
// Auth token is set as httpOnly cookie
window.location.href = '/dashboard';
} catch (error) {
console.error('Login error:', error);
alert('Login failed. Please try again.');
}
});

Show/hide UI based on passkey availability:

async function checkPasskeySupport() {
if (!window.PublicKeyCredential) {
return { supported: false, available: false };
}
const supported = await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();
return {
supported: true,
available: supported,
};
}
// Use it
const { supported, available } = await checkPasskeySupport();
if (!available) {
// Show fallback authentication
document.getElementById('passkey-login')?.style.display = 'none';
document.getElementById('password-login')?.style.display = 'block';
}

Allow passkeys in form autofill:

async function conditionalLogin() {
if (!window.PublicKeyCredential?.isConditionalMediationAvailable) {
return;
}
const available = await PublicKeyCredential.isConditionalMediationAvailable();
if (available) {
const beginResp = await fetch('/api/auth/passkey/begin', {
method: 'POST',
body: JSON.stringify({}),
});
const { publicKey, sessionId } = await beginResp.json();
// This will show in the autofill UI
const credential = await startAuthentication(publicKey, true);
await fetch('/api/auth/passkey/complete', {
method: 'POST',
headers: { 'SessionID': sessionId },
body: JSON.stringify(credential),
});
}
}
// Call on page load
conditionalLogin();

Allow users to register multiple passkeys:

async function listUserPasskeys() {
const resp = await fetch('/api/auth/passkeys', {
credentials: 'include',
});
const passkeys = await resp.json();
return passkeys;
}
async function deletePasskey(credentialId: string) {
await fetch(`/api/auth/passkeys/${credentialId}`, {
method: 'DELETE',
credentials: 'include',
});
}

Typical issues to check:

  • NotAllowedError – user cancelled the prompt or it timed out
  • InvalidStateError – credential already registered or reused
  • NotSupportedError – browser does not support WebAuthn
  • SecurityError – non‑HTTPS origin or RP ID mismatch

For local development:

  1. Use localhost (not 127.0.0.1)
  2. Configure RP ID correctly:
// In your API configuration
{
rpId: 'localhost',
rpName: 'Orbital (Dev)',
origin: 'http://localhost:3000',
}