Skip to content

Commit

Permalink
Update passkey login action
Browse files Browse the repository at this point in the history
  • Loading branch information
rawilk committed Oct 14, 2024
1 parent 58abfd5 commit aaf2402
Show file tree
Hide file tree
Showing 2 changed files with 89 additions and 56 deletions.
55 changes: 27 additions & 28 deletions resources/views/filament/actions/passkey-login.blade.php
Original file line number Diff line number Diff line change
@@ -1,37 +1,36 @@
<x-profile-filament::webauthn-script
x-data="webauthnForm({
mode: 'login',
wireId: '{{ $this->getId() }}',
{{-- adding time variable to url so a unique signature is always generated --}}
loginPublicKeyUrl: '{{ URL::signedRoute('profile-filament::webauthn.passkey_assertion_pk', ['t' => now()->unix()]) }}',
loginUsing: function (assertion) {
const component = window.Livewire.find('{{ $this->getId() }}');
return component.mountAction('{{ $getName() }}', { assertion });
},
mode="authenticate"
x-data="authenticateWebauthn({
publicKeyUrl: {{ Js::from($passkeyOptionsUrl()) }},
loginUsing: function (answer) {
$wire.mountAction({{ Js::from($getName()) }}, {
assertion: answer,
});
}
})"
x-on:click.prevent.stop="submit"
wire:ignore.self
>
<x-profile-filament::webauthn-waiting-indicator
:message="__('profile-filament::pages/mfa.webauthn.waiting')"
class="text-sm"
x-show="processing"
style="display: none;"
x-cloak
x-show="processing"
wire:ignore
:message="__('profile-filament::pages/mfa.webauthn.waiting')"
/>

<x-filament-actions::action
:action="$action"
:badge="$getBadge()"
:badge-color="$getBadgeColor()"
dynamic-component="filament::button"
:icon-position="$getIconPosition()"
:labeled-from="$getLabeledFromBreakpoint()"
:outlined="$isOutlined()"
:size="$getSize()"
class="fi-ac-btn-action"
x-show="! processing"
>
{{ $getLabel() }}
</x-filament-actions::action>
<div x-show="! processing">
<x-filament-actions::action
:action="$action"
:badge="$getBadge()"
:badge-color="$getBadgeColor()"
dynamic-component="filament::button"
:icon-position="$getIconPosition()"
:labeled-from="$getLabeledFromBreakpoint()"
:outlined="$isOutlined()"
:size="$getSize()"
class="fi-ac-btn-action"
>
{{ $getLabel() }}
</x-filament-actions::action>
</div>
</x-profile-filament::webauthn-script>
90 changes: 62 additions & 28 deletions src/Filament/Actions/PasskeyLoginAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,16 @@

namespace Rawilk\ProfileFilament\Filament\Actions;

use Closure;
use DanHarrin\LivewireRateLimiting\Exceptions\TooManyRequestsException;
use DanHarrin\LivewireRateLimiting\WithRateLimiting;
use Filament\Actions\Action;
use Filament\Http\Responses\Auth\Contracts\LoginResponse;
use Filament\Notifications\Notification;
use Illuminate\Http\Request;
use Illuminate\Pipeline\Pipeline;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\URL;
use Illuminate\Support\Timebox;
use Rawilk\ProfileFilament\Actions\Auth\PrepareUserSession;
use Rawilk\ProfileFilament\Dto\Auth\TwoFactorLoginEventBag;
Expand All @@ -30,48 +31,48 @@ class PasskeyLoginAction extends Action

protected ?array $pipes = null;

protected ?Closure $authenticateUsing = null;

