available() && Auth::user()->hasTwoFactorEnabled(), 404); // Validate the label BEFORE the browser ceremony, so an invalid name never // leaves an orphaned credential on the authenticator. $this->validate(); return $webauthn->registrationOptions(Auth::user()); } public function register(array $attestation, WebauthnService $webauthn): void { // WebAuthn must be available; no existing-factor guard here — registering a key // may itself be the FIRST factor enrollment (the guard in options() already // vetted the ceremony start; only require availability at submission time). abort_unless($webauthn->available(), 404); $this->validate(); $cred = $webauthn->verifyRegistration(Auth::user(), $attestation, trim($this->newName)); $this->reset('newName'); // First factor enrolled and no codes yet → generate + show the recovery modal. if (! Auth::user()->hasRecoveryCodes()) { Auth::user()->replaceRecoveryCodes(); $this->dispatch('openModal', component: 'modals.recovery-codes'); } AuditEvent::create([ 'user_id' => Auth::id(), 'actor' => Auth::user()->name ?? 'system', 'action' => 'webauthn.register', 'target' => $cred->name, 'ip' => request()->ip(), ]); $this->dispatch('notify', message: __('auth.webauthn_added')); } /** Destructive (R5): open the confirm modal, which re-dispatches `webauthnKeyRemoved`. */ public function confirmRemove(int $id): void { $cred = Auth::user()->webauthnCredentials()->whereKey($id)->first(); if (! $cred) { return; } $this->dispatch('openModal', component: 'modals.confirm-action', arguments: [ 'heading' => __('auth.webauthn_title'), 'body' => __('auth.webauthn_remove_confirm'), 'confirmLabel' => __('common.remove'), 'danger' => true, 'icon' => 'trash', 'event' => 'webauthnKeyRemoved', 'params' => ['id' => $id], 'notify' => '', ], ); } #[On('webauthnKeyRemoved')] public function remove(int $id, WebauthnService $webauthn): void { abort_unless($webauthn->available(), 404); $cred = Auth::user()->webauthnCredentials()->whereKey($id)->first(); if ($cred) { $name = $cred->name; $cred->delete(); AuditEvent::create([ 'user_id' => Auth::id(), 'actor' => Auth::user()->name ?? 'system', 'action' => 'webauthn.remove', 'target' => $name, 'ip' => request()->ip(), ]); $this->dispatch('notify', message: __('auth.webauthn_removed')); } } public function render() { return view('livewire.settings.webauthn-keys', [ 'available' => app(WebauthnService::class)->available() && Auth::user()->hasTwoFactorEnabled(), 'keys' => Auth::user()->webauthnCredentials()->latest()->get(), ]); } }