Weave Code
Code Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Passkeys Laravel Package

laravel/passkeys

Add passwordless WebAuthn/passkey authentication to Laravel. Install migrations, add a trait/contract to your User model, and use the @laravel/passkeys JS client for registration and login. Includes built-in routes for login, confirmation, and passkey management.

View on GitHub
Deep Wiki
Context7

Getting Started

Minimal Setup for First Use Case (User Registration)

  1. Install the package:

    composer require laravel/passkeys
    php artisan vendor:publish --tag=passkeys-migrations
    php artisan migrate
    
  2. Add trait to your User model (e.g., app/Models/User.php):

    use Laravel\Passkeys\PasskeyAuthenticatable;
    
    class User extends Authenticatable implements PasskeyUser
    {
        use PasskeyAuthenticatable;
    }
    
  3. Frontend integration (e.g., in your login blade or SPA):

    import { Passkeys } from '@laravel/passkeys';
    await Passkeys.register({ name: 'My Device' }); // Triggers WebAuthn flow
    
  4. Test registration:

    • Visit /user/passkeys/options (returns WebAuthn options for registration).
    • Submit the response to /user/passkeys to store the passkey.

First Login Flow

  1. Guest visits /passkeys/login/options → Returns WebAuthn challenge.
  2. Frontend submits assertion to /passkeys/login → Authenticates user.
  3. User redirected (default: /) or custom response (via PasskeyLoginResponse).

Key Files to Review First

  • config/passkeys.php: Adjust allowed_origins, guard, or middleware.
  • app/Models/User.php: Override getPasskeyDisplayName() if needed.
  • routes/web.php: Verify auto-registered routes aren’t conflicting.

Implementation Patterns

Workflow: Passwordless Login

  1. Frontend:

    // 1. Fetch options
    const options = await fetch('/passkeys/login/options').then(r => r.json());
    // 2. Show authenticator prompt (e.g., Touch ID)
    const credential = await navigator.credentials.get({ publicKey: options });
    // 3. Submit assertion
    await fetch('/passkeys/login', {
        method: 'POST',
        body: JSON.stringify({ credential }),
        headers: { 'Content-Type': 'application/json' },
    });
    
  2. Backend:

    • Route handles VerifyPasskey action → triggers PasskeyVerified event.
    • Use authorizeLoginUsing callback to block suspended accounts:
      Passkeys::authorizeLoginUsing(function ($request, $user, $passkey) {
          return !$user->is_banned; // Custom logic
      });
      

Workflow: Passkey Management (Add/Delete)

  1. Register a new passkey (authenticated user):

    const options = await fetch('/user/passkeys/options').then(r => r.json());
    const credential = await navigator.credentials.create({ publicKey: options });
    await fetch('/user/passkeys', { method: 'POST', body: JSON.stringify({ credential }) });
    
  2. Delete a passkey:

    await fetch(`/user/passkeys/${passkeyId}`, { method: 'DELETE' });
    

Integration Tips

  • Hybrid Auth: Combine with existing sessions:
    // In PasskeyLoginResponse::toResponse()
    auth()->login($user, true); // Persist session
    
  • Multi-Factor: Use GenerateVerificationOptions for reauth:
    $options = app(GenerateVerificationOptions::class)($request->user());
    // Frontend submits to `/passkeys/confirm` (no login).
    
  • Custom Models: Bind models early in AppServiceProvider:
    Passkeys::useUserModel(CustomUser::class);
    Passkeys::usePasskeyModel(CustomPasskey::class);
    

Gotchas and Tips

Pitfalls

  1. Origin Mismatch:

    • Ensure config('passkeys.allowed_origins') includes all frontend domains (e.g., app.example.com, staging.app.example.com).
    • Debug: Check browser console for NotAllowedError if origins don’t match.
  2. Pessimistic Locking:

    • Passkey verification uses selectForUpdate()avoid in read-heavy apps without transactions.
    • Fix: Wrap custom actions in DB::transaction() if needed.
  3. User Handle Secrets:

    • Defaults to APP_KEY. Change PASSKEYS_USER_HANDLE_SECRET in .env for production.
    • Regenerate if compromised (requires re-registering all passkeys).
  4. Middleware Conflicts:

    • management_middleware (e.g., password.confirm) may block API routes. Exclude with:
      Passkeys::ignoreRoutes(); // Then register custom routes.
      

Debugging

  • WebAuthn Errors:
    • Use webauthn-json-serializer to inspect raw responses:
      use Webauthn\Serializer\Json\JsonSerializer;
      $serializer = new JsonSerializer();
      $serializer->serialize($options); // Log to debug.
      
  • Database Issues:
    • Check passkeys table for orphaned records if PasskeyUser contract isn’t implemented.
    • Run php artisan passkeys:prune (if available) to clean up failed registrations.

Extension Points

  1. Custom Attestation:

    • Extend StorePasskey to validate attestation statements:
      class CustomStorePasskey extends StorePasskey
      {
          public function validateAttestation(AttestationStatement $statement): void
          {
              if (!$statement->isFidoU2f()) {
                  throw new \RuntimeException('Unsupported attestation.');
              }
          }
      }
      
      Bind in AppServiceProvider:
      $this->app->bind(StorePasskey::class, CustomStorePasskey::class);
      
  2. Rate Limiting:

    • Override throttle config or use middleware:
      Passkeys::middleware(['throttle:10,1']); // 10 attempts/minute.
      
  3. Passkey Metadata:

    • Add columns to passkeys table (e.g., device_type, last_used_at) and extend Passkey model:
      protected $casts = ['last_used_at' => 'datetime'];
      

Pro Tips

  • AAGUID Catalog: Auto-synced from GitHub. For air-gapped systems, manually update vendor/webauthn/webauthn-lib/src/Attestation/AAGUIDCatalog.php.
  • Testing: Use @laravel/passkeys npm package with webauthn-mock:
    import { mockPasskeys } from '@laravel/passkeys/testing';
    mockPasskeys(); // Simulates passkey flows in tests.
    
  • Fallbacks: Combine with traditional auth:
    if (auth()->attempt(['email' => $email, 'password' => $password])) {
        // Fallback to password login.
    }
    
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
jayeshmepani/jpl-moshier-ephemeris-php
elnasnato/laraliveui
labrodev/rest-sdk
sampaui/sampaui
babelqueue/php-sdk
facebook/capi-param-builder-php
babelqueue/symfony
hamzi/corewatch
minionfactory/raw-hydrator
hexters/coinpayment
rjcodes/rjcms
act-training/laravel-permissions-manager
alimarchal/laravel-chart-of-accounts
babenkoivan/elastic-scout-driver
mkwebdesign/filament-watchdog-v5
renatomarinho/laravel-page-speed
zedmagdy/filament-business-hours
renatovdemoura/blade-elements-ui
devgeek/beacon-admin
benjamin-rqt/data-watcher-bundle