omdasoft/laravel-webauthn

A WebAuthn (passkeys) backend API package for Laravel — API-first, action-based, and fully customizable.

Maintainers

Package info

github.com/omdasoft/laravel-webauthn

Documentation

pkg:composer/omdasoft/laravel-webauthn

Fund package maintenance!

omdasoft

Statistics

Installs: 3

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.0 2026-04-21 18:45 UTC

This package is auto-updated.

Last update: 2026-04-23 22:37:01 UTC


README

Latest Version on Packagist Total Downloads Tests PHPStan License

A Laravel package that provides a backend API implementation for WebAuthn (passkeys) authentication, designed for API-first applications with separate frontends.

Perfect for:

  • Single Page Applications (SPAs)
  • Mobile applications
  • Headless Laravel setups

The package provides:

  • API-only WebAuthn endpoints - Pure JSON API suitable for SPAs and mobile apps.
  • Action-based architecture - Core logic is separated into dedicated Action classes for easy customization.
  • Configurable models - Support for custom Passkey and User models.
  • Event-driven - Dispatches WebauthnLogin upon successful authentication.
  • InteractWithPasskeys trait - Easy integration with your User (Authenticatable) model.
  • Configurable API routes - Customizable prefix and middleware support.

Demo

Passkey registration and login demo

Project Status

This package is stable and ready for production use. All breaking changes are documented in CHANGELOG.md and follow Semantic Versioning.

Found a bug? Open an issue. Want to contribute? Read CONTRIBUTING.md.

Requirements

  • PHP ^8.3
  • Laravel 10|11|12|13

Installation

Install the package via Composer:

composer require omdasoft/laravel-webauthn

Publish the config, migration, and translations:

php artisan vendor:publish --provider="Omdasoft\LaravelWebauthn\LaravelWebauthnServiceProvider"

Or publish individually:

php artisan vendor:publish --tag="webauthn-config"
php artisan vendor:publish --tag="webauthn-migrations"
php artisan vendor:publish --tag="webauthn-translations"

Run migrations:

php artisan migrate

Configuration

After publishing, you can configure the package in config/webauthn.php.

  • relying_party.id

    • The relying party ID used for WebAuthn (usually the domain without protocol).
    • Set WEBAUTHN_RELYING_PARTY_ID in your .env.
    • Example: example.com
  • models.passkey

    • The model class used for storing passkeys.
    • Default: Omdasoft\LaravelWebauthn\Models\Passkey
  • models.authenticatable

    • Your application's user model class.
    • Default: App\Models\User
  • actions.handle_login

    • The class that handles user login after successful WebAuthn assertion (login).
    • Built-in options:
      • Omdasoft\LaravelWebauthn\Actions\Login\HandleSanctumLogin (Default)
      • Omdasoft\LaravelWebauthn\Actions\Login\HandleSessionLogin
    • You can also create your own by implementing HandleLoginAction.
WEBAUTHN_RELYING_PARTY_ID=example.com
WEBAUTHN_RELYING_PARTY_NAME="My Awesome App"
WEBAUTHN_ROUTE_PREFIX=api/webauthn
WEBAUTHN_STORAGE_DRIVER=cache
WEBAUTHN_CHALLENGE_TTL=3600

Challenge Storage

WebAuthn requires a challenge to be stored on the server between the initial "options" request and the final verification request. You can choose how this is stored:

  • cache (Default): Recommended for stateless APIs and applications using Sanctum with Bearer tokens. The challenge is retrieved using the challenge_id sent by the client.
  • session: Recommended for stateful applications (Inertia.js, Livewire, or Blade). This requires the client to support cookies to persist the session between requests.

Update your .env to choose the driver:

WEBAUTHN_STORAGE_DRIVER=cache

Translations

The package includes translatable error messages. You can publish them to customize the text:

php artisan vendor:publish --tag="webauthn-translations"

The translations will be available in resources/lang/vendor/webauthn/en/errors.php.

Flexibility and Custom Auth

This package is designed to be flexible. It works with Sanctum (default), Session, JWT, or any other authentication system.

Customizing Middleware

Update your config/webauthn.php:

'middlewares' => [
    'register' => ['auth:sanctum'], // Protect registration
    'login' => [], // Usually public
],

Customizing Login Logic

If you want to use standard Laravel sessions instead of Sanctum tokens, update config/webauthn.php:

'actions' => [
    'handle_login' => \Omdasoft\LaravelWebauthn\Actions\Login\HandleSessionLogin::class,
],

For completely custom logic (e.g., JWT), create a class that implements HandleLoginAction:

namespace App\Actions;

use Omdasoft\LaravelWebauthn\Contracts\HandleLoginAction;
use Illuminate\Contracts\Auth\Authenticatable;

class MyCustomLoginHandler implements HandleLoginAction
{
    public function execute(Authenticatable $user): array
    {
        // Your custom logic here
        return ['status' => 'success', 'custom_field' => 'value'];
    }
}

Model setup

Add the InteractWithPasskeys trait and HasPasskey contract to your user model:

use Omdasoft\LaravelWebauthn\Contracts\HasPasskey;
use Omdasoft\LaravelWebauthn\Traits\InteractWithPasskeys;
use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable implements HasPasskey
{
    use InteractWithPasskeys;
}

Customizing User Identification

If you want to change how the user's name or display name is sent to the authenticator (e.g., for the passkey creation prompt), you can override these methods in your User model:

public function getPasskeyIdentifier(): string // Default: $this->id
{
    return (string) $this->uuid;
}

public function getPasskeyName(): string // Default: $this->email
{
    return $this->username;
}

public function getPasskeyDisplayName(): string // Default: $this->name
{
    return $this->full_name;
}

API Routes and Endpoints

The package registers the following routes under your configured prefix (default: webauthn):

Registration (Attestation)

  • POST /register/options - Get creation options.
  • POST /register - Submit attestation response. Accepts an optional name field to label the passkey.

Login (Assertion)

  • POST /login/options - Get assertion options.
  • POST /login - Submit assertion response.

Error Handling

The package throws specific exceptions when something goes wrong. These exceptions return translatable messages:

  • ChallengeMissingException: The challenge ID was not provided.
  • ChallengeNotFoundException: The challenge has expired or does not exist.
  • UserUnauthenticatedException: Registration attempted without being logged in.
  • PasskeyNotFoundException: The passkey requested for login was not found.
  • UserNotFoundException: The user associated with the passkey was not found.

Events

The package dispatches:

  • Omdasoft\LaravelWebauthn\Events\WebauthnLogin: Dispatched after a successful login.
public function handle(WebauthnLogin $event)
{
    // $event->user is the logged-in user
}

Frontend Implementation

Since this is an API-first package, you need a frontend library to interact with the browser's WebAuthn API. We recommend using @simplewebauthn/browser.

1. Registering a Passkey

import { startRegistration } from '@simplewebauthn/browser';

const registerPasskey = async () => {
    // 1. Get registration options from your Laravel API
    const resp = await axios.post('/webauthn/register/options');
    const { challenge_id, passkey: options } = resp.data;

    // 2. Start the browser registration process
    const attestationResponse = await startRegistration(options);

    // 3. Send the response back to your API to complete registration
    await axios.post('/webauthn/register', {
        challenge_id,
        passkey: attestationResponse,
        name: 'My MacBook Pro' // Optional name for the passkey
    });
};

2. Logging in with a Passkey

import { startAuthentication } from '@simplewebauthn/browser';

const loginWithPasskey = async () => {
    try {
        // 1. Get authentication options
        const resp = await axios.post('/webauthn/login/options');
        const { challenge_id, passkey: options } = resp.data;

        // 2. Pass options to the browser API
        const assertionResponse = await startAuthentication(options);

        // 3. Complete authentication
        const loginResp = await axios.post('/webauthn/login', {
            challenge_id,
            passkey: assertionResponse
        });

        // 4. Handle success (e.g., redirect or update state)
        window.location.href = '/dashboard';
    } catch (error) {
        console.error('Passkey authentication failed', error);
    }
};

Testing and quality

composer ci

License

The MIT License (MIT). Please see License File for more information.