Skip to content

Instantly share code, notes, and snippets.

@rawilk
Last active September 29, 2022 15:03
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rawilk/d9384836ea03413b3f8c572cfdf9844d to your computer and use it in GitHub Desktop.
Save rawilk/d9384836ea03413b3f8c572cfdf9844d to your computer and use it in GitHub Desktop.
WebAuthn Key Authentication - Laravel WebAuthn Package
<div>
{{-- change "head" to a stack name you chose in your layout file. --}}
@push('head')
@webauthnScripts
@endpush
<div x-data="{
publicKey: @entangle('publicKey').defer,
keyData: @entangle('keyData').defer,
webAuthnSupported: true,
errorMessages: {{ Js::from($this->errorMessages) }},
errorMessage: null,
notifyCallback() {
return (errorName, defaultMessage) => {
this.errorMessage = this.errorMessages[errorName] || defaultMessage;
};
},
webAuthn: new WebAuthn,
init() {
this.webAuthnSupported = this.webAuthn.supported();
if (! this.webAuthnSupported) {
this.errorMessage = this.errorMessages[this.webAuthn.notSupportedType()];
}
this.webAuthn.registerNotifyCallback(this.notifyCallback());
this.authenticate();
},
authenticate() {
if (! this.webAuthnSupported) {
return;
}
this.keyData = null;
this.errorMessage = null;
// This is the most important part.
this.webAuthn.sign(JSON.parse(this.publicKey), data => {
this.keyData = data;
@this.login();
});
},
}">
<div x-show="! errorMessage">
<p>Interact with your authenticator...</p>
</div>
<div x-show="errorMessage">
<p class="text-base text-red-600" x-html="errorMessage"></p>
<div class="mt-10" x-show="webAuthnSupported">
<button type="button"
class="..."
x-on:click="errorMessage = null; authenticate();"
>
Retry
</button>
</div>
</div>
</div>
</div>
<?php
namespace App\Http\Livewire;
use App\Http\Requests\TwoFactorLoginRequest;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Lang;
use Livewire\Component;
use Rawilk\Webauthn\Actions\PrepareAssertionData;
use Rawilk\Webauthn\Facades\Webauthn;
class TwoFactorChallenge extends Component
{
/*
* Public WebAuthn assertion key for when
* a user has at least one key registered.
*/
public string $publicKey = '';
public $keyData;
public function getErrorMessagesProperty(): array
{
return [
...Lang::get('webauthn::alerts.auth') ?? [],
'InvalidStateError' => __('webauthn::alerts.login_not_allowed_error'),
'notSupported' => __('webauthn::alerts.browser_not_supported'),
'notSecured' => __('webauthn::alerts.browser_not_secure'),
];
}
// See code below for request example
public function login(TwoFactorLoginRequest $request)
{
$this->resetErrorBag();
$user = $request->challengedUser();
// WebAuthn package will update the last_used_at timestamp of the key.
$valid = Webauthn::validateAssertion($user, Arr::only((array) $this->keyData, [
'id',
'rawId',
'response',
'type',
]));
if (! $valid) {
// Notify user of failed authentication attempt.
return;
}
auth()->login($user);
// Handle successful login
}
public function mount(TwoFactorLoginRequest $request): void
{
if (! $request->hasChallengedUser()) {
redirect('/login');
}
$this->publicKey = json_encode(app(PrepareAssertionData::class)($request->challengedUser()));
}
public function render()
{
return view('livewire.two-factor-challenge');
}
}
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class TwoFactorLoginRequest extends FormRequest
{
/** The user attempting the two factor challenge */
protected $challengedUser;
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [];
}
/**
* Determine if there is a challenged user in the current session.
*/
public function hasChallengedUser(): bool
{
$model = app(config('auth.providers.users.model'));
return $this->session()->has('login.id')
&& $model::find($this->session()->get('login.id'));
}
/**
* Get the user that is attempting the two factor challenge.
*/
public function challengedUser()
{
if ($this->challengedUser) {
return $this->challengedUser;
}
$model = app(config('auth.providers.users.model'));
if (
! $this->session()->has('login.id')
|| ! $user = $model::find($this->session()->get('login.id'))
) {
throw new \Exception('No user found.');
}
return $this->challengedUser = $user;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment