Install the Package
composer require web-auth/webauthn-stimulus
Ensure your project uses Symfony 5.4+ and PHP 8.0+.
Enable the Bundle
Add to config/bundles.php:
return [
// ...
WebAuth\UX\WebauthnBundle::class => ['all' => true],
];
Configure the Bundle
Update config/packages/webauthn.yaml (auto-generated):
webauthn:
rp_name: "YourAppName"
rp_id: "yourdomain.com" # Must match your domain
signature_algorithm: -7 # ES256 (recommended)
attestation_conveyance_preference: "none"
user_verification: "preferred"
First Use Case: Registration
use WebAuth\UX\WebauthnBundle\Webauthn\Webauthn;
public function register(Webauthn $webauthn): Response
{
$challenge = $webauthn->createRegistrationChallenge();
return $this->render('registration.html.twig', [
'challenge' => $challenge->getChallenge(),
'options' => $challenge->getOptions(),
]);
}
webauthn_controller.js) to handle client-side Webauthn flow.Server-Side (Symfony)
createRegistrationChallenge()).$result = $webauthn->verifyRegistrationResponse(
$userId, // User identifier (e.g., UUID)
$username,
$responseData,
$challenge->getChallenge()
);
Client-Side (Stimulus)
webauthn_controller.js to trigger the Webauthn API:
this.webauthn.register({
challenge: this.data.get('challenge'),
options: this.data.get('options'),
success: (response) => {
this.dispatch('webauthn:registered', { detail: response });
},
});
webauthn:registered event in your app to submit the response to the server.Integration with Forms
WebauthnType:
$builder->add('webauthn', WebauthnType::class, [
'webauthn' => $webauthn,
'user_id' => $user->getId(),
'username' => $user->getEmail(),
]);
$challenge = $webauthn->createAuthenticationChallenge($userId);
this.webauthn.authenticate({
challenge: this.data.get('challenge'),
allowCredentials: this.data.get('allowCredentials'),
success: (response) => {
this.dispatch('webauthn:authenticated', { detail: response });
},
});
$result = $webauthn->verifyAuthenticationResponse(
$userId,
$responseData,
$challenge->getChallenge()
);
user_verification: "required" in config if you need explicit user presence (e.g., PIN entry).attestation_conveyance_preference: "direct" for self-service registrations or "none" for privacy.{% if webauthn_supported %}
{# Webauthn registration form #}
{% else %}
{# Fallback to password form #}
{% endif %}
webauthn:mock service in tests:
$this->container->get('webauthn')->setMockMode(true);
RP ID Mismatch
rp_id in config must match the domain where Webauthn is used (e.g., https://app.example.com).webauthn:rp-id command to validate:
php bin/console webauthn:rp-id
Challenge Timing
AbortController in Stimulus to cancel pending requests if the page unloads.Credential Storage
user_id and username. Changing either may break authentication.Browser Support
if (!window.PublicKeyCredential) {
// Show fallback UI
}
Debugging Failures
webauthn.yaml:
debug: true
Invalid challenge (timing issue).Credential already exists (duplicate registration).User verification required (config mismatch).Customize Authenticator Options Extend the default options in your controller:
$options = $webauthn->createRegistrationOptions();
$options->setAuthenticatorAttachment('platform'); // or 'cross-platform'
$options->setRequireResidentKey(true);
Handle Multiple Credentials
For users with multiple devices, store credentials in a credentials JSON column:
$credentials = json_decode($user->getCredentials(), true) ?: [];
$credentials[] = $result->getCredential();
$user->setCredentials(json_encode($credentials));
Security Headers Ensure your server sends:
Content-Security-Policy: frame-ancestors 'self'
To prevent clickjacking attacks on Webauthn prompts.
Extension Points
WebAuth\UX\WebauthnBundle\Webauthn\Verifier\VerifierInterface for custom logic.webauthn.registration.success or webauthn.authentication.success events.Performance
allowCredentials for authentication flows to reduce payload size.Local Development
Use ngrok to expose your local dev server with a valid domain:
ngrok http 8000
Update rp_id to your ngrok subdomain (e.g., your-subdomain.ngrok.io).
How can I help you explore Laravel packages today?