Weave 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

2Fa Webauthn Laravel Package

jbtronics/2fa-webauthn

Adds WebAuthn-based two-factor authentication to Laravel apps, letting users secure accounts with hardware security keys, passkeys, or built-in biometrics. Includes enrollment, verification, and integration with Laravel's auth scaffolding.

View on GitHub
Deep Wiki
Context7

Getting Started

First, the framing: this is a Symfony bundle, not a Laravel package. It provides a TwoFactorProviderInterface implementation for scheb/2fa-bundle and has no service provider, config file, Blade views, or Auth integration for Laravel. If you are a Laravel developer, "getting started" means one of three things: (a) you are working in a Symfony codebase, (b) you are porting the idea and will use web-auth/webauthn-lib directly in Laravel, or (c) you are running a Symfony service alongside your Laravel app. Everything below assumes (a) unless stated otherwise.

Minimal steps:

  1. composer require jbtronics/2fa-webauthn — this pulls scheb/2fa-bundle, web-auth/webauthn-lib, and Doctrine ORM. If you don't already have a working scheb/2fa setup with at least one other provider, stop and do that first; this package is an additional provider, not a 2FA solution on its own. Aim for v3.03 or later — earlier patch releases trigger warnings on current cose-lib versions (see Gotchas).
  2. Register TwoFactorWebauthnBundle in config/bundles.php.
  3. Import the bundle's routes (it ships a controller that handles the register/assert endpoints) — typically under a /2fa/webauthn prefix in config/routes.yaml.
  4. Create your key entity: extend the bundle's mapped superclass (a UserWebauthnKey-style class holding the credential ID, public key, counter, name, and timestamps) and add your ManyToOne association back to your User. Then generate a Doctrine migration.
  5. Implement the bundle's user interface on your User entity — the method that returns the user's registered keys — plus make sure your Doctrine user provider can load users by the identifier WebAuthn will send back.
  6. Register the provider in scheb_two_factor.two_factor_providers and enable the relevant firewall's 2FA config. The bundle exposes its provider under a service alias; copy the exact snippet from the README rather than guessing.
  7. Configure relying party ID / name and allowed origins to match your deployment host.

First use case: enroll a YubiKey or platform authenticator (Touch ID / Windows Hello) as a second factor for your admin firewall, alongside TOTP as a fallback. Wire a "Add security key" button on the user's security settings page pointing at the bundle's registration route, and let scheb/2fa render the assertion prompt at login.

Implementation Patterns

The scheb/2fa provider contract. The bundle's provider implements the three-part lifecycle: beginAuthentication() decides whether this provider applies to the user and returns a template/response, checkAuthentication() validates the submitted assertion, and prepareAuthentication() handles the redirect/response. In practice you rarely touch this class — you configure it — but knowing it exists is how you debug "why did login go to the TOTP form instead of the key prompt."

Registration flow. The shipped controller has two halves: a page that returns a PublicKeyCredentialCreationOptions payload, and an endpoint that receives the browser's navigator.credentials.create() result and persists it. The JS is a small wrapper around PublicKeyCredential — no build step required if you include the bundle's asset, but the payload is base64url-encoded and needs decoding before being passed to the browser API. If you use a frontend framework, replicate that decode in your own component instead of pulling in the bundle's vanilla JS.

Multiple keys per user, named. This is the single most valuable pattern: let users register a primary key plus a backup stored somewhere else, and give each a label (MacBook Touch ID, YubiKey 5C in desk drawer). The entity usually carries a name and creation/last-used timestamps — surface those in your account settings so users can audit and revoke.

Compose with recovery codes. Always enable scheb/2fa's backup-code provider in the same firewall. A user who loses their only key otherwise needs manual database surgery.

Step-up authentication. Because the provider is just a Symfony security voter-adjacent flow, you can also invoke it outside the login firewall for sensitive actions (changing email, exporting data) by redirecting through the 2FA flow with a target_path back to the action. This is where WebAuthn shines over TOTP.

Testing. Do not hand-craft assertion JSON. Two practical routes: unit-test your validator wiring with fixture credentials from webauthn-lib's test data, and drive end-to-end tests with Symfony Panther (or Playwright over CDP) using WebAuthn.addVirtualAuthenticator — Chrome DevTools Protocol gives you a fully scriptable, headless authenticator with configurable user-verification behaviour.