protected function setUp(): void
{
parent::setUp();

// Set some basic defaults...
$this->name('passkeyLogin');
$this->livewireClickHandlerEnabled(false);

$this->alpineClickHandler('login');

$this->defaultView('profile-filament::filament.actions.passkey-login');

$this->color('gray');

$this->label(__('profile-filament::pages/mfa.webauthn.passkey_login_button'));

$this->failureNotification(
fn (): Notification => Notification::make()
->danger()
->title(__('profile-filament::pages/mfa.webauthn.assert.failure_title'))
->body($this->error ?? __('profile-filament::pages/mfa.webauthn.assert.failure'))
->persistent()
);

$this->action(function (array $arguments, Request $request) {
try {
$this->rateLimit(5);
} catch (TooManyRequestsException $exception) {
Notification::make()
->title(__('filament-panels::pages/auth/login.notifications.throttled.title', [
'seconds' => $exception->secondsUntilAvailable,
'minutes' => ceil($exception->secondsUntilAvailable / 60),
]))
->body(array_key_exists('body', __('filament-panels::pages/auth/login.notifications.throttled') ?: []) ? __('filament-panels::pages/auth/login.notifications.throttled.body', [
'seconds' => $exception->secondsUntilAvailable,
'minutes' => ceil($exception->secondsUntilAvailable / 60),
]) : null)
->danger()
->send();

return;
$this->getRateLimitedNotification($exception)?->send();

$this->cancel();
}

if (! Arr::has($arguments, 'assertion')) {
return;
if (! $assertion = data_get($arguments, 'assertion')) {
$this->cancel();
}

$response = App::make(Timebox::class)->call(callback: function (Timebox $timebox) use ($arguments) {
$response = App::make(Timebox::class)->call(callback: function (Timebox $timebox) use ($assertion) {
try {
$response = Webauthn::verifyAssertion(
user: null,
assertionResponse: $arguments['assertion'],
assertionResponse: $assertion,
storedPublicKey: session()->pull(MfaSession::PasskeyAssertionPk->value),
requiresPasskey: true,
);
Expand All @@ -89,13 +90,17 @@ protected function setUp(): void
}, microseconds: 300 * 1000);

if (! $response) {
Notification::make()
->danger()
->title(__('profile-filament::pages/mfa.webauthn.assert.failure_title'))
->body($this->error ?? __('profile-filament::pages/mfa.webauthn.assert.failure'))
->send();
$this->failure();

return;
$this->cancel();
}

if (is_callable($this->authenticateUsing)) {
return $this->evaluate($this->authenticateUsing, [
'passkey' => $response['authenticator'],
'publicKeyCredentialSource' => $response['publicKeyCredentialSource'],
'request' => $request,
]);
}

/** @var \Rawilk\ProfileFilament\Models\WebauthnKey $authenticator */
Expand All @@ -107,7 +112,7 @@ protected function setUp(): void
data: [],
request: $request,
mfaChallengeMode: MfaChallengeMode::Webauthn,
assertionResponse: $arguments['assertion'],
assertionResponse: $assertion,
);

return app(Pipeline::class)
Expand All @@ -117,7 +122,14 @@ protected function setUp(): void
});
}

public function pipeThrough(array $pipes): self
public function authenticateUsing(?Closure $callback = null): static
{
$this->authenticateUsing = $callback;

return $this;
}

public function pipeThrough(array $pipes): static
{
$this->pipes = $pipes;

Expand All @@ -129,6 +141,14 @@ public function getLivewireTarget(): ?string
return 'mountAction';
}

public function passkeyOptionsUrl(): string
{
return URL::temporarySignedRoute(
name: 'profile-filament::webauthn.passkey_assertion_pk',
expiration: now()->addHour(),
);
}

protected function getAuthenticationPipes(): array
{
if (is_array($this->pipes)) {
Expand All @@ -137,4 +157,18 @@ protected function getAuthenticationPipes(): array

return [PrepareUserSession::class];
}

protected function getRateLimitedNotification(TooManyRequestsException $exception): Notification
{
return Notification::make()
->title(__('filament-panels::pages/auth/login.notifications.throttled.title', [
'seconds' => $exception->secondsUntilAvailable,
'minutes' => ceil($exception->secondsUntilAvailable / 60),
]))
->body(array_key_exists('body', __('filament-panels::pages/auth/login.notifications.throttled') ?: []) ? __('filament-panels::pages/auth/login.notifications.throttled.body', [
'seconds' => $exception->secondsUntilAvailable,
'minutes' => ceil($exception->secondsUntilAvailable / 60),
]) : null)
->danger();
}
}

0 comments on commit aaf2402

Please sign in to comment.