Passkeys (WebAuthn)
Sign in with biometrics, hardware security keys, or platform authenticators. EdgeBase implements the WebAuthn standard using SimpleWebAuthn on the server side, with a full registration and authentication flow.
Passkeys require a WebAuthn-capable environment (modern browsers, iOS 16+, Android 9+). The SDK provides the REST API layer; you use the browser's navigator.credentials API or a native WebAuthn library to handle the actual credential ceremony.
How It Works
- Register: Authenticated user requests registration options, creates a credential via the platform authenticator, and sends the attestation back to the server
- Authenticate: User requests authentication options, signs the challenge via their passkey, and sends the assertion back. The server verifies and creates a session
- Manage: Users can list their registered passkeys and delete individual ones
Configuration
Enable passkeys in your edgebase.config.ts:
// edgebase.config.ts
export default defineConfig({
auth: {
passkeys: {
enabled: true,
rpName: 'My App', // Displayed in authenticator UI
rpID: 'example.com', // Your domain (no protocol, no port)
origin: 'https://example.com', // Expected origin(s) for WebAuthn requests
},
},
});
Configuration Options
| Option | Type | Required | Description |
|---|---|---|---|
enabled | boolean | Yes | Enable WebAuthn/Passkeys |
rpName | string | Yes | Relying Party name shown in the authenticator prompt |
rpID | string | Yes | Relying Party ID — typically your domain (e.g., example.com) |
origin | string | string[] | Yes | Expected origin(s) for WebAuthn requests (e.g., https://example.com). Use an array for multiple origins (web + mobile) |
Local development example:
passkeys: {
enabled: true,
rpName: 'My App (Dev)',
rpID: 'localhost',
origin: 'http://localhost:3000',
},
Registration Flow
Registration adds a new passkey to an already-authenticated user's account. The user must be signed in first (via email/password, OAuth, etc.).
Step 1: Get Registration Options
Request WebAuthn registration options from the server. The server generates a challenge and returns parameters for the browser's navigator.credentials.create() call.
- JavaScript
- Swift
- Kotlin
// 1. Get registration options from EdgeBase
const res = await fetch('/api/auth/passkeys/register-options', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`,
},
});
const { options } = await res.json();
// 2. Create credential using the browser WebAuthn API
// (use @simplewebauthn/browser for convenience)
import { startRegistration } from '@simplewebauthn/browser';
const credential = await startRegistration(options);
// Use ASAuthorizationPlatformPublicKeyCredentialProvider for iOS 16+
// 1. Fetch registration options from /api/auth/passkeys/register-options
// 2. Use the options to create an ASAuthorizationPlatformPublicKeyCredentialRegistrationRequest
// 3. Present via ASAuthorizationController
// Use Android Credential Manager API (Android 9+)
// 1. Fetch registration options from /api/auth/passkeys/register-options
// 2. Create a CreatePublicKeyCredentialRequest with the options
// 3. Call credentialManager.createCredential(context, request)
Step 2: Register the Credential
Send the credential response back to the server for verification and storage.
- JavaScript
- Swift
- Kotlin
// 3. Send the credential to EdgeBase for verification
const registerRes = await fetch('/api/auth/passkeys/register', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`,
},
body: JSON.stringify({ response: credential }),
});
const { credentialId } = await registerRes.json();
// Passkey registered successfully
// Send the ASAuthorizationPlatformPublicKeyCredentialRegistration response
// to POST /api/auth/passkeys/register with the attestation object
// Send the CreatePublicKeyCredentialResponse
// to POST /api/auth/passkeys/register with the attestation response
Authentication Flow
Authentication allows a user to sign in using a registered passkey instead of a password.
Step 1: Get Authentication Options
- JavaScript
- Swift
- Kotlin
// 1. Request authentication options (public endpoint, no auth required)
const res = await fetch('/api/auth/passkeys/auth-options', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: 'user@example.com' }), // optional — narrows to user's credentials
});
const { options } = await res.json();
// 2. Get assertion from the browser
import { startAuthentication } from '@simplewebauthn/browser';
const assertion = await startAuthentication(options);
// Use ASAuthorizationPlatformPublicKeyCredentialAssertionRequest
// 1. Fetch auth options from POST /api/auth/passkeys/auth-options
// 2. Present assertion request via ASAuthorizationController
// Use Android Credential Manager
// 1. Fetch auth options from POST /api/auth/passkeys/auth-options
// 2. Create GetPublicKeyCredentialOption and call credentialManager.getCredential()
Step 2: Authenticate
- JavaScript
- Swift
- Kotlin
// 3. Send the assertion to EdgeBase
const authRes = await fetch('/api/auth/passkeys/authenticate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ response: assertion }),
});
const { accessToken, refreshToken, user } = await authRes.json();
// User is now signed in via passkey
// Send the ASAuthorizationPlatformPublicKeyCredentialAssertion response
// to POST /api/auth/passkeys/authenticate
// Returns accessToken, refreshToken, user
// Send the GetPublicKeyCredentialResponse
// to POST /api/auth/passkeys/authenticate
// Returns accessToken, refreshToken, user
Managing Passkeys
List Registered Passkeys
- JavaScript
const res = await fetch('/api/auth/passkeys', {
headers: { 'Authorization': `Bearer ${accessToken}` },
});
const { passkeys } = await res.json();
// passkeys: [{ id, credentialId, transports, createdAt }, ...]
Delete a Passkey
- JavaScript
await fetch(`/api/auth/passkeys/${encodeURIComponent(credentialId)}`, {
method: 'DELETE',
headers: { 'Authorization': `Bearer ${accessToken}` },
});
REST API
| Method | Endpoint | Auth | Description |
|---|---|---|---|
POST | /auth/passkeys/register-options | Required | Generate WebAuthn registration options |
POST | /auth/passkeys/register | Required | Verify attestation and store credential |
POST | /auth/passkeys/auth-options | Public | Generate WebAuthn authentication options |
POST | /auth/passkeys/authenticate | Public | Verify assertion and create session |
GET | /auth/passkeys | Required | List passkeys for the authenticated user |
DELETE | /auth/passkeys/:credentialId | Required | Delete a passkey |
Request/Response Examples
POST /auth/passkeys/register-options
// Response
{
"options": {
"challenge": "base64url-encoded-challenge",
"rp": { "name": "My App", "id": "example.com" },
"user": { "id": "...", "name": "user@example.com", "displayName": "User" },
"pubKeyCredParams": [...],
"excludeCredentials": [...]
}
}
POST /auth/passkeys/register
// Request
{ "response": { /* WebAuthn attestation response from navigator.credentials.create() */ } }
// Response
{ "credentialId": "base64url-encoded-credential-id" }
POST /auth/passkeys/auth-options
// Request (email is optional)
{ "email": "user@example.com" }
// Response
{
"options": {
"challenge": "base64url-encoded-challenge",
"rpId": "example.com",
"allowCredentials": [{ "id": "...", "transports": ["internal"] }],
"userVerification": "preferred"
}
}
POST /auth/passkeys/authenticate
// Request
{ "response": { /* WebAuthn assertion response from navigator.credentials.get() */ } }
// Response
{
"accessToken": "eyJ...",
"refreshToken": "eyJ...",
"user": { "id": "...", "email": "user@example.com", ... }
}
Architecture Notes
- Credential storage: WebAuthn credentials (public key, counter, transports) are stored in D1 (AUTH_DB) in the
_webauthn_credentialstable - D1 index: A
_passkey_indextable in D1 mapscredentialIdtouserId, enabling credential lookup during authentication - Challenge management: Registration and authentication challenges are stored in KV with a 5-minute TTL and are single-use (deleted after verification)
- PKCE-like flow: The server generates challenges and the client proves possession of the private key, similar to the OAuth PKCE pattern
Discoverable Credentials
If you call /auth/passkeys/auth-options without providing an email, the server generates options without allowCredentials restrictions. This enables discoverable credential (resident key) flows where the authenticator presents all available passkeys for the relying party, and the user selects which one to use.
Recommended Client Libraries
| Platform | Library |
|---|---|
| Web (JavaScript) | @simplewebauthn/browser |
| iOS (Swift) | AuthenticationServices framework (built-in, iOS 16+) |
| Android (Kotlin) | Credential Manager |