If you're porting to Laravel. Take the mental model, not the code: use web-auth/webauthn-lib directly (it is framework-agnostic), store credentials in an Eloquent model mirroring the bundle's columns, expose a POST /webauthn/register/options → POST /webauthn/register pair, and gate it behind password.confirm plus a custom middleware instead of scheb's firewall. Budget a day; the library does the cryptography, you write ~300 lines of glue.

Upgrading to v3.03. The only behaviour change is the removal of the non-standard Ed256/Ed512 COSE algorithm IDs. If you never deliberately enabled those (you almost certainly didn't — they were only reachable if you hand-built PublicKeyCredentialCreationOptions with a custom Algorithm list), the upgrade is a no-op. If you did, do a data audit before deploying: query your key table for any stored algorithm identifier that isn't a standard ES256/RS256/EdDSA value, because those credentials will stop verifying and users must re-register.

Gotchas and Tips

  • Secure context is mandatory. WebAuthn only runs over HTTPS or localhost. Plain HTTP on 192.168.x.x or a Docker hostname silently fails. Use mkcert for local dev with a real hostname if you need to test multi-device.
  • RP ID is forever. The relying party ID must be the registrable domain suffix of the origin, and changing it invalidates every credential already registered. Decide example.com vs app.example.com before you ship, and note that an RP ID of example.com allows credentials created on any subdomain.
  • Proxies break origin validation. Behind a load balancer, Symfony sees http://internal-host and the assertion's clientDataJSON.origin won't match. Configure trusted_proxies and framework.trusted_hosts, and verify X-Forwarded-Proto is honoured — this is the #1 "works on my machine" production failure with this class of package.
  • NotAllowedError is ambiguous. It means cancelled, timed out, or no matching credential. Don't log it as a security event or you'll drown in noise; show "try again" and move on.
  • Counter values lie. Some authenticators (notably many platform ones) always return 0, and some reset. Do not hard-fail on a non-increasing counter unless you want support tickets; log it as a signal instead.
  • getUserIdentifier() is baked into the credential. Changing how users are identified (email → UUID, or letting users change email) can orphan existing credentials depending on whether you rely on the user handle or the allow-list. Prefer an immutable ID in the user handle.
  • Passkeys and synced credentials. If you set authenticatorAttachment: cross-platform you exclude Touch ID/Windows Hello; if you require userVerification: required you exclude some older keys. For a second factor, preferred plus a broad attachment policy is usually the right default.
  • Cross-origin iframes are blocked without a Permissions-Policy: publickey-credentials-get header. Relevant if your login form is embedded.
  • Ed256/Ed512 are gone as of v3.03. Those non-standard COSE algorithm IDs never should have been enabled, and recent cose-lib releases warn about them; the bundle now refuses them outright. Staying on v3.0.x to keep them is the worse trade — you inherit the warning and a future collision, since their IDs overlap with the official EdDSA registry. Upgrade, and re-register the (hopefully zero) affected keys.
  • Version constraints matter. web-auth/webauthn-lib has had breaking major releases (v3 → v4 → v5 changed namespaces and option builders), and this bundle pins a range. The Ed256/Ed512 removal is downstream of a cose-lib bump, so the two are coupled: run composer why web-auth/webauthn-lib and composer why cose/cose-lib before adding anything else that touches them, and don't hand-pin cose-lib back to an older minor to silence the warning.
  • Low bus factor. 15 stars and effectively one maintainer. The surface area is small and MIT-licensed, so vendoring or forking is cheap — but don't assume the registration UI will keep pace with browser changes. Read the provider class once; it's short.
  • Debugging aid: log the raw clientDataJSON and authenticatorData (base64) on failure. Nearly every error reduces to "origin mismatch," "RP ID hash mismatch," or "challenge mismatch," and those three are distinguishable in ten seconds from those two blobs. After the v3.03 upgrade you can add a fourth: an assertion rejected with an unexpected-algorithm error means a pre-existing Ed256/Ed512 credential, not a bug in your config.
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.
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin