惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Security Latest
Security Latest
Apple Machine Learning Research
Apple Machine Learning Research
D
Docker
美团技术团队
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
宝玉的分享
宝玉的分享
月光博客
月光博客
J
Java Code Geeks
V
V2EX
IT之家
IT之家
T
Troy Hunt's Blog
D
DataBreaches.Net
Cloudbric
Cloudbric
Blog — PlanetScale
Blog — PlanetScale
H
Hackread – Cybersecurity News, Data Breaches, AI and More
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
G
Google Developers Blog
MongoDB | Blog
MongoDB | Blog
The GitHub Blog
The GitHub Blog
Jina AI
Jina AI
Google DeepMind News
Google DeepMind News
Engineering at Meta
Engineering at Meta
博客园 - Franky
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
H
Help Net Security
Application and Cybersecurity Blog
Application and Cybersecurity Blog
S
Security @ Cisco Blogs
N
News and Events Feed by Topic
aimingoo的专栏
aimingoo的专栏
S
Security Affairs
Hugging Face - Blog
Hugging Face - Blog
Forbes - Security
Forbes - Security
AI
AI
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
腾讯CDC
H
Heimdal Security Blog
The Cloudflare Blog
S
SegmentFault 最新的问题
Google Online Security Blog
Google Online Security Blog
Webroot Blog
Webroot Blog
有赞技术团队
有赞技术团队
The Hacker News
The Hacker News
Microsoft Security Blog
Microsoft Security Blog
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
罗磊的独立博客
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
博客园 - 聂微东
Help Net Security
Help Net Security
T
The Exploit Database - CXSecurity.com

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
Laravel Now Has Native Passkeys: A Complete Guide to laravel/passkeys
Hafiz · 2026-05-09 · via DEV Community

Originally published at hafiz.dev


For a long time, adding passkeys to a Laravel app meant reaching for a third-party package, assembling WebAuthn ceremonies by hand, or piecing together a tutorial that assumes you already know what a "relying party ID" is. That's done.

In late April 2026, Laravel shipped laravel/passkeys, a first-party package authored by Taylor Otwell that gives you a complete passkey story out of the box. Server package, npm client, Fortify integration. Three pieces that click together so passwordless auth is boring to wire up, which is exactly what you want from a security feature.

I covered the Spatie passkeys approach back in January, and that's still valid if you're Livewire-heavy or already have that package running. But the native package is the right call for new projects and anything using Fortify. Here's the full setup.

What Ships in laravel/passkeys

The passkey stack has three components that each handle a distinct concern.

laravel/passkeys is the server-side Composer package. It handles WebAuthn ceremonies, manages a passkeys database table, registers routes for login, confirmation, and credential management, and fires events you can hook into. If you need custom authorization logic or your own route definitions, escape hatches are built in.

@laravel/passkeys is the npm client. It handles browser-side ceremony coordination (registration and verification) and ships first-class helpers for React, Vue, and Svelte with SSR-safe hooks so client-only APIs don't fight your framework. The public API is two methods: Passkeys.register() and Passkeys.verify(). That's it.

Fortify integration wires everything together via Features::passkeys() in your app config and a passkeys section in config/fortify.php. Fortify apps get the same endpoints and the PasskeyUser and PasskeyAuthenticatable contracts without reimplementing any glue.

The package is v0.1.0 but that's not a red flag. It's already the default in Laravel's official starter kits and used by Fortify in production. The version number signals that the public API may still evolve, not that the package is unstable.

Installation

Start by pulling in the Composer package:

composer require laravel/passkeys

Enter fullscreen mode Exit fullscreen mode

Publish and run the migrations to create the passkeys table:

php artisan vendor:publish --tag=passkeys-migrations
php artisan migrate

Enter fullscreen mode Exit fullscreen mode

Next, add a secret to your .env for deriving stable opaque user handles. This keeps passkey associations private even if your user IDs are sequential integers:

PASSKEYS_USER_HANDLE_SECRET=your-random-secret-here

Enter fullscreen mode Exit fullscreen mode

Generate a value with:

php artisan key:generate --show

Enter fullscreen mode Exit fullscreen mode

Use that output as your secret. The package falls back to APP_KEY if you leave this blank, but keeping them separate is better practice. If you ever rotate your app key, users won't lose their passkeys. You can find a full reference of available artisan commands in the Laravel Artisan Commands reference.

Configuring Your User Model

Add the PasskeyUser contract and PasskeyAuthenticatable trait to your User model:

<?php

namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;
use Laravel\Passkeys\Contracts\PasskeyUser;
use Laravel\Passkeys\PasskeyAuthenticatable;

class User extends Authenticatable implements PasskeyUser
{
    use PasskeyAuthenticatable;

    // Rest of your model...
}

Enter fullscreen mode Exit fullscreen mode

The trait assumes your users table has name and email columns. Authenticators show these values in their UI during registration and account selection. displayName falls back from name to email to the auth identifier. Same for username.

If you need different display values, override the methods directly on the model:

public function getPasskeyDisplayName(): string
{
    return $this->full_name ?? $this->email;
}

public function getPasskeyUsername(): string
{
    return $this->email;
}

Enter fullscreen mode Exit fullscreen mode

That's the only change your model needs. No extra migrations, no pivot tables. The passkeys table handles credential storage and links to your user via a standard relationship that PasskeyAuthenticatable sets up for you.

Fortify Integration

If you're using Laravel Fortify, enabling passkeys takes one line in your features array:

use Laravel\Fortify\Features;

'features' => [
    Features::registration(),
    Features::resetPasswords(),
    Features::emailVerification(),
    Features::passkeys(), // Add this
],

Enter fullscreen mode Exit fullscreen mode

Fortify automatically registers the passkey routes and wires up the contracts. Nothing else changes on the server side. Your existing authorization setup with policies and gates stays untouched: passkeys only replace the authentication step, not what happens after it.

The Config File

Publish the config if you need to customize anything:

php artisan vendor:publish --tag="passkeys-config"

Enter fullscreen mode Exit fullscreen mode

The defaults in config/passkeys.php are sensible:

return [
    'relying_party_id' => parse_url(config('app.url'), PHP_URL_HOST),
    'allowed_origins' => [config('app.url')],
    'user_handle_secret' => env('PASSKEYS_USER_HANDLE_SECRET', config('app.key')),
    'timeout' => 60000,
    'guard' => 'web',
    'middleware' => ['web'],
    'management_middleware' => ['password.confirm'],
    'throttle' => 'throttle:6,1',
    'redirect' => '/',
];

Enter fullscreen mode Exit fullscreen mode

A few worth understanding before you change anything.

relying_party_id is your domain, derived from APP_URL. Passkeys are cryptographically bound to this value. If the domain the browser accesses doesn't match, the ceremony fails. Make sure APP_URL reflects the actual domain you're serving, especially in local development.

management_middleware defaults to password.confirm, which means users must re-confirm their password before adding or revoking passkeys. Don't disable this. It's the right friction for a security-critical action. The same principle applies here as with sensitive token operations in Passport vs Sanctum.

throttle limits passkey attempts to 6 per minute. Sensible for production. Adjust it if you have unusual traffic patterns, but don't remove it entirely.

Routes the Package Registers

You don't define any routes yourself. The server package registers these automatically:

POST   /passkeys/register/options   (generate registration challenge)
POST   /passkeys/register           (store the new credential)
POST   /passkeys/verify/options     (generate authentication challenge)
POST   /passkeys/verify             (authenticate with passkey)
DELETE /passkeys/{passkey}          (revoke a specific passkey)

Enter fullscreen mode Exit fullscreen mode

If you need custom route definitions (different middleware, prefixes, or custom controllers), you can disable auto-registration in the config and define them yourself. The underlying action classes are all public and importable, so you're not losing functionality by taking manual control.

How the WebAuthn Flow Works

It helps to see the ceremony before writing the frontend code:

View the interactive diagram on hafiz.dev

Registration follows the same pattern: browser requests options, authenticator creates a key pair, public key gets stored on your server. Nothing sensitive ever leaves the device. The private key never travels over the network, which is the core security advantage over passwords. No credentials to steal from a database breach.

Frontend Integration (Vue)

Install the npm client:

npm install @laravel/passkeys
npm run build

Enter fullscreen mode Exit fullscreen mode

Here's a Vue 3 component that handles both registration (authenticated users adding a passkey) and login (on the login page):

<script setup>
import { ref } from 'vue'
import { Passkeys } from '@laravel/passkeys'

const registering = ref(false)
const verifying = ref(false)
const error = ref(null)

async function registerPasskey() {
    registering.value = true
    error.value = null

    try {
        await Passkeys.register({ name: 'My Device' })
        // Passkey saved, refresh the list or show a success toast
    } catch (e) {
        error.value = e.message
    } finally {
        registering.value = false
    }
}

async function loginWithPasskey() {
    verifying.value = true
    error.value = null

    try {
        await Passkeys.verify()
        // Redirects automatically on success
    } catch (e) {
        error.value = e.message
    } finally {
        verifying.value = false
    }
}
</script>

<template>
    <div class="space-y-4">
        <!-- Show on profile/settings for authenticated users -->
        <button @click="registerPasskey" :disabled="registering" class="btn">
            {{ registering ? 'Registering...' : 'Add a Passkey' }}
        </button>

        <!-- Show on your login page -->
        <button @click="loginWithPasskey" :disabled="verifying" class="btn">
            {{ verifying ? 'Verifying...' : 'Sign in with Passkey' }}
        </button>

        <p v-if="error" class="text-red-500 text-sm">{{ error }}</p>
    </div>
</template>

Enter fullscreen mode Exit fullscreen mode

Passkeys.register() handles the full browser ceremony: it fetches the challenge from /passkeys/register/options, prompts the authenticator, and POSTs the resulting credential back to the server. Passkeys.verify() does the same for login and then redirects to the path defined in config/passkeys.php → redirect on success.

