Stars: 165
Forks: 27
Pull Requests: 30
Issues: 24
Watchers: 1
Last Updated: 2023-09-16 04:20:28
Authenticate users with Passkeys: fingerprints, patterns and biometric data.
License: MIT License
Languages: PHP, JavaScript
Authenticate users with Passkeys: fingerprints, patterns and biometric data.
// App\Http\Controllers\LoginController.php
use Laragear\WebAuthn\Http\Requests\AssertedRequest;
public function login(AssertedRequest $request)
{
$user = $request->login();
return response()->json(['message' => "Welcome back, $user->name!"]);
}
You want to add two-factor authentication to your app? Check out Laragear TwoFactor.
Your support allows me to keep this package free, up-to-date and maintainable. Alternatively, you can spread the word!
ext-openssl
.Require this package into your project using Composer:
composer require laragear/webauthn
Passkeys, hence WebAuthn, consists in two ceremonies: attestation, and assertion.
Attestation is the process of asking the authenticator (a phone, laptop, USB key...) to create a private-public key pair, save the private key internally, and store the public key inside the server. For that to work, the browser must support WebAuthn, which is what intermediates between the authenticator (OS & device hardware) and the server.
Assertion is the process of pushing a cryptographic challenge to the authenticator, which will return back to the server signed by the private key of the device. Upon arrival, the server checks the signature is correct with the stored public key, ready to log in.
The private key doesn't leave the authenticator, there are no shared passwords stored anywhere, and Passkeys only work on the server domain (like google.com) or subdomain (like auth.google.com).
We need to make sure your users can register their devices and authenticate with them.
eloquent-webauthn
driverwebauthn_credentials
tableAfter that, you can quickly start WebAuthn with the included controllers and helpers to make your life easier.
Info
While you can use Passkeys without users by invoking the ceremonies manually, Laragear WebAuthn is intended to be used with already existing Users.
eloquent-webauthn
driverLaragear WebAuthn works by extending the Eloquent User Provider with an additional check to find a user for the given WebAuthn Credentials (Assertion). This makes this WebAuthn package compatible with any guard you may have.
Simply go into your auth.php
configuration file, change the driver from eloquent
to eloquent-webauthn
, and add the password_fallback
to true
.
return [
// ...
'providers' => [
'users' => [
'driver' => 'eloquent-webauthn',
'model' => App\User::class,
'password_fallback' => true,
],
]
];
The password_fallback
indicates the User Provider should fall back to validate the password when the request is not a WebAuthn Assertion. It's enabled to seamlessly use both classic (password) and WebAuthn authentication procedures.
webauthn_credentials
tableCreate the webauthn_credentials
table by publishing the migration file and migrating the table:
php artisan vendor:publish --provider="Laragear\WebAuthn\WebAuthnServiceProvider" --tag="migrations"
php artisan migrate
You may edit the migration to your liking, like adding new columns, but not to remove them or change their name.
Add the WebAuthnAuthenticatable
contract and the WebAuthnAuthentication
trait to the User class, or any other that uses authentication.
<?php
namespace App;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Laragear\WebAuthn\Contracts\WebAuthnAuthenticatable;
use Laragear\WebAuthn\WebAuthnAuthentication;
class User extends Authenticatable implements WebAuthnAuthenticatable
{
use WebAuthnAuthentication;
// ...
}
From here you're ready to work with WebAuthn Authentication. The following steps will help you close the gap to a full implementation.
WebAuthn uses exclusive routes to register and authenticate users. Creating these routes and controller may be cumbersome, specially if it's your first time in the WebAuthn realm.
Instead, go for a quick start and publish the controllers included in Laragear WebAuthn. These controllers will be located at app\Http\Controllers\WebAuthn
.
php artisan vendor:publish --provider="Laragear\WebAuthn\WebAuthnServiceProvider" --tag="controllers"
Next, to pick these controllers easily, go into your web.php
routes file and register a default set of routes with the WebAuthn::routes()
method.
// web.php
use Illuminate\Support\Facades\Route;
use Laragear\WebAuthn\WebAuthn;
Route::view('welcome');
// WebAuthn Routes
WebAuthn::routes();
This package includes a simple but convenient script to handle WebAuthn Attestation and Assertion. To use it, just publish the webauthn.js
asset into your application public resources.
php artisan vendor:publish --provider="Laragear\WebAuthn\WebAuthnServiceProvider" --tag="js"
You will receive the resources/js/vendor/webauthn/webauthn.js
file which you can include into your authentication views and use it programmatically
<!doctype html>
<head>
{{-- ... --}}
<script src="{{ Vite::asset('resources/js/vendor/webauthn/webauthn.js') }}"></script>
@vite(['resources/js/app.js'])
</head>
octicon-info mr-2" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true">
You can also edit the script file to transform it into a module so it can be bundled in your Vite frontend, exporting the class as export default class WebAuthn { ... }
, and add it to the @vite
assets.
@vite(['resources/js/vendor/webauthn/webauthn.js', 'resources/js/app.js'])
Once done, you can easily start registering an user device, and login in users that registered a device previusly.
For example, let's imagine an user logs in normally, and enters its profile view. You may show a WebAuthn registration HTML with the following code:
<form id="register-form">
<button type="submit" value="Register authenticator">
</form>
<!-- Registering authenticator -->
<script>
const register = event => {
event.preventDefault()
new WebAuthn().register()
.then(response => alert('Registration successful!'))
.catch(error => alert('Something went wrong, try again!'))
}
document.getElementById('register-form').addEventListener('submit', register)
</script>
In our Login view, we can use the WebAuthn credentials to log in the user.
<form id="login-form">
<input id="email" type="email" value="[email protected]">
<button type="submit" value="Log in with your device">
</form>
<!-- Login users -->
<script>
const login = event => {
event.preventDefault()
new WebAuthn().login({
email: document.getElementById('email').value,
}, {
remember: document.getElementById('remember').checked ? 'on' : null,
}).then(response => alert('Authentication successful!'))
.catch(error => alert('Something went wrong, try again!'))
}
document.getElementById('login-form').addEventListener('submit', login)
</script>
You can copy-paste this helper into your authentication routes, or import it into a bundler like Laravel Vite, Webpack, parcel, or many more, as long you adjust the script to the bundler needs. If the script doesn't suit your needs, you're free to modify it or create your own.
Both register()
and login()
accept different parameters for the initial request to the server, and the subsequent response to the server. For example, you can use this to remember the user being authenticated.
new WebAuthn().login({
// Initial request to the server
email: document.getElementById('email').value,
}, {
// Response from the authenticator to the server
remember: document.getElementById('remember').checked ? 'on' : null,
})
By default, the helper assumes you're using the default WebAuthn routes. If you're using different routes for WebAuthn, you can set them at runtime.
const webAuthn = new WebAuthn({
registerOptions: 'webauthn/register/options',
register: 'webauthn/register',
loginOptions: 'webauthn/login/options',
login: 'webauthn/login',
});
Here is good place to use ziggy if it's in your project.
You may add headers to all WebAuthn authentication requests using the second parameter of the WebAuthn
constructor. These headers will be present on all requests made by the instance.
const webAuthn = new WebAuthn({}, {
'X-Colors': 'red',
});
You may use a different
WebAuthn
instances with different headers for both Attestation and Assertion.
Attestation is the ceremony to create WebAuthn Credentials. To create an Attestable Response that the user device can understand, use the AttestationRequest::toCreate()
form request.
For example, we can create our own AttestationController
to create it.
// app\Http\Controllers\WebAuthn\AttestationController.php
use Laragear\WebAuthn\Http\Requests\AttestationRequest;
public function createChallenge(AttestationRequest $request)
{
return $request->toCreate();
}
The device will receive the "instructions" to make a key, and will respond with it. You can use the AttestedRequest
form request and its save()
method to persist the WebAuthn key if it is valid. The request will automatically return a Validation exception if something fails.
// app\Http\Controllers\WebAuthn\AttestationController.php
use Laragear\WebAuthn\Http\Requests\AttestedRequest;
public function register(AttestedRequest $attestation)
{
$attestation->save();
return 'Now you can login without passwords!';
}
You may pass an array, or a callback, to the save()
, which will allow you to modify the underlying WebAuthn Eloquent Model before saving it. For example, we could add an alias for the key present in the Request data.
// app\Http\Controllers\WebAuthn\AttestationController.php
use Laragear\WebAuthn\Http\Requests\AttestedRequest;
public function register(AttestedRequest $request)
{
$request->validate(['alias' => 'nullable|string']);
$attestation->save($request->only('alias'));
// Same as:
// $attestation->save(function ($credentials) use ($request) {
// $credentials->alias = $request->input('alias');
// })
}
Both
AttestationRequest
andAttestedRequest
validates the authenticated user. If the user is not authenticated, an HTTP 403 status code will be returned.
By default, the authenticator decides how to verify user when creating a credential. Some may ask to press a "Continue" button to confirm presence, others will verify the User with biometrics, patterns or passwords.
You can override this using fastRegistration()
to only check for user presence if possible, or secureRegistration()
to actively verify the User.
// app\Http\Controllers\WebAuthn\AttestationController.php
use Laragear\WebAuthn\Http\Requests\AttestationRequest;
public function createChallenge(AttestationRequest $request)
{
return $request->fastRegistration()->toCreate();
}
Userless/One-touch/Typeless login This enables one click/tap login, without the need to specify the user credentials (like the email) beforehand.
For this to work, the device has to save the "username id" inside itself. Some authenticators may save it regardless, others may be not compatible. To make this mandatory when creating the WebAuthn Credential, use the userless()
method of the AttestationRequest
form request.
// app\Http\Controllers\WebAuthn\AttestationController.php
use Laragear\WebAuthn\Http\Requests\AttestationRequest;
public function registerDevice(AttestationRequest $request)
{
return $request->userless()->toCreate();
}
The Authenticator WILL require user verification on login when using
userless()
. Its highly probable the user will also be asked for user verification on login, as it will depend on the authenticator itself.
By default, during Attestation, the device will be informed about the existing enabled credentials already registered in the application. This way the device can avoid creating another one for the same purpose.
You can enable multiple credentials per device using allowDuplicates()
, which in turn will always return an empty list of credentials to exclude. This way the authenticator will think there are no already stored credentials for your app.
// app\Http\Controllers\WebAuthn\AttestationController.php
use Laragear\WebAuthn\Http\Requests\AttestationRequest;
public function registerDevice(AttestationRequest $request)
{
return $request->allowDuplicates()->make();
}
The Assertion procedure also follows a two-step procedure: the user will input its username, the server will return the IDs of the WebAuthn credentials to use, and the device pick one to sign the response. If you're using userless login, only the challenge is returned.
First, use the AssertionRequest::toVerify()
form request. It will automatically create an assertion for the user that matches the credentials, or a blank one in case you're using userless login. Otherwise, you may set stricter validation rules to always ask for credentials.
For example, we can use our own AssertionController
to handle it.
// app\Http\Controllers\WebAuthn\AssertionController.php
use Laragear\WebAuthn\Http\Requests\AssertionRequest;
public function createChallenge(AssertionRequest $request)
{
$request->validate(['email' => 'sometimes|email']);
return $request->toVerify($request->only('email'));
}
After that, you may receive the challenge using the AssertedRequest
request object by just type-hinting it in the controller.
Since the authentication is pretty much straightforward, you only need to check if the login()
method returns the newly authenticated user or null
when it fails. When it's a success, it will take care of regenerating the session for you.
// app\Http\Controllers\WebAuthn\AssertionController.php
use Laragear\WebAuthn\Http\Requests\AssertedRequest;
public function createChallenge(AssertedRequest $request)
{
$user = $request->login();
return $user
? response("Welcome back, $user->name!");
: response('Something went wrong, try again!');
}
If you need greater control on the Assertion procedure, you may want to Assert manually.
If you have debugging enabled the assertion error during authentication will be logged in your application logs, which by default is
storage/logs/laravel.log
.
In the same style of attestation user verification, the authenticator decides if it should verify the user on login or not.
You may only require the user presence with fastLogin()
, or actively verify the user with secureLogin()
.
// app\Http\Controllers\WebAuthn\AssertionController.php
use Laragear\WebAuthn\Http\Requests\AssertionRequest;
public function createChallenge(AssertionRequest $request)
{
$request->validate(['email' => 'sometimes|email']);
return $request->fastLogin()->toVerify($request->only('email'));
}
By default, the eloquent-webauthn
can be used to log in users with passwords when the credentials are not a WebAuthn JSON payload. This way, your normal Authentication flow is unaffected:
// app\Http\Controllers\Auth\LoginController.php
use Illuminate\Support\Facades\Auth;
public function login(Request $request)
{
$request->validate(['email' => 'required|email', 'password' => 'required|string']);
if (Auth::attempt($request->only('email', 'password'))) {
return redirect()->home();
}
return back()->withErrors(['email' => 'No user found with these credentials']);
}
You may disable the fallback to only allow WebAuthn authentication by setting password_fallback
to false
. This may force you to handle classic user/password using a separate guard.
During assertion, the package will automatically detect if a Credential has been cloned by comparing how many times the user has logged in with it.
If it's detected as cloned, the Credential is disabled, a CredentialCloned
event is fired, and the Assertion gets denied.
You can use the event to warn the user:
use Illuminate\Support\Facades\Event;
use Laragear\WebAuthn\Events\CredentialCloned;
use App\Notifications\SecureYourDevice;
Event::listen(CredentialCloned::class, function ($cloned) {
$notification = new SecureYourDevice($cloned->credential);
$cloned->credential->user->notify($notification);
});
The purpose of the WebAuthnAuthenticatable
contract is to allow managing credentials within the User instance. The most useful methods are:
webAuthnData()
: Returns the non-variable WebAuthn user data to create credentials.flushCredentials()
: Removes all credentials. You can exclude credentials by their id.disableAllCredentials()
: Disables all credentials. You can exclude credentials by their id.makeWebAuthnCredential()
: Creates a new WebAuthn Credential instance.webAuthnCredentials()
: One-to-Many relation to query for WebAuthn Credentials.You can use these methods to, for example, find a credential to blacklist, or disable WebAuthn completely by flushing all registered devices.
The following events are fired by this package, which you can hook into in your application:
Event | Description |
---|---|
CredentialCreated |
An User has registered a new WebAuthn Credential through Attestation. |
CredentialEnabled |
A disabled WebAuthn Credential was enabled using enable() . |
CredentialDisabled |
A enabled WebAuthn Credential was disabled using disable() . |
CredentialCloned |
A WebAuthn Credential was detected as cloned dring Assertion. |
If you want to manually Attest and Assert users, you may instance their respective pipelines used for both WebAuthn Ceremonies:
Pipeline | Description |
---|---|
AttestationCreator |
Creates a request to create a WebAuthn Credential. |
AttestationValidator |
Validates a response with the WebAuthn Credential and stores it. |
AssertionCreator |
Creates a request to validate a WebAuthn Credential. |
AssertionValidator |
Validates a response for a WebAuthn Credential. |
All of these pipelines require the current Request to work, as is used to generate Challenges in the Session and validate different parts of the authentication data.
For example, you may manually authenticate a user with its WebAuthn Credentials AssertionValidator
pipeline. We can just type-hint a pipeline in a Controller action argument and Laravel will automatically inject the instance to it.
use Laragear\WebAuthn\Assertion\Validator\AssertionValidation;
use Laragear\WebAuthn\Assertion\Validator\AssertionValidator;
use Illuminate\Support\Facades\Auth;
public function authenticate(Request $request, AssertionValidator $assertion)
{
$credential = $assertion
->send(new AssertionValidation($request))
->thenReturn()
->credential;
Auth::login($credential->user);
return "Welcome aboard, {$credential->user->name}!";
}
Since these are Laravel Pipelines, you're free to push additional pipes. These pipes can be a class with handle()
, or just a function that receives the validation procedure.
use Laragear\WebAuthn\Assertion\Validator\AssertionValidator;
use Exception;
public function authenticate(Request $request, AssertionValidator $assertion)
{
$credential = $assertion
->send(new AssertionValidation($request))
// Add new pipes to the validation.
->pipe(function($validation, $next) {
if ($validation->user?->isNotAwesome()) {
throw new Exception('The user is not awesome');
}
return $next($validation);
})
->thenReturn()
->credential;
Auth::login($credential->user);
return "Welcome aboard, {$credential->user->name}!";
}
The pipes list and the pipes themselves are not covered by API changes, and are marked as
internal
. These may change between versions without notice.
Laragear WebAuthn was made to work out-of-the-box, but you can override the configuration by simply publishing the config file.
php artisan vendor:publish --provider="Laragear\WebAuthn\WebAuthnServiceProvider" --tag="config"
After that, you will receive the config/webauthn.php
config file with an array like this:
<?php
return [
'relying_party' => [
'name' => env('WEBAUTHN_NAME', env('APP_NAME')),
'id' => env('WEBAUTHN_ID'),
],
'challenge' => [
'bytes' => 16,
'timeout' => 60,
'key' => '_webauthn',
]
];
return [
'relying_party' => [
'name' => env('WEBAUTHN_NAME', env('APP_NAME')),
'id' => env('WEBAUTHN_ID'),
],
];
The Relying Party is just a way to uniquely identify your application in the user device:
name
: The name of the application. Defaults to the application name.id
: An unique ID the application, like the site URL. If null
, the device may fill it internally, usually as the full domain.WebAuthn authentication only work on the top domain it was registered.
Instead of modifying the config file, you should use the environment variables to set the name and ID for WebAuthn.
WEBAUTHN_NAME=SecureBank
WEBAUTHN_ID=https://auth.securebank.com
return [
'challenge' => [
'bytes' => 16,
'timeout' => 60,
'key' => '_webauthn',
]
];
The outgoing challenges are random string of bytes. This controls how many bytes, the seconds which the challenge is valid, and the session key used to store the challenge while its being resolved by the device.
In theory this package should work without any problems with these packages, but you may need to override or redirect the authentication flow (read: override methods) to one using WebAuthn.
There is no support for using WebAuthn with these packages because these are meant to be used with classic user-password authentication. Any issue regarding these packages will be shot down with extreme prejudice.
If you think WebAuthn is critical for these packages, consider supporting this package.
Yes. In the case of old browsers, you should have a fallback detection script. This can be asked with the included JavaScript helper in a breeze:
if (WebAuthn.doesntSupportWebAuthn()) {
alert('Your device is not secure enough to use this site!');
}
No. WebAuthn only stores a cryptographic public key generated randomly by the device.
No. WebAuthn kills the phishing because, unlike passwords, the private key never leaves the device, and the key-pair is bound to the top-most domain it was registered.
An user bing phished at staetbank.com
won't be able to login with a key made on the legit site statebank.com
, as the device won't be able to find it.
No, unless explicitly requested and consented. This package doesn't support other attestation conveyances than none
, so it's never transmitted.
Yes, as long you are hashing them as you should. This is done by Laravel by default. You can also disable them.
Yes.
Not by default, but you can enable it.
Yes. If you're not using a password fallback, you may need to create a logic to register a new device using an email or SMS. It's assumed he is reading his email using a trusted device.
Disabling a credential doesn't delete it, so it's useful as a blacklisting mechanism and these can also be re-enabled. When the credential is deleted, it goes away forever from the server, so the credential in the authenticator device becomes orphaned.
Yes. If it does, the other part of the credentials in your server gets orphaned. You may want to show the user a list of registered credentials in the application to delete them.
Extremely secure since it works only on HTTPS (or localhost
). Also, no password or codes are exchanged nor visible in the screen.
Yes. Just be sure to create recovery helpers to avoid locking out your users.
Yes, but it's very basic.
If you need more complex WebAuthn management, consider using the navigator.credentials
API directly.
Yes and no. To register users, you still need to use captcha, honeypots, or other mechanisms to stop bots.
Once a user is registered, bots won't be able to login because the real user is the only one that has the private key required for WebAuthn.
Yes, the included WebAuthn Helper does it automatically for you.
Yes, public keys are encrypted when saved into the database.
No. You're free to create your own flow for recovery.
My recommendation is to send an email to the user, pointing to a route that registers a new device, and immediately redirect him to blacklist which credential was lost (or blacklist the only one he has).
It depends.
This is entirely up to hardware, OS and browser vendor themselves, but mostly the OS. Some OS or browsers may offer a way to sync private keys on the cloud, even letting the assertion challenge to be signed remotely instead of transmitting the private key. Please check your target platforms of choice.
By default, this WebAuthn implementation accepts almost everything. Some combinations of devices, OS and Web browsers may differ on what to make available for WebAuthn authentication.
You may check this site for authenticator support.
This package supports WebAuthn 2.0, which is W3C Recommendation. Your device/OS/browser may be using an unsupported version.
There are no plans to support older WebAuthn specs. The new WebAuthn 3.0 draft spec needs to be finished to be supported.
Use localhost
exclusively (not 127.0.0.1
or ::1
) or use a proxy to tunnel your site through HTTPS. WebAuthn only works on localhost
or under HTTPS
only.
none
attestation conveyance?Because direct
, indirect
and enterprise
attestations are mostly used on high-security high-risk scenarios, where an entity has total control on the devices used to authenticate. Imagine banks, medical, or military.
If you deem this feature critical for you, consider supporting this package.
No. The user can use whatever to authenticate in your app. This may be enabled on future versions.
Remember that your WebAuthn routes must use Sessions, because the Challenges are stored there.
More information can be retrieved in your application logs.
There should be no problems using this package with Laravel Octane.
These are some details about this WebAuthn implementation:
If you discover any security related issues, please email [email protected] instead of using the issue tracker.
The MIT License (MIT). Please see License File for more information.
Contains Code from Lukas Buchs WebAuthn 2.0 implementation. The MIT License (MIT) where applicable.
Laravel is a Trademark of Taylor Otwell. Copyright © 2011-2022 Laravel LLC.