Skip to content

Passkeys#2192

Open
JoanFo1456 wants to merge 20 commits intopelican-dev:mainfrom
JoanFo1456:passkeys
Open

Passkeys#2192
JoanFo1456 wants to merge 20 commits intopelican-dev:mainfrom
JoanFo1456:passkeys

Conversation

@JoanFo1456
Copy link
Contributor

Right now, only Jesus knows how this works, the added resources are required to work, either we ship them already or we use filament:assets.
I'm still open to changes, but it works, so by the law of a good developer, would be nice to NOT touch it.

@coderabbitai
Copy link

coderabbitai bot commented Feb 7, 2026

📝 Walkthrough

Walkthrough

This PR integrates WebAuthn-based passkey authentication into the application using Spatie's Laravel Passkeys package. It adds database migrations, user model updates, authentication routes, a Livewire component for passkey management in the profile, UI integration in the login page, JavaScript WebAuthn helpers, and related configuration and localization files.

Changes

Cohort / File(s) Summary
Core Passkey Integration
app/Models/User.php, app/Providers/Filament/FilamentServiceProvider.php, config/passkeys.php, database/migrations/...create_passkeys_table.php
User model now implements HasPasskeys with new methods getPasskeyDisplayName() and getPasskeyUserId(). FilamentServiceProvider registers the Passkeys Livewire component alias. New passkeys configuration file defines actions, relying party settings, and models. Migration creates passkeys table with foreign key to users.
Passkey Management Component
app/Livewire/Passkeys.php
New Livewire component extending PasskeysComponent, integrating Filament actions and schemas. Provides methods for storing passkeys, deleting passpkeys with confirmation, rendering the passkey list, and sending notifications on create/delete operations.
Authentication Routes
routes/auth.php
New auth/passkeys route group with two endpoints: GET /authentication-options (GeneratePasskeyAuthenticationOptionsController) and POST /authenticate (AuthenticateUsingPasskeyController).
Login & Profile Pages
app/Filament/Pages/Auth/Login.php, app/Filament/Pages/Auth/EditProfile.php
Login page adds content() method to include passkey login view. EditProfile page adds new "passkeys" tab with Fingerprint icon that renders the passkeys management component.
Passkey Views
resources/views/passkeys/*
New view files for passkey authentication (authenticate.blade.php, login.blade.php), passkey management UI (passkeys.blade.php, passkeys-tab.blade.php), profile display (profile.blade.php), and JavaScript integration script (createScript.blade.php).
Frontend Scripts & Dependencies
resources/js/app.js, resources/js/passkeys/index.js, package.json
App.js imports new passkeys module. Passkeys module exposes SimpleWebAuthn helpers (browserSupportsWebAuthn, startAuthentication, startRegistration) as global window functions. New NPM dependency @simplewebauthn/browser@^13.2.2 added.
Localization
lang/en/passkeys.php, lang/en/profile.php
New passkeys language file with translations for actions, statuses, descriptions, and notifications. Profile language file updated with "Passkeys" tab label.
Dependency Updates
composer.json
Laravel framework constraint updated from ^12.49 to ~12.49.0. New Spatie package dependency spatie/laravel-passkeys@^1.5 added.

Sequence Diagram

sequenceDiagram
    actor User
    participant Browser
    participant LoginPage as Login Page
    participant AuthController as Auth Controller
    participant WebAuthn as WebAuthn API
    participant PasskeysDB as Passkeys DB
    participant UserDB as User DB

    User->>LoginPage: Clicks "Authenticate with Passkey"
    LoginPage->>AuthController: GET /auth/passkeys/authentication-options
    AuthController->>PasskeysDB: Fetch passkeys for user
    AuthController->>LoginPage: Return passkey options
    LoginPage->>Browser: Call startAuthentication(options)
    Browser->>WebAuthn: Initiate WebAuthn ceremony
    WebAuthn->>User: Prompt for passkey
    User->>WebAuthn: Verify with passkey
    WebAuthn->>Browser: Return signed assertion
    Browser->>LoginPage: Receive passkey assertion
    LoginPage->>AuthController: POST /auth/passkeys/authenticate (assertion)
    AuthController->>PasskeysDB: Verify credential_id and signature
    AuthController->>UserDB: Fetch user and establish session
    AuthController->>LoginPage: Redirect to dashboard
    LoginPage->>User: Login successful
Loading
sequenceDiagram
    actor User
    participant ProfilePage as Profile Page
    participant PasskeysUI as Passkeys UI
    participant Livewire as Livewire Component
    participant WebAuthn as WebAuthn API
    participant PasskeysDB as Passkeys DB

    User->>ProfilePage: Navigate to Passkeys tab
    ProfilePage->>PasskeysUI: Render passkey management form
    User->>PasskeysUI: Enter passkey name and click create
    PasskeysUI->>Livewire: validatePasskeyProperties()
    Livewire->>PasskeysUI: Emit passkeyPropertiesValidated event
    PasskeysUI->>WebAuthn: Call startRegistration(passkeyOptions)
    WebAuthn->>User: Prompt to register new passkey
    User->>WebAuthn: Complete registration ceremony
    WebAuthn->>PasskeysUI: Return passkey credential
    PasskeysUI->>Livewire: storePasskey(passkey JSON)
    Livewire->>PasskeysDB: Store passkey with name and credential_id
    Livewire->>User: Send creation notification
    PasskeysUI->>PasskeysUI: Refresh passkey list
Loading
🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Passkeys' is vague and generic, using a non-descriptive term that doesn't convey meaningful information about the specific changes or primary purpose of the changeset. Consider a more descriptive title such as 'Add passkey authentication support' or 'Implement WebAuthn/passkeys integration' to better convey the PR's purpose.
✅ Passed checks (1 passed)
Check name Status Explanation
Description check ✅ Passed The description mentions passkeys functionality and explains resource requirements, which is related to the changeset containing passkey authentication implementation across multiple files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@Atelloblue

This comment was marked as off-topic.

@JoanFo1456
Copy link
Contributor Author

What is this exactly?

A passkey is something you can use to log in without the need of writing user and password each time, not sure how it entirely works, just know that.

@Atelloblue

This comment was marked as off-topic.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🤖 Fix all issues with AI agents
In `@app/Models/User.php`:
- Around line 104-109: The trait imports contain duplicates—remove the repeated
HasFactory and Notifiable from the mixed import so only the new trait remains;
specifically, keep the existing use HasFactory; keep the existing use
Notifiable; and change the combined line that currently reads use HasFactory,
InteractsWithPasskeys, Notifiable; to import only InteractsWithPasskeys (i.e.,
use InteractsWithPasskeys;) so trait imports are not duplicated in the User
model.

In `@public/css/marcelweidum/filament-passkeys/filament-passkeys-styles.css`:
- Around line 1-2: The committed compiled stylesheet
public/css/marcelweidum/filament-passkeys/filament-passkeys-styles.css is
orphaned and should be removed or integrated: either delete that file from the
repo (public/css/marcelweidum/filament-passkeys/filament-passkeys-styles.css) if
the marcelweidum/filament-passkeys integration already provides styles via
vendor/filament, or add an import for the stylesheet into the single CSS
entrypoint resources/css/app.css (so Vite builds/serves it) and remove the
compiled public asset to avoid duplication; update any packaging/build config
accordingly.

In `@resources/views/vendor/passkeys/components/authenticate.blade.php`:
- Around line 14-22: Replace the non-interactive <div> that calls
authenticateWithPasskey() with a semantic <button type="button"> so the passkey
action is focusable and keyboard-operable; keep the existing onclick handler
(authenticateWithPasskey()), transfer the CSS classes (e.g., "underline
cursor-pointer") to the button, preserve the conditional slot rendering (use the
same $slot or fallback text {{
__('passkeys::passkeys.authenticate_using_passkey') }}), and add any necessary
ARIA attributes (e.g., aria-label) if the visible text is insufficient for
screen readers.

In
`@resources/views/vendor/passkeys/components/partials/authenticateScript.blade.php`:
- Around line 2-17: The authenticateWithPasskey function lacks error handling
and doesn't check fetch status; wrap the logic in a try/catch inside
authenticateWithPasskey, check response.ok after fetch('{{
route('passkeys.authentication_options') }}') and throw or handle non-OK
statuses before calling response.json(), catch errors from both fetch and
startAuthentication({ optionsJSON: options }) and surface a user-visible message
(e.g., set an error text element or alert) so the user knows what failed; also
ensure you only add the 'formdata' listener to the element with id
'passkey-login-form' once (or remove it after use) to avoid duplicate handlers
when retrying.

In `@resources/views/vendor/passkeys/livewire/partials/createScript.blade.php`:
- Around line 3-8: Wrap the await startRegistration call inside a try/catch in
the Livewire.on('passkeyPropertiesValidated', ...) handler: try to await
startRegistration({ optionsJSON: passkeyOptions }) and on success call
`@this.call`('storePasskey', JSON.stringify(passkey)); in the catch block log the
error (console.error) and invoke a backend/error handler like
`@this.call`('passkeyRegistrationFailed', error.message) (or emit a Livewire
event) so user cancellation or WebAuthn errors are handled gracefully; update
the handler around startRegistration and storePasskey accordingly.

In `@resources/views/vendor/passkeys/livewire/passkeys.blade.php`:
- Around line 6-7: The label's for="name" doesn't match any input id, breaking
accessibility; update the input element with an id attribute that matches the
label (e.g., add id="name" to the <input> that uses wire:model="name") so the
<label for="name"> correctly associates and clicking the label focuses the
input; if there are multiple name fields ensure the id is unique but still
matches the label.
- Around line 31-35: The delete button in the passkeys.blade.php view calls
deletePasskey({{ $passkey->id }}) directly and lacks a confirmation step; update
the button to prompt the user before invoking the Livewire action (e.g., use
Livewire's wire:confirm or a JS confirm) so deletion is not immediate. Modify
the <button> that triggers deletePasskey to include a confirmation
attribute/handler that aborts the call when the user cancels, ensuring the
deletePasskey method (on the Livewire component) only runs after explicit user
confirmation.
🧹 Nitpick comments (3)
database/migrations/2026_02_07_103629_create_passkeys_table.php (1)

8-31: Missing down() method — rollback will fail.

The migration lacks a down() method, so php artisan migrate:rollback will not undo this table creation. While not a blocker, it's good practice to include one.

♻️ Add down() method
     });
   }
+
+  public function down(): void
+  {
+      Schema::dropIfExists('passkeys');
+  }
 };
public/js/marcelweidum/filament-passkeys/filament-passkeys-scripts.js (1)

1-2: Vendored minified JS lacks provenance and version tracking.

This appears to be a pre-built asset from the filament-passkeys package (wrapping @simplewebauthn/browser). Committing minified vendor JS directly makes it impossible to audit changes, verify integrity, or track the upstream version. Consider:

  1. Documenting the exact package version this was built from (e.g., in a comment or adjacent VERSION file).
  2. Ideally, relying on filament:assets or composer's asset publishing (php artisan vendor:publish) so the asset is reproducible from a known package version rather than a manually committed blob.
resources/views/vendor/passkeys/livewire/passkeys.blade.php (1)

1-42: Hardcoded light-mode styles won't work with Filament's dark mode.

Classes like text-gray-700, bg-gray-100, border-gray-300 are light-mode-only. Filament supports dark mode, and these elements will be unreadable or visually broken in dark themes. Consider using Filament's built-in component classes or adding dark: variants.

Comment on lines +2 to +17
async function authenticateWithPasskey(remember = false) {
const response = await fetch('{{ route('passkeys.authentication_options') }}')

const options = await response.json();

const startAuthenticationResponse = await startAuthentication({ optionsJSON: options, });

const form = document.getElementById('passkey-login-form');

form.addEventListener('formdata', ({formData}) => {
formData.set('remember', remember);
formData.set('start_authentication_response', JSON.stringify(startAuthenticationResponse));
});

form.submit();
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

No error handling — user gets no feedback on failure.

Both fetch() and startAuthentication() can fail (network error, user cancels ceremony, server error), but there is no try/catch. An unhandled rejection leaves the user stuck with no indication of what went wrong. Additionally, the fetch response status isn't checked before parsing JSON.

Proposed fix
     async function authenticateWithPasskey(remember = false) {
-        const response = await fetch('{{ route('passkeys.authentication_options') }}')
-
-        const options = await response.json();
-
-        const startAuthenticationResponse = await startAuthentication({ optionsJSON: options, });
-
-        const form = document.getElementById('passkey-login-form');
-    
-        form.addEventListener('formdata', ({formData}) => {
-            formData.set('remember', remember);
-            formData.set('start_authentication_response', JSON.stringify(startAuthenticationResponse));
-        });
-
-        form.submit();
+        try {
+            const response = await fetch('{{ route('passkeys.authentication_options') }}');
+
+            if (!response.ok) {
+                throw new Error('Failed to fetch authentication options');
+            }
+
+            const options = await response.json();
+
+            const startAuthenticationResponse = await startAuthentication({ optionsJSON: options });
+
+            const form = document.getElementById('passkey-login-form');
+
+            form.addEventListener('formdata', ({formData}) => {
+                formData.set('remember', remember);
+                formData.set('start_authentication_response', JSON.stringify(startAuthenticationResponse));
+            });
+
+            form.submit();
+        } catch (error) {
+            console.error('Passkey authentication failed:', error);
+            alert(error.message || 'Authentication failed. Please try again.');
+        }
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async function authenticateWithPasskey(remember = false) {
const response = await fetch('{{ route('passkeys.authentication_options') }}')
const options = await response.json();
const startAuthenticationResponse = await startAuthentication({ optionsJSON: options, });
const form = document.getElementById('passkey-login-form');
form.addEventListener('formdata', ({formData}) => {
formData.set('remember', remember);
formData.set('start_authentication_response', JSON.stringify(startAuthenticationResponse));
});
form.submit();
}
async function authenticateWithPasskey(remember = false) {
try {
const response = await fetch('{{ route('passkeys.authentication_options') }}');
if (!response.ok) {
throw new Error('Failed to fetch authentication options');
}
const options = await response.json();
const startAuthenticationResponse = await startAuthentication({ optionsJSON: options });
const form = document.getElementById('passkey-login-form');
form.addEventListener('formdata', ({formData}) => {
formData.set('remember', remember);
formData.set('start_authentication_response', JSON.stringify(startAuthenticationResponse));
});
form.submit();
} catch (error) {
console.error('Passkey authentication failed:', error);
alert(error.message || 'Authentication failed. Please try again.');
}
}
🤖 Prompt for AI Agents
In
`@resources/views/vendor/passkeys/components/partials/authenticateScript.blade.php`
around lines 2 - 17, The authenticateWithPasskey function lacks error handling
and doesn't check fetch status; wrap the logic in a try/catch inside
authenticateWithPasskey, check response.ok after fetch('{{
route('passkeys.authentication_options') }}') and throw or handle non-OK
statuses before calling response.json(), catch errors from both fetch and
startAuthentication({ optionsJSON: options }) and surface a user-visible message
(e.g., set an error text element or alert) so the user knows what failed; also
ensure you only add the 'formdata' listener to the element with id
'passkey-login-form' once (or remove it after use) to avoid duplicate handlers
when retrying.

Comment on lines +6 to +7
<label for="name" class="block text-sm font-medium text-gray-700">{{ __('passkeys::passkeys.name') }}</label>
<input autocomplete="off" type="text" wire:model="name" class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
Copy link

@coderabbitai coderabbitai bot Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Label for attribute doesn't match any input id — broken accessibility.

The <label for="name"> won't associate with the <input> because the input is missing id="name". Clicking the label won't focus the input.

Proposed fix
-                <input autocomplete="off" type="text" wire:model="name" class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
+                <input id="name" autocomplete="off" type="text" wire:model="name" class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<label for="name" class="block text-sm font-medium text-gray-700">{{ __('passkeys::passkeys.name') }}</label>
<input autocomplete="off" type="text" wire:model="name" class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
<label for="name" class="block text-sm font-medium text-gray-700">{{ __('passkeys::passkeys.name') }}</label>
<input id="name" autocomplete="off" type="text" wire:model="name" class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
🤖 Prompt for AI Agents
In `@resources/views/vendor/passkeys/livewire/passkeys.blade.php` around lines 6 -
7, The label's for="name" doesn't match any input id, breaking accessibility;
update the input element with an id attribute that matches the label (e.g., add
id="name" to the <input> that uses wire:model="name") so the <label for="name">
correctly associates and clicking the label focuses the input; if there are
multiple name fields ensure the id is unique but still matches the label.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This ^^^

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lancepioch Thanks for confirming! Adding id="name" to the input will ensure the label association works correctly, improving keyboard navigation and screen reader support for users managing their passkeys.

Comment on lines +31 to +35
<div>
<button wire:click="deletePasskey({{ $passkey->id }})" class="inline-flex justify-center py-2 px-4 text-sm font-medium text-white bg-red-600">
{{ __('passkeys::passkeys.delete') }}
</button>
</div>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Passkey deletion has no confirmation dialog.

Deleting a passkey is a destructive, irreversible action that could lock a user out of their account if it's their only authentication method. A single misclick on the delete button would immediately remove it. Add a confirmation step (e.g., wire:confirm).

Proposed fix using Livewire's wire:confirm
-                        <button wire:click="deletePasskey({{ $passkey->id }})" class="inline-flex justify-center py-2 px-4 text-sm font-medium text-white bg-red-600">
+                        <button wire:click="deletePasskey({{ $passkey->id }})" wire:confirm="{{ __('passkeys::passkeys.confirm_delete') }}" class="inline-flex justify-center py-2 px-4 text-sm font-medium text-white bg-red-600 rounded-md">
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div>
<button wire:click="deletePasskey({{ $passkey->id }})" class="inline-flex justify-center py-2 px-4 text-sm font-medium text-white bg-red-600">
{{ __('passkeys::passkeys.delete') }}
</button>
</div>
<div>
<button wire:click="deletePasskey({{ $passkey->id }})" wire:confirm="{{ __('passkeys::passkeys.confirm_delete') }}" class="inline-flex justify-center py-2 px-4 text-sm font-medium text-white bg-red-600 rounded-md">
{{ __('passkeys::passkeys.delete') }}
</button>
</div>
🤖 Prompt for AI Agents
In `@resources/views/vendor/passkeys/livewire/passkeys.blade.php` around lines 31
- 35, The delete button in the passkeys.blade.php view calls deletePasskey({{
$passkey->id }}) directly and lacks a confirmation step; update the button to
prompt the user before invoking the Livewire action (e.g., use Livewire's
wire:confirm or a JS confirm) so deletion is not immediate. Modify the <button>
that triggers deletePasskey to include a confirmation attribute/handler that
aborts the call when the user cancels, ensuring the deletePasskey method (on the
Livewire component) only runs after explicit user confirmation.

@JoanFo1456
Copy link
Contributor Author

As for the other Rabbit comments, I'm not sure if I have to act or leave it this way, as it's how the package does it.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@app/Models/User.php`:
- Around line 516-519: The method getPasskeyUserId() currently returns an int
which violates the HasPasskeys interface (expects string) and will cause a
TypeError; update getPasskeyUserId() to return a string by casting the id to
string (e.g. return (string) $this->id) or, if preferred, return the user's UUID
(e.g. return $this->uuid) so the method signature and return type match the
interface and WebAuthn expectations.
🧹 Nitpick comments (1)
app/Models/User.php (1)

510-514: username is required and cannot be null — the ?? fallback is dead code.

Per the validation rules on Line 165, username is required. The null-coalesce to $this->email will never trigger. Not harmful, but worth noting — if you truly want a fallback for edge cases (e.g., incomplete model hydration), this is fine as defensive coding.

@pelican-dev pelican-dev deleted a comment from b0rrajo Feb 7, 2026
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm almost positive this should NOT be committed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm almost positive this should NOT be committed.

Was making Rabbit suggestions but if you check discussion I need to redo this entirely ig...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm almost positive this should NOT be committed.

Comment on lines +6 to +7
<label for="name" class="block text-sm font-medium text-gray-700">{{ __('passkeys::passkeys.name') }}</label>
<input autocomplete="off" type="text" wire:model="name" class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This ^^^

@JoanFo1456 JoanFo1456 marked this pull request as draft February 7, 2026 14:36
@JoanFo1456
Copy link
Contributor Author

I'm aware of the tests failing, just found that composer upgraded everything when adding the spatie passkeys, I'll be checking how to fix it and push, I miss my server...

@JoanFo1456
Copy link
Contributor Author

The last commit to shut phpstan was not tested entirely, I appreciate any suggestions on that + the pr itself.

@JoanFo1456 JoanFo1456 marked this pull request as ready for review February 7, 2026 21:43
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

🤖 Fix all issues with AI agents
In `@app/Filament/PasskeysPlugin.php`:
- Line 49: Remove the duplicate Livewire component registration by keeping the
Livewire::component('filament-passkeys', Passkeys::class) call only inside
PasskeysPlugin::boot() and deleting the identical registration in
FilamentServiceProvider; locate the duplicate Livewire::component(...)
invocation in FilamentServiceProvider and remove that line so the component is
registered exclusively by PasskeysPlugin::boot().

In `@app/Livewire/Passkeys.php`:
- Around line 26-33: The Action handler in deleteAction currently forwards
$arguments['passkey'] directly to deletePasskey(int $passkeyId) which can be
re-hydrated as a string under Filament v3/Livewire with strict_types=1; update
the action closure in deleteAction to cast the incoming argument to int (e.g.,
(int) $arguments['passkey']) before calling deletePasskey so the signature of
deletePasskey matches and no TypeError occurs.
- Around line 35-43: The deletePasskey method currently calls
parent::deletePasskey(passkeyId) without verifying ownership, allowing deletion
of other users' passkeys; before calling parent::deletePasskey(int $passkeyId)
validate the authenticated user's ownership by loading the passkey via
auth()->user()->passkeys()->findOrFail($passkeyId) (or equivalent) to ensure the
passkey belongs to the current user, then proceed to call parent::deletePasskey
and send the Notification as before.

In `@config/passkeys.php`:
- Around line 26-30: The relying_party 'id' currently uses
parse_url(config('app.url'), PHP_URL_HOST) which can be null if app.url is
missing or malformed; update config/passkeys.php to compute the host from
parse_url(config('app.url'), PHP_URL_HOST) and if that yields null fall back to
parsing env('APP_URL') and finally to a safe default (e.g. config('app.name') or
request()->getHost()) so the 'relying_party' => ['id' => ...] is never null;
adjust the logic around the relying_party 'id' entry (parse_url,
config('app.url')) to use this fallback chain.
- Line 8: Replace the hardcoded redirect value for 'redirect_to_after_login'
with a Filament URL helper so the post-login redirect adapts to panel path
changes: use Filament::getPanel('app')->getUrl() (or call the appropriate static
helper to obtain the app panel URL) in place of '/' for the
'redirect_to_after_login' config entry; alternatively, if you prefer
user-controlled redirects, update the existing 'redirect_to_admin' preference
handling to feed its value into the same config key so users can choose their
passkey post-login destination.

In `@lang/en/passkeys.php`:
- Around line 5-19: The translation entries (e.g., authenticate_using_passkey,
create, delete, error_something_went_wrong_generating_the_passkey, invalid,
last_used, name, name_placeholder, no_passkeys_registered, not_used_yet,
passkeys, description, created_notification_title, deleted_notification_title)
should be plain string values, not calls to the translator helper; replace each
__('passkeys::passkeys.<key>') expression with the actual translation string for
that key (or the package's published string) so the returned array contains
literal strings and does not invoke __() during file evaluation or caching.

In `@lang/en/profile.php`:
- Line 73: Fix the typo in the translation value for the 'passkeys_description'
key by changing "1Pass" to "1Password", and remove the near‑identical duplicate
by centralizing the string: keep the canonical 'passkeys_description' entry in
one translations file (e.g., the passkeys translations) and have the other file
(the profile translations) reference that same key instead of duplicating the
text so both use a single source of truth.

In `@resources/views/passkeys/livewire/passkeys.blade.php`:
- Line 51: The `@include`('passkeys::livewire.partials.createScript') line in the
passkeys.blade.php view references a non-existent view and an unregistered view
namespace; either remove that include entirely or replace it with the correct
dotted view path (e.g., use the existing dot-notation view name for the partial)
or create the missing partial file at
resources/views/passkeys/livewire/partials/createScript.blade.php so the include
resolves; update the include reference string accordingly wherever it appears.
- Around line 34-40: The deletePasskey method is missing an ownership check and
accepts a raw ID from the frontend, enabling an IDOR; update deletePasskey to
first load the passkey via the current user relationship (e.g.,
$this->currentUser()->passkeys()->findOrFail($passkeyId)) to ensure it belongs
to the authenticated user, then call parent::deletePasskey($passkeyId) (or
delete via the resolved model) so deletion only proceeds for owned passkeys.
🧹 Nitpick comments (3)
app/Filament/Pages/Auth/EditProfile.php (1)

237-248: Consider adding a visibility condition for the passkeys tab.

Other tabs like oauth (line 189) are conditionally visible. The passkeys tab is always shown regardless of configuration. If the passkeys feature should be toggleable (e.g., via config or feature flag), consider adding a ->visible(...) clause similar to other conditional tabs.

resources/views/passkeys/components/authenticate.blade.php (1)

8-15: Session-based notification in Blade @php block works but is unconventional.

Triggering Filament\Notifications\Notification::make()->send() inside a @php block in Blade is functional but couples notification logic to the view layer. This is a minor concern — consider moving this to a controller/middleware or Livewire component for cleaner separation. Not blocking.

config/passkeys.php (1)

15-21: Inconsistent FQCN formatting: leading backslash on line 18.

Line 18 uses \Spatie\LaravelPasskeys\Actions\... with a leading backslash, while lines 16, 17, 19, and 20 omit it. This is functionally equivalent but inconsistent.

Proposed fix
-        'generate_passkey_authentication_options' => \Spatie\LaravelPasskeys\Actions\GeneratePasskeyAuthenticationOptionsAction::class,
+        'generate_passkey_authentication_options' => Spatie\LaravelPasskeys\Actions\GeneratePasskeyAuthenticationOptionsAction::class,

'no_oauth' => 'No Accounts Linked',
'no_api_keys' => 'No API Keys',
'no_ssh_keys' => 'No SSH Keys',
'passkeys_description' => 'Passkeys let you log in without needing a password. Instead of a password, you can generate a passkey which will be stored in 1Pass, MacOS password app, or alternative app on your favourite OS.',
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Typo: "1Pass" should be "1Password".

Also, this description is duplicated nearly verbatim in lang/en/passkeys.php (line 17). Consider defining it in one place and referencing it from the other to avoid drift.

🤖 Prompt for AI Agents
In `@lang/en/profile.php` at line 73, Fix the typo in the translation value for
the 'passkeys_description' key by changing "1Pass" to "1Password", and remove
the near‑identical duplicate by centralizing the string: keep the canonical
'passkeys_description' entry in one translations file (e.g., the passkeys
translations) and have the other file (the profile translations) reference that
same key instead of duplicating the text so both use a single source of truth.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@resources/views/passkeys/livewire/partials/createScript.blade.php`:
- Around line 1-11: The handler registered in
Livewire.on('passkeyPropertiesValidated', ...) calls startRegistration(...)
without error handling, causing unhandled promise rejections when the user
cancels or the browser lacks passkey support; wrap the await startRegistration
call in a try/catch, on success continue to call `@this.call`('storePasskey',
JSON.stringify(passkey)), and on error call a new Livewire endpoint (e.g.,
`@this.call`('handlePasskeyError', error.message)) or emit a client-side event to
surface a user-friendly message and log the error; also inspect error.name
(e.g., NotAllowedError / NotSupportedError / AbortError) to tailor the message.
🧹 Nitpick comments (2)
app/Filament/Pages/Auth/EditProfile.php (1)

237-248: Consider adding a visibility condition for the passkeys tab.

Other tabs like oauth and 2fa conditionally show based on feature availability (e.g., ->visible(fn () => Filament::hasMultiFactorAuthentication())). The passkeys tab is always visible regardless of whether the PasskeysPlugin is registered on the panel. If passkeys support is optional or can be disabled, guard this tab similarly.

 Tab::make('passkeys')
     ->label(trans('profile.tabs.passkeys'))
     ->icon(TablerIcon::Fingerprint)
+    ->visible(fn () => filament()->hasPlugin('filament-passkeys'))
     ->schema([
app/Livewire/Passkeys.php (1)

47-55: Consider wrapping storePasskey in a try/catch for a friendlier UX.

If parent::storePasskey() throws (e.g., malformed WebAuthn response, duplicate key), the user sees an unhandled error instead of a notification. A try/catch with an error notification would be more consistent with the delete flow's UX.

Suggested improvement
 public function storePasskey(string $passkey): void
 {
-    parent::storePasskey($passkey);
-
-    Notification::make()
-        ->title(__('passkeys.created_notification_title'))
-        ->success()
-        ->send();
+    try {
+        parent::storePasskey($passkey);
+
+        Notification::make()
+            ->title(__('passkeys.created_notification_title'))
+            ->success()
+            ->send();
+    } catch (\Throwable $e) {
+        Notification::make()
+            ->title(__('passkeys.error_notification_title'))
+            ->danger()
+            ->send();
+    }
 }

@JoanFo1456 JoanFo1456 requested a review from Boy132 February 8, 2026 17:00
->description(trans('passkeys.description'))
->schema([
Group::make([
view('passkeys.livewire.passkeys-tab'),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think you should be adding a view like this. iirc there is a View component from filament.

@@ -0,0 +1,63 @@
<?php

declare(strict_types=1);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove this.

);

FilamentView::registerRenderHook(
PanelsRenderHook::AUTH_LOGIN_FORM_AFTER,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't need a render hook for the login form, we already have a custom login page.
Same goes for all the blade views.

public function subject(...$subjects): self
{
foreach (Arr::wrap($subjects) as $subject) {
foreach ($subjects as $subject) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unrelated change?

Copy link
Contributor Author

@JoanFo1456 JoanFo1456 Feb 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know, that was a laravel bug and fixed with #2167

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was using main the day I made passkeys with the latest changes, then how is it possible to fail?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You must have done composer upgrade as it updated to v12.50 which is broken.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You must have done composer upgrade as it updated to v12.50 which is broken.

I didn't, i just made a require to spatie's passkeys.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure why this needs to be a giant discussion. Require 12.49 , revert this change, and be done with it.

@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove this.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@composer.json`:
- Line 18: The composer.json entry "laravel/framework": "~12.49.0" is too
restrictive and pins Laravel to patch releases only; change this constraint back
to a caret-based range (e.g. "^12.49") so the project can receive minor releases
within Laravel 12.x, and then run composer update to confirm dependency
resolution; if composer fails, identify and fix the underlying conflicting
package (inspect composer why/why-not and check packages that require
illuminate/* such as spatie/laravel-passkeys) rather than keeping the tilde pin.

"gboquizosanchez/filament-log-viewer": "^2.1",
"guzzlehttp/guzzle": "^7.10",
"laravel/framework": "^12.49",
"laravel/framework": "~12.49.0",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's check the actual composer.json file to confirm the constraint
cat composer.json | head -50

Repository: pelican-dev/panel

Length of output: 1973


🌐 Web query:

spatie laravel-passkeys composer requirements

💡 Result:

spatie/laravel-passkeys Composer requirements (current Packagist release):

  • php: ^8.2 | ^8.3 | ^8.4
  • illuminate/contracts: ^11.0 | ^12.0 (i.e., Laravel 11 or 12 components)
  • spatie/laravel-package-tools: ^1.16
  • web-auth/webauthn-lib: ^5.0 [1]

Docs note (Spatie): the package “requires PHP 8.4+, Laravel 12+.” [2]

Practical requirement: it includes a Livewire component, so you should have Livewire installed. [3]

Sources: [1] (packagist.org) [2] (spatie.be) [3] (github.com)

Citations:


Unnecessarily restrictive Laravel version constraint.

Changing from ^12.49 to ~12.49.0 pins Laravel to patch-level updates only (12.49.x), blocking all future minor releases (12.50, 12.51, …). This prevents the project from receiving new features, bug fixes, and security patches shipped in minor versions.

spatie/laravel-passkeys supports illuminate/contracts: ^11.0 | ^12.0, meaning it's compatible with all Laravel 12.x versions—there is no technical reason to restrict Laravel to 12.49.x. This constraint appears to be a workaround for a dependency resolution issue rather than a legitimate compatibility requirement. Investigate and resolve the root cause instead of locking down the version.

Proposed fix: revert to the original caret constraint
-        "laravel/framework": "~12.49.0",
+        "laravel/framework": "^12.49",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"laravel/framework": "~12.49.0",
"laravel/framework": "^12.49",
🤖 Prompt for AI Agents
In `@composer.json` at line 18, The composer.json entry "laravel/framework":
"~12.49.0" is too restrictive and pins Laravel to patch releases only; change
this constraint back to a caret-based range (e.g. "^12.49") so the project can
receive minor releases within Laravel 12.x, and then run composer update to
confirm dependency resolution; if composer fails, identify and fix the
underlying conflicting package (inspect composer why/why-not and check packages
that require illuminate/* such as spatie/laravel-passkeys) rather than keeping
the tilde pin.

$this->captchaService = $captchaService;
}

public function content(Schema $schema): Schema
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use the form function that's already there. And remove all the extra views.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I was checking this, and I don't seem to find a way to make it how you want, as the view is required for the button to appear.
I would like it if you could give me some kind of direction so I can fix it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You extract the components that are used in the views and add them to the form instead, e.g. with Action::make or TextInput::make or whatever is needed.

Copy link
Contributor Author

@JoanFo1456 JoanFo1456 Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You extract the components that are used in the views and add them to the form instead, e.g. with Action::make or TextInput::make or whatever is needed.

I was checking this, and I don't seem to know how, as each method will always return me to the same point. JS won't call passkeys or if it does, it won't log in.
MarcelWeldium passkeys worked this way, and it requires the views for the actions to work.

It uses a lot of custom JS on a lot of files to do certain things, view is necessary to ensure everything works, as each file imports another view.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll still try to find a way, but I'm not sure about it...

@JoanFo1456
Copy link
Contributor Author

@Boy132
After checking these days, at least on my end, I didn't find a way to make it work entirely. If you please have time, could you check it?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants