Skip to content

Commit 37afffb

Browse files
Frontend Passkeys (#1883)
1 parent 3493394 commit 37afffb

4 files changed

Lines changed: 291 additions & 0 deletions

File tree

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
---
2+
id: d0ed4ac0-536a-47a1-965b-202b52baddb2
3+
blueprint: tag
4+
title: 'User:Delete_Passkey_Form'
5+
description: 'Creates a form to delete a passkey'
6+
intro: 'As the tag name suggests, it allows you to delete a passkey.'
7+
parameters:
8+
-
9+
name: id
10+
type: string
11+
description: 'The passkey ID to delete. Required.'
12+
-
13+
name: redirect
14+
type: string
15+
description: Where the user should be taken after successfully deleting a passkey.
16+
-
17+
name: HTML Attributes
18+
type:
19+
description: 'Set HTML attributes as if you were in an HTML element. For example, `class="delete-form"`.'
20+
related_entries:
21+
- 38323438-4719-4a7b-ba5a-8abfe0d7dfc0
22+
- 7a958307-4cdb-47f3-a689-0c7de57e3ff7
23+
- 7432f1cb-7418-4d54-8e65-51b1ae3bcb3a
24+
---
25+
## Overview
26+
27+
The `user:delete_passkey_form` tag renders a form to delete a passkey.
28+
29+
### Example
30+
31+
The tag is typically used inside a [`{{ user:passkeys }}`](/tags/user-passkeys) loop:
32+
33+
::tabs
34+
35+
::tab antlers
36+
```antlers
37+
{{ user:passkeys as="passkeys" }}
38+
{{ passkeys }}
39+
<div>
40+
{{ name }}
41+
42+
{{ user:delete_passkey_form :id="id" }}
43+
<button type="submit">Delete</button>
44+
{{ /user:delete_passkey_form }}
45+
</div>
46+
{{ /passkeys }}
47+
{{ /user:passkeys }}
48+
```
49+
::tab blade
50+
```blade
51+
<s:user:passkeys as="passkeys">
52+
@foreach ($passkeys as $passkey)
53+
<div>
54+
{{ $passkey['name'] }}
55+
56+
<s:user:delete_passkey_form :id="$passkey['id']">
57+
<button type="submit">Delete</button>
58+
</s:user:delete_passkey_form>
59+
</div>
60+
@endforeach
61+
</s:user:passkeys>
62+
```
63+
::

content/collections/tags/user-login_form.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,14 @@ variables:
3434
name: success
3535
type: string
3636
description: A success message.
37+
-
38+
name: passkey_options_url
39+
type: string
40+
description: URL to fetch WebAuthn assertion options for passkey login.
41+
-
42+
name: passkey_verify_url
43+
type: string
44+
description: URL to verify passkey login.
3745
id: 7432f1cb-7418-4d54-8e65-51b1ae3bcb3a
3846
---
3947
## Overview
@@ -101,3 +109,45 @@ The tag will render the opening and closing `<form>` HTML elements for you. The
101109
</s:user:login_form>
102110
```
103111
::
112+
113+
## Passkeys
114+
115+
You can add passkey authentication to your login form using Statamic's frontend JavaScript helpers.
116+
117+
1. First, include the helpers script on your page:
118+
```html
119+
<script src="/vendor/statamic/frontend/js/helpers.js"></script>
120+
```
121+
122+
2. Use the provided variables to add a passkey login option:
123+
```antlers
124+
{{ user:login_form }}
125+
<input type="email" name="email" value="{{ old:email }}" />
126+
<input type="password" name="password" value="{{ old:password }}" />
127+
<button type="submit">Log in with Password</button>
128+
129+
<button type="button" id="passkey-login">Login with Passkey</button> {{# [tl! focus:start] #}}
130+
131+
<script>
132+
Statamic.$passkeys.configure({
133+
optionsUrl: '{{ passkey_options_url }}',
134+
verifyUrl: '{{ passkey_verify_url }}',
135+
onSuccess: (data) => window.location = data.redirect || '/',
136+
onError: (error) => alert(error.message)
137+
});
138+
139+
document.getElementById('passkey-login').addEventListener('click', () => {
140+
Statamic.$passkeys.authenticate();
141+
});
142+
143+
// Enable browser autofill for passkeys
144+
Statamic.$passkeys.initAutofill();
145+
</script> {{# [tl! focus:end] #}}
146+
{{ /user:login_form }}
147+
```
148+
3. Add `autocomplete="username webauthn"` to your email input for browser autofill to work.
149+
150+
For more information on managing passkeys on the frontend, see the following docs:
151+
- [`{{ user:passkeys }}`](/tags/user-passkeys)
152+
- [`{{ user:passkey_form }}`](/tags/user-passkey_form)
153+
- [`{{ user:delete_passkey_form }}`](/tags/user-delete_passkey_form)
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
---
2+
id: 7a958307-4cdb-47f3-a689-0c7de57e3ff7
3+
blueprint: tag
4+
title: 'User:Passkey_Form'
5+
description: 'Creates a passkey registration form'
6+
intro: 'Allows authenticated users to set up passkeys'
7+
variables:
8+
-
9+
name: passkey_option_url
10+
type: string
11+
description: 'URL to fetch WebAuthn attestation options for creating a new passkey.'
12+
-
13+
name: passkey_verify_url
14+
type: string
15+
description: 'URL to store the new passkey after registration.'
16+
related_entries:
17+
- 38323438-4719-4a7b-ba5a-8abfe0d7dfc0
18+
- d0ed4ac0-536a-47a1-965b-202b52baddb2
19+
- 7432f1cb-7418-4d54-8e65-51b1ae3bcb3a
20+
---
21+
## Overview
22+
23+
The `user:passkey_form` tag provides the necessary URLs to set up passkeys for authenticated users.
24+
25+
### JavaScript helpers
26+
27+
You'll need to include the frontend helpers script on your page:
28+
29+
```html
30+
<script src="/vendor/statamic/frontend/js/helpers.js"></script>
31+
```
32+
33+
### Example
34+
35+
::tabs
36+
37+
::tab antlers
38+
```antlers
39+
{{ user:passkey_form }}
40+
<input type="text" id="passkey-name" placeholder="Passkey name (e.g., My Laptop)">
41+
<button type="button" id="create-passkey">Create Passkey</button>
42+
43+
<script>
44+
document.getElementById('create-passkey').addEventListener('click', () => {
45+
const name = document.getElementById('passkey-name').value || 'My Passkey';
46+
47+
Statamic.$passkeys.register({
48+
optionsUrl: '{{ passkey_option_url }}',
49+
verifyUrl: '{{ passkey_verify_url }}',
50+
name: name,
51+
onSuccess: () => location.reload(),
52+
onError: (error) => alert(error.message),
53+
// csrfToken (optional)
54+
});
55+
});
56+
</script>
57+
{{ /user:passkey_form }}
58+
```
59+
::tab blade
60+
```blade
61+
<s:user:passkey_form>
62+
<input type="text" id="passkey-name" placeholder="Passkey name (e.g., My Laptop)">
63+
<button type="button" id="create-passkey">Create Passkey</button>
64+
65+
<script>
66+
document.getElementById('create-passkey').addEventListener('click', () => {
67+
const name = document.getElementById('passkey-name').value || 'My Passkey';
68+
69+
Statamic.$passkeys.register({
70+
optionsUrl: '{{ $passkey_option_url }}',
71+
verifyUrl: '{{ $passkey_verify_url }}',
72+
name: name,
73+
onSuccess: () => location.reload(),
74+
onError: (error) => alert(error.message),
75+
// csrfToken (optional)
76+
});
77+
});
78+
</script>
79+
</s:user:passkey_form>
80+
```
81+
::
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
---
2+
id: 38323438-4719-4a7b-ba5a-8abfe0d7dfc0
3+
blueprint: tag
4+
title: 'User:Passkeys'
5+
description: 'Lists the current user''s passkeys'
6+
intro: 'Loop through the authenticated user''s registered passkeys.'
7+
variables:
8+
-
9+
name: id
10+
type: string
11+
description: 'The passkey identifier.'
12+
-
13+
name: name
14+
type: string
15+
description: 'The user-defined passkey name.'
16+
-
17+
name: last_login
18+
type: Carbon
19+
description: 'The last time the passkey was used for login, or null if never used.'
20+
related_entries:
21+
- 7432f1cb-7418-4d54-8e65-51b1ae3bcb3a
22+
- 7a958307-4cdb-47f3-a689-0c7de57e3ff7
23+
- d0ed4ac0-536a-47a1-965b-202b52baddb2
24+
---
25+
## Overview
26+
27+
The `user:passkeys` tag loops through the user's passkeys. Useful for building a passkey management page where users can view and delete their passkeys.
28+
29+
### Example
30+
31+
::tabs
32+
33+
::tab antlers
34+
```antlers
35+
{{ user:passkeys as="passkeys" }}
36+
{{ if passkeys }}
37+
<h3>Your Passkeys</h3>
38+
<ul>
39+
{{ passkeys }}
40+
<li>
41+
<strong>{{ name }}</strong>
42+
{{ if last_login }}
43+
<span>Last used: {{ last_login format="M j, Y g:i A" }}</span>
44+
{{ else }}
45+
<span>Never used</span>
46+
{{ /if }}
47+
48+
{{ user:delete_passkey_form :id="id" }}
49+
<button type="submit">Delete</button>
50+
{{ /user:delete_passkey_form }}
51+
</li>
52+
{{ /passkeys }}
53+
</ul>
54+
{{ else }}
55+
<p>You haven't set up any passkeys yet.</p>
56+
{{ /if }}
57+
{{ /user:passkeys }}
58+
```
59+
::tab blade
60+
```blade
61+
<s:user:passkeys as="passkeys">
62+
@if ($passkeys)
63+
<h3>Your Passkeys</h3>
64+
<ul>
65+
@foreach ($passkeys as $passkey)
66+
<li>
67+
<strong>{{ $passkey['name'] }}</strong>
68+
@if ($passkey['last_login'])
69+
<span>Last used: {{ $passkey['last_login']->format('M j, Y g:i A') }}</span>
70+
@else
71+
<span>Never used</span>
72+
@endif
73+
74+
<s:user:delete_passkey_form :id="$passkey['id']">
75+
<button type="submit">Delete</button>
76+
</s:user:delete_passkey_form>
77+
</li>
78+
@endforeach
79+
</ul>
80+
@else
81+
<p>You haven't set up any passkeys yet.</p>
82+
@endif
83+
</s:user:passkeys>
84+
```
85+
::
86+
87+
## Aliasing
88+
89+
You can use the `as` parameter to alias the passkeys into a variable, which allows you to use `{{ if passkeys }}` to check if there are any passkeys before rendering.
90+
91+
```antlers
92+
{{ user:passkeys as="passkeys" }}
93+
{{ if passkeys }}
94+
{{# Render passkeys list #}}
95+
{{ /if }}
96+
{{ /user:passkeys }}
97+
```

0 commit comments

Comments
 (0)