Usage
This page walks through a complete passkey flow — from registration to authentication — using the JavaScript WebAuthn API on the client side and the Laravel Passkey API on the server side.
Overview
The passkey flow consists of two independent flows:
- Registration — Associate a new passkey with an authenticated user
- Authentication — Log in using a registered passkey
Both flows follow the same two-step pattern: get options → submit result.
Registration flow
Step 1 — Get registration options
Call POST /api/passkeys/register/options while authenticated to get the WebAuthn challenge and options:
const response = await fetch('/api/passkeys/register/options', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiToken}`,
},
body: JSON.stringify({
app_name: 'My Application',
app_url: 'https://example.com',
}),
})
const options = await response.json()
Step 2 — Create the credential
Pass the options to the browser's navigator.credentials.create() API:
const credential = await navigator.credentials.create({
publicKey: {
...options,
challenge: Uint8Array.from(atob(options.challenge), c => c.charCodeAt(0)),
user: {
...options.user,
id: Uint8Array.from(atob(options.user.id), c => c.charCodeAt(0)),
},
},
})
Step 3 — Register the passkey
Submit the credential to POST /api/passkeys/register:
await fetch('/api/passkeys/register', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiToken}`,
},
body: JSON.stringify({
label: 'My MacBook',
id: credential.id,
rawId: btoa(String.fromCharCode(...new Uint8Array(credential.rawId))),
type: credential.type,
response: {
clientDataJSON: btoa(String.fromCharCode(...new Uint8Array(credential.response.clientDataJSON))),
attestationObject: btoa(String.fromCharCode(...new Uint8Array(credential.response.attestationObject))),
},
}),
})
Authentication flow
Step 1 — Get verification options
Call POST /api/passkeys/verify/options (no authentication required) to get the challenge. You must provide the credential_id of the passkey to use:
const response = await fetch('/api/passkeys/verify/options', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
credential_id: credentialId, // base64url credential ID
}),
})
const options = await response.json()
Step 2 — Get the assertion
Pass the options to the browser's navigator.credentials.get() API:
const assertion = await navigator.credentials.get({
publicKey: {
...options,
challenge: Uint8Array.from(atob(options.challenge), c => c.charCodeAt(0)),
allowCredentials: options.allowCredentials?.map(c => ({
...c,
id: Uint8Array.from(atob(c.id), c => c.charCodeAt(0)),
})),
},
})
Step 3 — Authenticate
Submit the assertion to POST /api/passkeys/login to receive an API token:
const authResponse = await fetch('/api/passkeys/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
id: assertion.id,
rawId: btoa(String.fromCharCode(...new Uint8Array(assertion.rawId))),
type: assertion.type,
response: {
clientDataJSON: btoa(String.fromCharCode(...new Uint8Array(assertion.response.clientDataJSON))),
authenticatorData: btoa(String.fromCharCode(...new Uint8Array(assertion.response.authenticatorData))),
signature: btoa(String.fromCharCode(...new Uint8Array(assertion.response.signature))),
},
}),
})
const { user, token } = await authResponse.json()
// For token-based actions (Sanctum/Passport): use `token` as the Bearer token
// For session-based action (CreateWebSessionAction): the session cookie is set automatically
POST /api/passkeys/login endpoint does not require authentication. The shape of the response depends on the auth_action configured in config/passkey.php — it can return a Sanctum token, a Passport token, or simply establish a web session.