Getting Started

Usage

End-to-end example of passkey registration and authentication with Laravel Passkey API.

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:

  1. Registration — Associate a new passkey with an authenticated user
  2. 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:

register.js
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:

register.js
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:

register.js
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:

login.js
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:

login.js
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:

login.js
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
The 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.