Skip to content

Commit 9bb3dc0

Browse files
authored
feat: add passkey enrollment prompts (#1328)
* feat: add passkey enrollment prompts * update passkey handling in prompts and tests * fix: increase e2e test timeout and add passkeys prompt handling * Update docs * Update tests
1 parent df4420c commit 9bb3dc0

14 files changed

Lines changed: 7319 additions & 5443 deletions

docs/resource-specific-documentation.md

Lines changed: 59 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -10,32 +10,13 @@ The Deploy CLI's own client grant is intentionally not exported nor configurable
1010

1111
## Prompts
1212

13-
Multilingual custom text prompts follow a particular hierarchy. Under the root-level `prompts` resource property is a proprietary `customText` property that is used to bundle custom text translations with other prompts settings. Underneath `customText` is the two-character language code. Thirdly is the prompt ID, followed by the screen ID, followed by text ID.
13+
The prompts resource allows you to configure Universal Login pages, including custom text, custom HTML partials, and screen renderers.
1414

15-
RenderSettings of a prompt-screen follow a particular hierarchy. Under the root-level `prompts` we store `screenRenderers` property that is used to configure the rendering settings of a given prompt & screen. Thirdly is the prompt Name, followed by the screen Name mapped to the respective renderer configs file. Refer [more](https://auth0.com/docs/customize/login-pages/advanced-customizations/getting-started/configure-acul-screens) on this.
15+
**Custom Text**: Multilingual text translations follow a hierarchy - language code → prompt ID → screen ID → text ID.
1616

17-
Custom partials allow you to inject custom HTML (via Liquid templates) into specific insertion points of a prompt screen. Under `partials` is the prompt type, then the screen type, then the insertion point name mapped to the Liquid HTML content.
17+
**Partials**: Custom HTML that can be injected at specific insertion points in prompts.
1818

19-
**Hierarchy**
20-
21-
```yaml
22-
prompts:
23-
identifier_first: true|false
24-
universal_login_experience: new|classic
25-
webauthn_platform_first_factor: true|false
26-
customText:
27-
<LANGUAGE>: # two character language code
28-
<PROMPT_ID>: # prompt ID
29-
<SCREEN_ID>: # prompt screen ID
30-
<TEXT_ID>: 'Some text'
31-
partials:
32-
<PROMPT_TYPE>: # e.g. login, signup
33-
<SCREEN_TYPE>: # e.g. login, signup
34-
<INSERTION_POINT>: 'Liquid HTML content' # e.g. form-content-start, form-content-end
35-
screenRenderers:
36-
- <PROMPT-NAME>:
37-
<SCREEN-NAME>: ./prompts/screenRenderSettings/promptName_screenName.json #Add the renderer configs for a given prompt & a given screen
38-
```
19+
**Screen Renderers**: Configure rendering settings for specific prompt-screen combinations. Refer to the [Advanced Customizations documentation](https://auth0.com/docs/customize/login-pages/advanced-customizations/getting-started/configure-acul-screens) for more details.
3920

4021
**YAML Example**
4122

@@ -48,13 +29,9 @@ Folder structure when in YAML mode.
4829
/login-id_login-id.json
4930
/login-passwordless_login-passwordless-email-code.json
5031
/login-passwordless_login-passwordless-sms-otp.json
51-
/login-password_login-password.json
52-
/signup-password_signup-password.json
5332
./tenant.yaml
5433
```
5534

56-
In YAML mode, partial Liquid content is inlined directly in `tenant.yaml`:
57-
5835
```yaml
5936
# Contents of ./tenant.yaml
6037
prompts:
@@ -67,38 +44,36 @@ prompts:
6744
login:
6845
description: Login description in english
6946
buttonText: Button text
70-
mfa:
71-
mfa-detect-browser-capabilities:
72-
pickAuthenticatorText: 'Try another method'
73-
reloadButtonText: 'Reload'
74-
noJSErrorTitle: 'JavaScript Required'
75-
mfa-login-options:
76-
pageTitle: 'Log in to ${clientName}'
77-
authenticatorNamesSMS: 'SMS'
47+
passkeys:
48+
passkey-enrollment:
49+
title: Create a passkey for ${clientName}
50+
createButtonText: Create a passkey
7851
partials:
7952
login:
8053
login:
8154
form-content-start: |
82-
<div class="login-notice">
83-
Welcome back! Please log in to continue.
55+
<div class="custom-login-banner">
56+
<p>Welcome! Please log in to continue.</p>
8457
</div>
85-
signup:
86-
signup:
58+
passkeys:
59+
passkeys-enrollment:
8760
form-content-start: |
88-
<div class="signup-notice">
89-
Create your account to get started.
61+
<div class="passkey-enrollment-header">
62+
<p>Enhance your account security by creating a passkey.</p>
63+
</div>
64+
passkeys-enrollment-local:
65+
form-footer-end: |
66+
<div class="passkey-local-enrollment-info">
67+
<p>This passkey will be saved to this device only.</p>
9068
</div>
91-
form-content-end: |
92-
<p class="signup-terms">
93-
By signing up, you agree to our <a href="/terms">Terms of Service</a>.
94-
</p>
9569
screenRenderers:
9670
- signup-id:
9771
signup-id: ./prompts/screenRenderSettings/signup-id_signup-id.json
9872
- login-passwordless:
9973
login-passwordless-email-code: ./prompts/screenRenderSettings/login-passwordless_login-passwordless-email-code.json
10074
login-passwordless-sms-otp: ./prompts/screenRenderSettings/login-passwordless_login-passwordless-sms-otp.json
10175
```
76+
```
10277

10378
**Directory Example**
10479

@@ -110,10 +85,11 @@ Folder structure when in directory mode.
11085
/login
11186
/login
11287
/form-content-start.liquid
113-
/signup
114-
/signup
88+
/passkeys
89+
/passkeys-enrollment
11590
/form-content-start.liquid
116-
/form-content-end.liquid
91+
/passkeys-enrollment-local
92+
/form-footer-end.liquid
11793
/screenRenderSettings
11894
/signup-id_signup-id.json
11995
/login-id_login-id.json
@@ -125,9 +101,29 @@ Folder structure when in directory mode.
125101
/partials.json
126102
/prompts.json
127103
```
128-
129104
In directory mode, `partials.json` is a manifest that maps each insertion point to its `.liquid` file (paths are relative to the `prompts/` directory):
130105
106+
Contents of `custom-text.json`:
107+
108+
```json
109+
{
110+
"en": {
111+
"login": {
112+
"login": {
113+
"description": "Login description in english",
114+
"buttonText": "Button text"
115+
}
116+
},
117+
"passkeys": {
118+
"passkey-enrollment": {
119+
"title": "Create a passkey for ${clientName}",
120+
"createButtonText": "Create a passkey"
121+
}
122+
}
123+
}
124+
}
125+
```
126+
131127
Contents of `partials.json`:
132128

133129
```json
@@ -142,16 +138,18 @@ Contents of `partials.json`:
142138
]
143139
}
144140
],
145-
"signup": [
141+
"passkeys": [
146142
{
147-
"signup": [
143+
"passkeys-enrollment": [
148144
{
149145
"name": "form-content-start",
150-
"template": "partials/signup/signup/form-content-start.liquid"
151-
},
146+
"template": "partials/passkeys/passkeys-enrollment/form-content-start.liquid"
147+
}
148+
],
149+
"passkeys-enrollment-local": [
152150
{
153-
"name": "form-content-end",
154-
"template": "partials/signup/signup/form-content-end.liquid"
151+
"name": "form-footer-end",
152+
"template": "partials/passkeys/passkeys-enrollment-local/form-footer-end.liquid"
155153
}
156154
]
157155
}
@@ -167,15 +165,16 @@ Contents of `partials/login/login/form-content-start.liquid`:
167165
</div>
168166
```
169167

170-
Contents of `partials/signup/signup/form-content-end.liquid`:
168+
Contents of `partials/passkeys/passkeys-enrollment/form-content-start.liquid`:
171169

172170
```liquid
173-
<p class="signup-terms">
174-
By signing up, you agree to our <a href="/terms">Terms of Service</a>.
175-
</p>
171+
<div class="passkey-enrollment-header">
172+
<p>Enhance your account security by creating a passkey.</p>
173+
<p>Passkeys provide a faster and more secure way to sign in.</p>
174+
</div>
176175
```
177176

178-
Contents of `promptName_screenName.json`
177+
Contents of `screenRenderSettings/signup-id_signup-id.json`:
179178

180179
```json
181180
{

examples/directory/prompts/partials.json

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,12 @@
99
]
1010
}
1111
],
12-
"signup": [
12+
"passkeys": [
1313
{
14-
"signup": [
14+
"passkeys-enrollment": [
1515
{
1616
"name": "form-content-start",
17-
"template": "partials/signup/signup/form-content-start.liquid"
18-
},
19-
{
20-
"name": "form-content-end",
21-
"template": "partials/signup/signup/form-content-end.liquid"
17+
"template": "partials/passkeys/passkeys-enrollment/form-content-start.liquid"
2218
}
2319
]
2420
}
Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
<!-- Custom HTML injected at the start of the login form body -->
2-
<div class="login-notice">
3-
Welcome back! Please log in to continue.
1+
<div class="custom-login-banner">
2+
<p>Welcome! Please log in to continue.</p>
43
</div>
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<div class="passkey-enrollment-header">
2+
<p>Enhance your account security by creating a passkey.</p>
3+
</div>

examples/yaml/tenant.yaml

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -361,29 +361,37 @@ prompts:
361361
continueButtonText: Continue without a new passkey
362362
passkeyBenefit1Title: Sign in quickly with this device
363363
passkeyBenefit2Title: No need to remember a password
364+
enable_ulp_wcag_compliance: false
364365
identifier_first: false
365-
universal_login_experience: new
366-
webauthn_platform_first_factor: false
367366
partials:
368367
login:
369368
login:
370-
form-content-start: |
371-
<!-- Custom HTML injected at the start of the login form body -->
372-
<div class="login-notice">
373-
Welcome back! Please log in to continue.
369+
form-content-start: >-
370+
<div class="custom-login-banner">
371+
<p>Welcome! Please log in to continue.</p>
374372
</div>
375373
signup:
376374
signup:
377-
form-content-start: |
378-
<!-- Custom HTML injected at the start of the signup form body -->
379-
<div class="signup-notice">
380-
Create your account to get started.
375+
form-content-end: >-
376+
<div class="custom-signup-footer">
377+
<p>By signing up, you agree to our terms and conditions.</p>
378+
</div>
379+
passkeys:
380+
passkeys-enrollment:
381+
form-content-start: >-
382+
<div class="passkey-enrollment-header">
383+
<p>Enhance your account security by creating a passkey.</p>
384+
<p>Passkeys provide a faster and more secure way to sign in.</p>
385+
</div>
386+
secondary-actions-start: >-
387+
<div class="passkey-help-link">
388+
<a href="https://example.com/passkeys-help" target="_blank">Learn more about passkeys</a>
389+
</div>
390+
passkeys-enrollment-local:
391+
form-footer-end: >-
392+
<div class="passkey-local-enrollment-info">
393+
<p>This passkey will be saved to this device only.</p>
381394
</div>
382-
form-content-end: |
383-
<!-- Custom HTML injected at the end of the signup form body -->
384-
<p class="signup-terms">
385-
By signing up, you agree to our <a href="/terms">Terms of Service</a>.
386-
</p>
387395
screenRenderers:
388396
- signup-id:
389397
signup-id: ./prompts/screenRenderSettings/signup-id_signup-id.json

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
"lint": "eslint . && kacl lint",
1212
"format": "npx prettier --write .",
1313
"test": "ts-mocha -p tsconfig.json --recursive 'test/**/*.test*' --exclude 'test/e2e/*' --timeout 20000",
14-
"test:e2e:node-module": "ts-mocha -p tsconfig.json --recursive 'test/e2e/*.test*' --timeout 120000",
14+
"test:e2e:node-module": "ts-mocha -p tsconfig.json --recursive 'test/e2e/*.test*' --timeout 150000",
1515
"test:e2e:cli": "sh ./test/e2e/e2e-cli.sh",
1616
"test:coverage": "nyc npm run test && nyc report --reporter=lcov",
1717
"build": "rimraf ./lib && npx tsc",

src/tools/auth0/handlers/prompts.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,8 +123,12 @@ const customPartialsPromptTypes = [
123123
'signup',
124124
'signup-id',
125125
'signup-password',
126+
'passkeys',
126127
];
127128

129+
// Prompts that may not be available on all tenants (early access features)
130+
const optionalPartialsPromptTypes = ['passkeys'];
131+
128132
export type CustomPartialsPromptTypes = (typeof customPartialsPromptTypes)[number];
129133

130134
const customPartialsScreenTypes = [
@@ -136,6 +140,8 @@ const customPartialsScreenTypes = [
136140
'signup-password',
137141
'login-passwordless-sms-otp',
138142
'login-passwordless-email-code',
143+
'passkeys-enrollment',
144+
'passkeys-enrollment-local',
139145
] as const;
140146

141147
export type CustomPartialsScreenTypes = (typeof customPartialsPromptTypes)[number];
@@ -430,6 +436,27 @@ export default class PromptsHandler extends DefaultHandler {
430436
return null;
431437
}
432438

439+
// Handle 400 errors for prompt types not available on all tenants (early access features)
440+
// Error format: "Path validation error: 'Invalid value \"passkeys\"' on property prompt (Name of the prompt)."
441+
if (
442+
error &&
443+
error?.statusCode === 400 &&
444+
error.message?.includes('Path validation error') &&
445+
error.message?.includes('on property prompt')
446+
) {
447+
// Check if the error message contains any of the optional prompt types
448+
const unavailablePrompt = optionalPartialsPromptTypes.find((promptType) =>
449+
error.message?.includes(promptType)
450+
);
451+
452+
if (unavailablePrompt) {
453+
log.warn(
454+
`Skipping partials for prompt type '${unavailablePrompt}' because it is not available on this tenant.`
455+
);
456+
return null;
457+
}
458+
}
459+
433460
if (error && error.statusCode === 429) {
434461
log.error(
435462
`The global rate limit has been exceeded, resulting in a ${error.statusCode} error. ${error.message}. Although this is an error, it is not blocking the pipeline.`

0 commit comments

Comments
 (0)