Passkeys
PandaBear tidak mengimplementasikan WebAuthn.
Passkey tetap merupakan tanggung jawab:
Laravel Fortify
+
laravel/passkeys2
3
PandaBear hanya menambahkan tiga integration point:
- Passkey card pada Security Settings;
- Passkey login button pada Login Page;
- Passkey dihitung sebagai second factor untuk
requireTwoFactor().
Contoh Minimal
Aktifkan Fortify feature:
// config/fortify.php
use Laravel\Fortify\Features;
'features' => [
Features::registration(),
Features::resetPasswords(),
Features::twoFactorAuthentication([
'confirm' => true,
]),
Features::passkeys(),
],2
3
4
5
6
7
8
9
10
11
12
13
14
15
User Model:
<?php
declare(strict_types=1);
namespace App\Models;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Laravel\Fortify\Contracts\PasskeyUser;
use Laravel\Fortify\PasskeyAuthenticatable;
final class User
extends Authenticatable
implements PasskeyUser
{
use PasskeyAuthenticatable;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Routes:
php artisan route:list --name=passkeyGET passkeys/login/options passkey.login-options
POST passkeys/login passkey.login
GET user/passkeys/options passkey.registration-options
POST user/passkeys passkey.store
DELETE user/passkeys/{passkey} passkey.destroy2
3
4
5
Semua route tersebut berasal dari Fortify.
PandaBear tidak mendaftarkan satupun.
Pembagian Tanggung Jawab
| Concern | Owner |
|---|---|
| WebAuthn ceremony | laravel/passkeys |
| Challenge storage | laravel/passkeys |
| Credential verification | laravel/passkeys |
| Passkey routes | Fortify |
| Password confirmation untuk management | Fortify |
| Passkey login | Fortify |
| Table/model Passkey | laravel/passkeys |
| Passkey card di Panel | PandaBear Security Settings |
| Passkey login button | PandaBear Login Page |
| Passkey sebagai second factor | RequireTwoFactor |
User Model Requirement
use Laravel\Fortify\Contracts\PasskeyUser;
use Laravel\Fortify\PasskeyAuthenticatable;2
Trait menyediakan:
| Member | Signature | Digunakan oleh |
|---|---|---|
passkeys() | passkeys(): HasMany | Security Settings, RequireTwoFactor |
hasPasskeysEnabled() | hasPasskeysEnabled(): bool | Application code |
getPasskeyUserHandle() | string | WebAuthn registration |
getPasskeyDisplayName() | string | display name |
getPasskeyUsername() | string | username |
Contoh model yang lengkap:
final class User
extends Authenticatable
implements
MustVerifyEmail,
PanelNotifiable,
PanelUser,
PasskeyUser
{
use Notifiable;
use PasskeyAuthenticatable;
use TwoFactorAuthenticatable;
}2
3
4
5
6
7
8
9
10
11
12
Security Settings Passkey Card
Props:
'canManagePasskeys' =>
Features::canManagePasskeys(),
'passkeys' =>
Features::canManagePasskeys()
? $this->passkeys()
: [],2
3
4
5
6
7
List:
$user
->passkeys()
->select([
'id',
'name',
'credential',
'created_at',
'last_used_at',
])
->latest()
->get()
->map(
static fn (
$passkey
): array => [
'id' =>
$passkey->id,
'name' =>
$passkey->name,
'authenticator' =>
$passkey
->authenticator,
'created_at_diff' =>
$passkey
->created_at
->diffForHumans(),
'last_used_at_diff' =>
$passkey
->last_used_at
?->diffForHumans(),
]
)
->values()
->all();2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
Payload:
| Key | Type | Arti |
|---|---|---|
id | int | Record key |
name | string | Nama yang diberikan user |
authenticator | string|null | Jenis authenticator berdasarkan AAGUID |
created_at_diff | string | Humanized timestamp |
last_used_at_diff | string|null | Terakhir digunakan |
credential di-select karena dibutuhkan untuk menghitung authenticator, tetapi tidak dikirim ke frontend.
Jika model bukan PasskeyUser, list menjadi:
[]TypeScript:
export type Passkey = {
id: number
name: string
authenticator: string | null
created_at_diff: string
last_used_at_diff: string | null
}2
3
4
5
6
7
Components
ManagePasskeys.vue
Props:
export type Props = {
canManagePasskeys?: boolean
passkeys?: Passkey[]
}2
3
4
Default:
canManagePasskeys = false
passkeys = []2
Jika feature tidak aktif, component tidak merender apa pun.
Delete menggunakan Fortify melalui Wayfinder:
import {
destroy,
} from '@/actions/Laravel/Passkeys/Http/Controllers/PasskeyRegistrationController'
router.delete(
destroy.url(id),
{
preserveScroll: true,
onError,
},
)2
3
4
5
6
7
8
9
10
11
Component bergantung pada host components:
@/components/PasskeyItem
@/components/PasskeyRegister
@/components/Heading2
3
Package tidak menyediakan component tersebut.
PasskeyVerify.vue
Digunakan di Login Page.
Props:
type Props = {
routes?: {
options: UrlMethodPair
submit: UrlMethodPair
}
label?: string
loadingLabel?: string
separator?: string
}2
3
4
5
6
7
8
9
10
Defaults:
label = Sign in with a passkey
loadingLabel = Authenticating...
separator = Or continue with email2
3
Contoh:
<PasskeyVerify
label="Use your security key"
separator="Or sign in with a password"
/>2
3
4
Menggunakan:
@laravel/passkeys/vueJika browser tidak mendukung WebAuthn:
button + separator tidak direnderTanpa custom routes, fallback:
/passkeys/login/options
/passkeys/login2
Success:
onSuccess: (
response
) => {
router.visit(
response.redirect
?? '/dashboard'
)
}2
3
4
5
6
7
8
Passkey sebagai Second Factor
RequireTwoFactor:
private function hasSecondFactor(
object $user
): bool {
if (
method_exists(
$user,
'hasEnabledTwoFactorAuthentication'
)
&& $user
->hasEnabledTwoFactorAuthentication()
) {
return true;
}
if (
EmailCodeFactor::isEnabledFor(
$user
)
) {
return true;
}
return method_exists(
$user,
'passkeys'
)
&& $user
->passkeys()
->exists();
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
Jadi satu Passkey cukup untuk memenuhi:
$panel
->auth()
->requireTwoFactor();2
3
method_exists() digunakan agar model tanpa trait menghasilkan:
falsebukan exception.
Host Modules
Published component membutuhkan:
| Module | Sumber |
|---|---|
| PasskeyRegistrationController action | Wayfinder |
PasskeyItem | Starter kit/application |
PasskeyRegister | Starter kit/application |
Generate:
php artisan wayfinder:generateInstaller dapat melaporkan missing modules:
php artisan panel:install@laravel/passkeys juga merupakan npm dependency.
Testing
use Inertia\Testing\AssertableInertia;
it(
'sends the passkey list to the security page',
function (): void {
$this
->actingAs($user)
->withSession([
'auth.password_confirmed_at'
=> now()->timestamp,
])
->get(
'/admin/settings/security'
)
->assertOk()
->assertInertia(
fn (
AssertableInertia $page
) =>
$page
->component(
'panel/settings/Security'
)
->has(
'canManagePasskeys'
)
->has(
'passkeys'
)
);
}
);2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
Untuk requireTwoFactor(), Panel hanya memeriksa:
$user
->passkeys()
->exists();2
3
Registration ceremony sendiri merupakan concern Fortify.
Hal yang Perlu Diperhatikan
- PandaBear tidak mendaftarkan Passkey route.
- Semua Panel menggunakan application-wide route yang sama.
PasskeyVerifyfallback ke/dashboardjika response tidak membawa redirect.RedirectPanelHomekemudian dapat mengarahkan ke Panel.- Panel pada custom domain merupakan WebAuthn origin yang berbeda.
fortify.passkeys.relying_party_iddanallowed_originsharus mencakup host tersebut.- Passkey management berada di balik password confirmation.
canManagePasskeys = falsemenyembunyikan card secara silent.- Passkey list tidak dipaginate.
- RequireTwoFactor dapat menjalankan
exists()query sekali per request untuk account yang belum memiliki TOTP atau Email Code.