For React, the import and API are identical. The Svelte helpers follow the same pattern. The package abstracts all the @simplewebauthn/browser ceremony complexity behind a clean two-method interface, which is what you want when you're not trying to become a WebAuthn expert.

Managing Registered Passkeys

Users should be able to see and revoke their passkeys. This matters more than people expect. Users register on their laptop, their phone, and their work machine, then wonder why three entries show up. Give them the tools to clean it up.

A basic controller looks like this:

// PasskeyController.php
use Illuminate\Http\Request;
use Laravel\Passkeys\Models\Passkey;

class PasskeyController extends Controller
{
    public function index(Request $request)
    {
        $passkeys = $request->user()->passkeys()->latest()->get();

        return view('passkeys.index', compact('passkeys'));
    }

    public function destroy(Request $request, Passkey $passkey)
    {
        $this->authorize('delete', $passkey);

        $passkey->delete();

        return back()->with('status', 'Passkey removed.');
    }
}

Enter fullscreen mode Exit fullscreen mode

The passkeys() relationship is defined by the PasskeyAuthenticatable trait. Each Passkey record has a name, created_at, and last_used_at column. Surface all three in the UI so users can tell which device is which and spot ones they don't recognise.

Wire the delete action to the DELETE /passkeys/{passkey} route the package already registered. The management_middleware (password confirm by default) protects both the management view and the delete action, so users need to re-authenticate before making changes.

Comparing to spatie/laravel-passkeys

Both packages use web-auth/webauthn-lib under the hood and get you to the same outcome. The difference is approach.

laravel/passkeys (native) is first-party and stack-agnostic on the frontend. Right choice for new Laravel 11, 12, or 13 projects and anything using Fortify. If you're starting fresh, use this.

spatie/laravel-passkeys ships Livewire components out of the box. If your app is already Livewire-heavy and you have Spatie's package working, there's no reason to migrate. The earlier passkeys guide covers that setup in full.

Don't run both at the same time. They register overlapping routes and you'll get conflicts.

Things to Get Right Before You Ship

A few things that will save you a debugging session:

HTTPS is required. WebAuthn only works on secure origins. For local development, use valet secure (Valet or Herd) or configure SSL in Sail. If APP_URL uses http://, the browser refuses to run the ceremony entirely. No error message. Just silence.

Keep password auth as a fallback. Not every user is on a passkey-capable device. Passkeys should be additive. Don't remove your existing login form. Make it an option alongside the passkey button, not a replacement for it.

Account recovery needs thought. If a user loses access to all their registered devices, how do they get back in? The package doesn't solve this. Email-based recovery or admin-initiated password resets are the standard approaches. Build this flow before you go live.

Multiple passkeys per user are supported by default. Users register on multiple devices, and that's expected. Your management UI (a list with a revoke button per passkey) handles this. Show name, created_at, and last_used_at so users can make sense of what's there.

The management_middleware default is password.confirm. Users re-confirm their password before adding or revoking passkeys. Don't strip it out. It's the same security pattern you'd apply to any sensitive account action.

Local Development

One thing that trips people up: APP_URL in .env must match the domain you're actually accessing in the browser. A mismatch makes the relying party check fail, and the error can be cryptic.

APP_URL=https://myapp.test

Enter fullscreen mode Exit fullscreen mode

If you're on Valet:

valet secure myapp

Enter fullscreen mode Exit fullscreen mode

That's all you need. The package reads APP_URL for its relying party config automatically.

FAQ

Does this work on Laravel 11 and 12, or only 13?

The package requires illuminate/contracts: ^11.0|^12.0|^13.0, so all three versions are supported. You don't need to upgrade to Laravel 13 to use it.

Do I need Fortify to use this?

No. Fortify integration is optional. The server package works standalone: you define your own routes and handle redirects. Features::passkeys() just automates the setup if Fortify is already in your stack.

What if I'm already using spatie/laravel-passkeys?

Stay on Spatie unless you have a specific reason to switch, especially if the Livewire setup is working. If you do migrate, uninstall the Spatie package and remove its service provider first. Don't run both simultaneously.

Is v0.1.0 stable enough for production?

The package is already the default in Laravel's official starter kits and backed by Fortify. The v0.1.0 label means the public API may evolve, not that it's experimental. For new projects, use it without hesitation.

Can I use this without a JavaScript framework?

Yes. The framework-specific helpers (Vue, React, Svelte) are convenience wrappers around the same core API. If you're using Blade without a frontend framework, you can call Passkeys.register() and Passkeys.verify() from a plain <script> block after importing @laravel/passkeys.

Get It Wired Up

Native passkeys is a small, focused addition to any Laravel project. The config is sensible by default, Fortify integration is a single line, and the frontend API is two method calls. If you're starting a new Laravel project today and want passwordless auth, this is the path.

If you're adding passkeys to an existing production app or migrating a complex auth setup, get in touch and we can work through the integration together.