@keycloakify/login-ui 250004.4.1 → 250004.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/core/KcContext/KcContext.d.ts +20 -0
- package/core/KcContext/kcContextMocks.js +6 -2
- package/keycloak-theme/login/pages/login/Form.tsx +183 -132
- package/keycloak-theme/login/pages/login/useScript.tsx +64 -0
- package/keycloak-theme/login/pages/login-passkeys-conditional-authenticate/Page.tsx +3 -3
- package/keycloak-theme/login/pages/login-passkeys-conditional-authenticate/useScript.tsx +4 -4
- package/keycloak-theme/login/pages/login-password/Page.tsx +45 -0
- package/keycloak-theme/login/pages/login-password/useScript.tsx +64 -0
- package/keycloak-theme/login/pages/login-username/Page.tsx +12 -12
- package/keycloak-theme/login/pages/login-username/useScript.tsx +4 -4
- package/keycloak-theme/login/pages/webauthn-authenticate/Page.tsx +3 -3
- package/keycloak-theme/login/pages/webauthn-authenticate/useScript.tsx +4 -4
- package/keycloak-theme/login/pages/webauthn-register/Page.tsx +3 -3
- package/keycloak-theme/login/pages/webauthn-register/useScript.tsx +4 -4
- package/package.json +1 -1
- package/src/core/KcContext/KcContext.ts +20 -0
- package/src/core/KcContext/kcContextMocks.ts +22 -2
|
@@ -145,6 +145,16 @@ export declare namespace KcContext {
|
|
|
145
145
|
iconClasses?: string;
|
|
146
146
|
}[];
|
|
147
147
|
};
|
|
148
|
+
enableWebAuthnConditionalUI?: boolean;
|
|
149
|
+
authenticators?: {
|
|
150
|
+
authenticators: WebauthnAuthenticate.WebauthnAuthenticator[];
|
|
151
|
+
};
|
|
152
|
+
challenge: string;
|
|
153
|
+
userVerification: WebauthnAuthenticate["userVerification"];
|
|
154
|
+
rpId: string;
|
|
155
|
+
createTimeout: number | string;
|
|
156
|
+
isUserIdentified: "true" | "false";
|
|
157
|
+
shouldDisplayAuthenticators?: boolean;
|
|
148
158
|
};
|
|
149
159
|
type Register = Common & {
|
|
150
160
|
pageId: "register.ftl";
|
|
@@ -290,6 +300,16 @@ export declare namespace KcContext {
|
|
|
290
300
|
showTryAnotherWayLink?: boolean;
|
|
291
301
|
attemptedUsername?: string;
|
|
292
302
|
};
|
|
303
|
+
enableWebAuthnConditionalUI?: boolean;
|
|
304
|
+
authenticators?: {
|
|
305
|
+
authenticators: WebauthnAuthenticate.WebauthnAuthenticator[];
|
|
306
|
+
};
|
|
307
|
+
challenge: string;
|
|
308
|
+
userVerification: WebauthnAuthenticate["userVerification"];
|
|
309
|
+
rpId: string;
|
|
310
|
+
createTimeout: number | string;
|
|
311
|
+
isUserIdentified: "true" | "false";
|
|
312
|
+
shouldDisplayAuthenticators?: boolean;
|
|
293
313
|
};
|
|
294
314
|
type WebauthnAuthenticate = Common & {
|
|
295
315
|
pageId: "webauthn-authenticate.ftl";
|
|
@@ -168,7 +168,9 @@ const loginUrl = Object.assign(Object.assign({}, kcContextCommonMock.url), { log
|
|
|
168
168
|
export const kcContextMocks = [
|
|
169
169
|
id(Object.assign(Object.assign({}, kcContextCommonMock), { pageId: "login.ftl", url: loginUrl, realm: Object.assign(Object.assign({}, kcContextCommonMock.realm), { loginWithEmailAllowed: true, rememberMe: true, password: true, resetPasswordAllowed: true, registrationAllowed: true }), auth: kcContextCommonMock.auth, social: {
|
|
170
170
|
displayInfo: true
|
|
171
|
-
}, usernameHidden: false, login: {}, registrationDisabled: false
|
|
171
|
+
}, usernameHidden: false, login: {}, registrationDisabled: false, enableWebAuthnConditionalUI: false, authenticators: {
|
|
172
|
+
authenticators: []
|
|
173
|
+
}, challenge: "", userVerification: "not specified", rpId: "", createTimeout: "0", isUserIdentified: "false", shouldDisplayAuthenticators: false })),
|
|
172
174
|
id(Object.assign(Object.assign({}, kcContextCommonMock), { url: Object.assign(Object.assign({}, loginUrl), { registrationAction: "#" }), isAppInitiatedAction: false, passwordRequired: true, recaptchaRequired: false, pageId: "register.ftl", profile: {
|
|
173
175
|
attributesByName
|
|
174
176
|
}, scripts: [
|
|
@@ -220,7 +222,9 @@ export const kcContextMocks = [
|
|
|
220
222
|
id(Object.assign(Object.assign({}, kcContextCommonMock), { pageId: "login-username.ftl", url: loginUrl, realm: Object.assign(Object.assign({}, kcContextCommonMock.realm), { loginWithEmailAllowed: true, rememberMe: true, password: true, resetPasswordAllowed: true, registrationAllowed: true }), social: {
|
|
221
223
|
displayInfo: true
|
|
222
224
|
}, usernameHidden: false, login: {}, registrationDisabled: false, challenge: "", userVerification: "not specified", rpId: "", createTimeout: "0", isUserIdentified: "false" })),
|
|
223
|
-
id(Object.assign(Object.assign({}, kcContextCommonMock), { pageId: "login-password.ftl", url: loginUrl, realm: Object.assign(Object.assign({}, kcContextCommonMock.realm), { resetPasswordAllowed: true })
|
|
225
|
+
id(Object.assign(Object.assign({}, kcContextCommonMock), { pageId: "login-password.ftl", url: loginUrl, realm: Object.assign(Object.assign({}, kcContextCommonMock.realm), { resetPasswordAllowed: true }), enableWebAuthnConditionalUI: false, authenticators: {
|
|
226
|
+
authenticators: []
|
|
227
|
+
}, challenge: "", userVerification: "not specified", rpId: "", createTimeout: "0", isUserIdentified: "false", shouldDisplayAuthenticators: false })),
|
|
224
228
|
id(Object.assign(Object.assign({}, kcContextCommonMock), { pageId: "webauthn-authenticate.ftl", url: loginUrl, authenticators: {
|
|
225
229
|
authenticators: []
|
|
226
230
|
}, realm: Object.assign(Object.assign({}, kcContextCommonMock.realm), { password: true, registrationAllowed: true }), challenge: "", userVerification: "not specified", rpId: "", createTimeout: "0", isUserIdentified: "false", shouldDisplayAuthenticators: false })),
|
|
@@ -5,6 +5,7 @@ import { useI18n } from "../../i18n";
|
|
|
5
5
|
import { useKcContext } from "../../KcContext";
|
|
6
6
|
import { useKcClsx } from "@keycloakify/login-ui/useKcClsx";
|
|
7
7
|
import { kcSanitize } from "@keycloakify/login-ui/kcSanitize";
|
|
8
|
+
import { useScript } from "./useScript";
|
|
8
9
|
|
|
9
10
|
export function Form() {
|
|
10
11
|
const { kcContext } = useKcContext();
|
|
@@ -16,149 +17,199 @@ export function Form() {
|
|
|
16
17
|
|
|
17
18
|
const { kcClsx } = useKcClsx();
|
|
18
19
|
|
|
20
|
+
const webAuthnButtonId = "authenticateWebAuthnButton";
|
|
21
|
+
|
|
22
|
+
useScript({ webAuthnButtonId });
|
|
23
|
+
|
|
19
24
|
return (
|
|
20
|
-
|
|
21
|
-
<div id="kc-form
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
25
|
+
<>
|
|
26
|
+
<div id="kc-form">
|
|
27
|
+
<div id="kc-form-wrapper">
|
|
28
|
+
{kcContext.realm.password && (
|
|
29
|
+
<form
|
|
30
|
+
id="kc-form-login"
|
|
31
|
+
onSubmit={() => {
|
|
32
|
+
setIsLoginButtonDisabled(true);
|
|
33
|
+
return true;
|
|
34
|
+
}}
|
|
35
|
+
action={kcContext.url.loginAction}
|
|
36
|
+
method="post"
|
|
37
|
+
>
|
|
38
|
+
{!kcContext.usernameHidden && (
|
|
39
|
+
<div className={kcClsx("kcFormGroupClass")}>
|
|
40
|
+
<label htmlFor="username" className={kcClsx("kcLabelClass")}>
|
|
41
|
+
{!kcContext.realm.loginWithEmailAllowed
|
|
42
|
+
? msg("username")
|
|
43
|
+
: !kcContext.realm.registrationEmailAsUsername
|
|
44
|
+
? msg("usernameOrEmail")
|
|
45
|
+
: msg("email")}
|
|
46
|
+
</label>
|
|
47
|
+
<input
|
|
48
|
+
tabIndex={2}
|
|
49
|
+
id="username"
|
|
50
|
+
className={kcClsx("kcInputClass")}
|
|
51
|
+
name="username"
|
|
52
|
+
defaultValue={kcContext.login.username ?? ""}
|
|
53
|
+
type="text"
|
|
54
|
+
autoFocus
|
|
55
|
+
autoComplete="username"
|
|
56
|
+
aria-invalid={kcContext.messagesPerField.existsError(
|
|
57
|
+
"username",
|
|
58
|
+
"password"
|
|
59
|
+
)}
|
|
60
|
+
/>
|
|
61
|
+
{kcContext.messagesPerField.existsError("username", "password") && (
|
|
62
|
+
<span
|
|
63
|
+
id="input-error"
|
|
64
|
+
className={kcClsx("kcInputErrorMessageClass")}
|
|
65
|
+
aria-live="polite"
|
|
66
|
+
dangerouslySetInnerHTML={{
|
|
67
|
+
__html: kcSanitize(
|
|
68
|
+
kcContext.messagesPerField.getFirstError(
|
|
69
|
+
"username",
|
|
70
|
+
"password"
|
|
71
|
+
)
|
|
72
|
+
)
|
|
73
|
+
}}
|
|
74
|
+
/>
|
|
75
|
+
)}
|
|
76
|
+
</div>
|
|
77
|
+
)}
|
|
78
|
+
|
|
33
79
|
<div className={kcClsx("kcFormGroupClass")}>
|
|
34
|
-
<label htmlFor="
|
|
35
|
-
{
|
|
36
|
-
? msg("username")
|
|
37
|
-
: !kcContext.realm.registrationEmailAsUsername
|
|
38
|
-
? msg("usernameOrEmail")
|
|
39
|
-
: msg("email")}
|
|
80
|
+
<label htmlFor="password" className={kcClsx("kcLabelClass")}>
|
|
81
|
+
{msg("password")}
|
|
40
82
|
</label>
|
|
41
|
-
<
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
)}
|
|
54
|
-
/>
|
|
55
|
-
{kcContext.messagesPerField.existsError("username", "password") && (
|
|
56
|
-
<span
|
|
57
|
-
id="input-error"
|
|
58
|
-
className={kcClsx("kcInputErrorMessageClass")}
|
|
59
|
-
aria-live="polite"
|
|
60
|
-
dangerouslySetInnerHTML={{
|
|
61
|
-
__html: kcSanitize(
|
|
62
|
-
kcContext.messagesPerField.getFirstError(
|
|
63
|
-
"username",
|
|
64
|
-
"password"
|
|
65
|
-
)
|
|
66
|
-
)
|
|
67
|
-
}}
|
|
83
|
+
<PasswordWrapper passwordInputId="password">
|
|
84
|
+
<input
|
|
85
|
+
tabIndex={3}
|
|
86
|
+
id="password"
|
|
87
|
+
className={kcClsx("kcInputClass")}
|
|
88
|
+
name="password"
|
|
89
|
+
type="password"
|
|
90
|
+
autoComplete="current-password"
|
|
91
|
+
aria-invalid={kcContext.messagesPerField.existsError(
|
|
92
|
+
"username",
|
|
93
|
+
"password"
|
|
94
|
+
)}
|
|
68
95
|
/>
|
|
69
|
-
|
|
96
|
+
</PasswordWrapper>
|
|
97
|
+
{kcContext.usernameHidden &&
|
|
98
|
+
kcContext.messagesPerField.existsError("username", "password") && (
|
|
99
|
+
<span
|
|
100
|
+
id="input-error"
|
|
101
|
+
className={kcClsx("kcInputErrorMessageClass")}
|
|
102
|
+
aria-live="polite"
|
|
103
|
+
dangerouslySetInnerHTML={{
|
|
104
|
+
__html: kcSanitize(
|
|
105
|
+
kcContext.messagesPerField.getFirstError(
|
|
106
|
+
"username",
|
|
107
|
+
"password"
|
|
108
|
+
)
|
|
109
|
+
)
|
|
110
|
+
}}
|
|
111
|
+
/>
|
|
112
|
+
)}
|
|
70
113
|
</div>
|
|
71
|
-
)}
|
|
72
114
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
115
|
+
<div className={kcClsx("kcFormGroupClass", "kcFormSettingClass")}>
|
|
116
|
+
<div id="kc-form-options">
|
|
117
|
+
{kcContext.realm.rememberMe && !kcContext.usernameHidden && (
|
|
118
|
+
<div className="checkbox">
|
|
119
|
+
<label>
|
|
120
|
+
<input
|
|
121
|
+
tabIndex={5}
|
|
122
|
+
id="rememberMe"
|
|
123
|
+
name="rememberMe"
|
|
124
|
+
type="checkbox"
|
|
125
|
+
defaultChecked={!!kcContext.login.rememberMe}
|
|
126
|
+
/>{" "}
|
|
127
|
+
{msg("rememberMe")}
|
|
128
|
+
</label>
|
|
129
|
+
</div>
|
|
130
|
+
)}
|
|
131
|
+
</div>
|
|
132
|
+
<div className={kcClsx("kcFormOptionsWrapperClass")}>
|
|
133
|
+
{kcContext.realm.resetPasswordAllowed && (
|
|
134
|
+
<span>
|
|
135
|
+
<a
|
|
136
|
+
tabIndex={6}
|
|
137
|
+
href={kcContext.url.loginResetCredentialsUrl}
|
|
138
|
+
>
|
|
139
|
+
{msg("doForgotPassword")}
|
|
140
|
+
</a>
|
|
141
|
+
</span>
|
|
142
|
+
)}
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
|
|
146
|
+
<div id="kc-form-buttons" className={kcClsx("kcFormGroupClass")}>
|
|
78
147
|
<input
|
|
79
|
-
|
|
80
|
-
id="
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
148
|
+
type="hidden"
|
|
149
|
+
id="id-hidden-input"
|
|
150
|
+
name="credentialId"
|
|
151
|
+
value={kcContext.auth.selectedCredential}
|
|
152
|
+
/>
|
|
153
|
+
<input
|
|
154
|
+
tabIndex={7}
|
|
155
|
+
disabled={isLoginButtonDisabled}
|
|
156
|
+
className={kcClsx(
|
|
157
|
+
"kcButtonClass",
|
|
158
|
+
"kcButtonPrimaryClass",
|
|
159
|
+
"kcButtonBlockClass",
|
|
160
|
+
"kcButtonLargeClass"
|
|
88
161
|
)}
|
|
162
|
+
name="login"
|
|
163
|
+
id="kc-login"
|
|
164
|
+
type="submit"
|
|
165
|
+
value={msgStr("doLogIn")}
|
|
89
166
|
/>
|
|
90
|
-
</PasswordWrapper>
|
|
91
|
-
{kcContext.usernameHidden &&
|
|
92
|
-
kcContext.messagesPerField.existsError("username", "password") && (
|
|
93
|
-
<span
|
|
94
|
-
id="input-error"
|
|
95
|
-
className={kcClsx("kcInputErrorMessageClass")}
|
|
96
|
-
aria-live="polite"
|
|
97
|
-
dangerouslySetInnerHTML={{
|
|
98
|
-
__html: kcSanitize(
|
|
99
|
-
kcContext.messagesPerField.getFirstError(
|
|
100
|
-
"username",
|
|
101
|
-
"password"
|
|
102
|
-
)
|
|
103
|
-
)
|
|
104
|
-
}}
|
|
105
|
-
/>
|
|
106
|
-
)}
|
|
107
|
-
</div>
|
|
108
|
-
|
|
109
|
-
<div className={kcClsx("kcFormGroupClass", "kcFormSettingClass")}>
|
|
110
|
-
<div id="kc-form-options">
|
|
111
|
-
{kcContext.realm.rememberMe && !kcContext.usernameHidden && (
|
|
112
|
-
<div className="checkbox">
|
|
113
|
-
<label>
|
|
114
|
-
<input
|
|
115
|
-
tabIndex={5}
|
|
116
|
-
id="rememberMe"
|
|
117
|
-
name="rememberMe"
|
|
118
|
-
type="checkbox"
|
|
119
|
-
defaultChecked={!!kcContext.login.rememberMe}
|
|
120
|
-
/>{" "}
|
|
121
|
-
{msg("rememberMe")}
|
|
122
|
-
</label>
|
|
123
|
-
</div>
|
|
124
|
-
)}
|
|
125
|
-
</div>
|
|
126
|
-
<div className={kcClsx("kcFormOptionsWrapperClass")}>
|
|
127
|
-
{kcContext.realm.resetPasswordAllowed && (
|
|
128
|
-
<span>
|
|
129
|
-
<a tabIndex={6} href={kcContext.url.loginResetCredentialsUrl}>
|
|
130
|
-
{msg("doForgotPassword")}
|
|
131
|
-
</a>
|
|
132
|
-
</span>
|
|
133
|
-
)}
|
|
134
167
|
</div>
|
|
135
|
-
</
|
|
168
|
+
</form>
|
|
169
|
+
)}
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
136
172
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
disabled={isLoginButtonDisabled}
|
|
147
|
-
className={kcClsx(
|
|
148
|
-
"kcButtonClass",
|
|
149
|
-
"kcButtonPrimaryClass",
|
|
150
|
-
"kcButtonBlockClass",
|
|
151
|
-
"kcButtonLargeClass"
|
|
152
|
-
)}
|
|
153
|
-
name="login"
|
|
154
|
-
id="kc-login"
|
|
155
|
-
type="submit"
|
|
156
|
-
value={msgStr("doLogIn")}
|
|
157
|
-
/>
|
|
158
|
-
</div>
|
|
173
|
+
{kcContext.enableWebAuthnConditionalUI && (
|
|
174
|
+
<>
|
|
175
|
+
<form id="webauth" action={kcContext.url.loginAction} method="post">
|
|
176
|
+
<input type="hidden" id="clientDataJSON" name="clientDataJSON" />
|
|
177
|
+
<input type="hidden" id="authenticatorData" name="authenticatorData" />
|
|
178
|
+
<input type="hidden" id="signature" name="signature" />
|
|
179
|
+
<input type="hidden" id="credentialId" name="credentialId" />
|
|
180
|
+
<input type="hidden" id="userHandle" name="userHandle" />
|
|
181
|
+
<input type="hidden" id="error" name="error" />
|
|
159
182
|
</form>
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
183
|
+
{kcContext.authenticators !== undefined &&
|
|
184
|
+
kcContext.authenticators.authenticators.length !== 0 && (
|
|
185
|
+
<>
|
|
186
|
+
<form id="authn_select" className={kcClsx("kcFormClass")}>
|
|
187
|
+
{kcContext.authenticators.authenticators.map((authenticator, i) => (
|
|
188
|
+
<input
|
|
189
|
+
key={i}
|
|
190
|
+
type="hidden"
|
|
191
|
+
name="authn_use_chk"
|
|
192
|
+
readOnly
|
|
193
|
+
value={authenticator.credentialId}
|
|
194
|
+
/>
|
|
195
|
+
))}
|
|
196
|
+
</form>
|
|
197
|
+
</>
|
|
198
|
+
)}
|
|
199
|
+
<br /> {/* We use a br here because kcMarginTopClass is not defined in login v1 */}
|
|
200
|
+
<input
|
|
201
|
+
id={webAuthnButtonId}
|
|
202
|
+
type="button"
|
|
203
|
+
className={kcClsx(
|
|
204
|
+
"kcButtonClass",
|
|
205
|
+
"kcButtonDefaultClass",
|
|
206
|
+
"kcButtonBlockClass",
|
|
207
|
+
"kcButtonLargeClass"
|
|
208
|
+
)}
|
|
209
|
+
value={msgStr("passkey-doAuthenticate")}
|
|
210
|
+
/>
|
|
211
|
+
</>
|
|
212
|
+
)}
|
|
213
|
+
</>
|
|
163
214
|
);
|
|
164
215
|
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
import { assert } from "tsafe/assert";
|
|
3
|
+
import { useInsertScriptTags } from "@keycloakify/login-ui/tools/useInsertScriptTags";
|
|
4
|
+
import { waitForElementMountedOnDom } from "@keycloakify/login-ui/tools/waitForElementMountedOnDom";
|
|
5
|
+
// NOTE: If you are in a Vite project you can use `import.meta.env.BASE_URL` directly, this is a shim to support Webpack.
|
|
6
|
+
import { BASE_URL } from "../../../kc.gen";
|
|
7
|
+
import { useI18n } from "../../i18n";
|
|
8
|
+
import { useKcContext } from "../../KcContext";
|
|
9
|
+
|
|
10
|
+
export function useScript(params: { webAuthnButtonId: string }) {
|
|
11
|
+
const { webAuthnButtonId } = params;
|
|
12
|
+
|
|
13
|
+
const { kcContext } = useKcContext();
|
|
14
|
+
assert(kcContext.pageId === "login.ftl");
|
|
15
|
+
|
|
16
|
+
const { msgStr, isFetchingTranslations } = useI18n();
|
|
17
|
+
|
|
18
|
+
const { insertScriptTags } = useInsertScriptTags({
|
|
19
|
+
effectId: "Login",
|
|
20
|
+
scriptTags: [
|
|
21
|
+
{
|
|
22
|
+
type: "module",
|
|
23
|
+
textContent: () => `
|
|
24
|
+
import { authenticateByWebAuthn } from "${BASE_URL}keycloak-theme/login/js/webauthnAuthenticate.js";
|
|
25
|
+
import { initAuthenticate } from "${BASE_URL}keycloak-theme/login/js/passkeysConditionalAuth.js";
|
|
26
|
+
|
|
27
|
+
const authButton = document.getElementById("${webAuthnButtonId}");
|
|
28
|
+
const input = {
|
|
29
|
+
isUserIdentified : ${kcContext.isUserIdentified},
|
|
30
|
+
challenge : ${JSON.stringify(kcContext.challenge)},
|
|
31
|
+
userVerification : ${JSON.stringify(kcContext.userVerification)},
|
|
32
|
+
rpId : ${JSON.stringify(kcContext.rpId)},
|
|
33
|
+
createTimeout : ${kcContext.createTimeout}
|
|
34
|
+
};
|
|
35
|
+
authButton.addEventListener("click", () => {
|
|
36
|
+
authenticateByWebAuthn({
|
|
37
|
+
...input,
|
|
38
|
+
errmsg : ${JSON.stringify(msgStr("webauthn-unsupported-browser-text"))}
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
initAuthenticate({
|
|
43
|
+
...input,
|
|
44
|
+
errmsg : ${JSON.stringify(msgStr("passkey-unsupported-browser-text"))}
|
|
45
|
+
});
|
|
46
|
+
`
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
if (isFetchingTranslations) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
(async () => {
|
|
57
|
+
await waitForElementMountedOnDom({
|
|
58
|
+
elementId: webAuthnButtonId
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
insertScriptTags();
|
|
62
|
+
})();
|
|
63
|
+
}, [isFetchingTranslations]);
|
|
64
|
+
}
|
|
@@ -26,9 +26,9 @@ export function Page() {
|
|
|
26
26
|
|
|
27
27
|
const { kcClsx } = useKcClsx();
|
|
28
28
|
|
|
29
|
-
const
|
|
29
|
+
const webAuthnButtonId = "authenticateWebAuthnButton";
|
|
30
30
|
|
|
31
|
-
useScript({
|
|
31
|
+
useScript({ webAuthnButtonId });
|
|
32
32
|
|
|
33
33
|
return (
|
|
34
34
|
<Template
|
|
@@ -213,7 +213,7 @@ export function Page() {
|
|
|
213
213
|
style={{ display: "none" }}
|
|
214
214
|
>
|
|
215
215
|
<input
|
|
216
|
-
id={
|
|
216
|
+
id={webAuthnButtonId}
|
|
217
217
|
type="button"
|
|
218
218
|
autoFocus
|
|
219
219
|
value={msgStr("passkey-doAuthenticate")}
|
|
@@ -7,8 +7,8 @@ import { BASE_URL } from "../../../kc.gen";
|
|
|
7
7
|
import { useI18n } from "../../i18n";
|
|
8
8
|
import { useKcContext } from "../../KcContext";
|
|
9
9
|
|
|
10
|
-
export function useScript(params: {
|
|
11
|
-
const {
|
|
10
|
+
export function useScript(params: { webAuthnButtonId: string }) {
|
|
11
|
+
const { webAuthnButtonId } = params;
|
|
12
12
|
|
|
13
13
|
const { kcContext } = useKcContext();
|
|
14
14
|
assert(kcContext.pageId === "login-passkeys-conditional-authenticate.ftl");
|
|
@@ -24,7 +24,7 @@ export function useScript(params: { authButtonId: string }) {
|
|
|
24
24
|
import { authenticateByWebAuthn } from "${BASE_URL}keycloak-theme/login/js/webauthnAuthenticate.js";
|
|
25
25
|
import { initAuthenticate } from "${BASE_URL}keycloak-theme/login/js/passkeysConditionalAuth.js";
|
|
26
26
|
|
|
27
|
-
const authButton = document.getElementById("${
|
|
27
|
+
const authButton = document.getElementById("${webAuthnButtonId}");
|
|
28
28
|
const input = {
|
|
29
29
|
isUserIdentified : ${kcContext.isUserIdentified},
|
|
30
30
|
challenge : ${JSON.stringify(kcContext.challenge)},
|
|
@@ -55,7 +55,7 @@ export function useScript(params: { authButtonId: string }) {
|
|
|
55
55
|
|
|
56
56
|
(async () => {
|
|
57
57
|
await waitForElementMountedOnDom({
|
|
58
|
-
elementId:
|
|
58
|
+
elementId: webAuthnButtonId
|
|
59
59
|
});
|
|
60
60
|
|
|
61
61
|
insertScriptTags();
|
|
@@ -7,6 +7,7 @@ import { useKcClsx } from "@keycloakify/login-ui/useKcClsx";
|
|
|
7
7
|
import { useKcContext } from "../../KcContext";
|
|
8
8
|
import { useI18n } from "../../i18n";
|
|
9
9
|
import { Template } from "../../components/Template";
|
|
10
|
+
import { useScript } from "./useScript";
|
|
10
11
|
|
|
11
12
|
export function Page() {
|
|
12
13
|
const { kcContext } = useKcContext();
|
|
@@ -18,6 +19,10 @@ export function Page() {
|
|
|
18
19
|
|
|
19
20
|
const [isLoginButtonDisabled, setIsLoginButtonDisabled] = useState(false);
|
|
20
21
|
|
|
22
|
+
const webAuthnButtonId = "authenticateWebAuthnButton";
|
|
23
|
+
|
|
24
|
+
useScript({ webAuthnButtonId });
|
|
25
|
+
|
|
21
26
|
return (
|
|
22
27
|
<Template
|
|
23
28
|
headerNode={msg("doLogIn")}
|
|
@@ -98,6 +103,46 @@ export function Page() {
|
|
|
98
103
|
</form>
|
|
99
104
|
</div>
|
|
100
105
|
</div>
|
|
106
|
+
{kcContext.enableWebAuthnConditionalUI && (
|
|
107
|
+
<>
|
|
108
|
+
<form id="webauth" action={kcContext.url.loginAction} method="post">
|
|
109
|
+
<input type="hidden" id="clientDataJSON" name="clientDataJSON" />
|
|
110
|
+
<input type="hidden" id="authenticatorData" name="authenticatorData" />
|
|
111
|
+
<input type="hidden" id="signature" name="signature" />
|
|
112
|
+
<input type="hidden" id="credentialId" name="credentialId" />
|
|
113
|
+
<input type="hidden" id="userHandle" name="userHandle" />
|
|
114
|
+
<input type="hidden" id="error" name="error" />
|
|
115
|
+
</form>
|
|
116
|
+
{kcContext.authenticators !== undefined &&
|
|
117
|
+
kcContext.authenticators.authenticators.length !== 0 && (
|
|
118
|
+
<>
|
|
119
|
+
<form id="authn_select" className={kcClsx("kcFormClass")}>
|
|
120
|
+
{kcContext.authenticators.authenticators.map((authenticator, i) => (
|
|
121
|
+
<input
|
|
122
|
+
key={i}
|
|
123
|
+
type="hidden"
|
|
124
|
+
name="authn_use_chk"
|
|
125
|
+
readOnly
|
|
126
|
+
value={authenticator.credentialId}
|
|
127
|
+
/>
|
|
128
|
+
))}
|
|
129
|
+
</form>
|
|
130
|
+
</>
|
|
131
|
+
)}
|
|
132
|
+
<br /> {/* We use a br here because kcMarginTopClass is not defined in login v1 */}
|
|
133
|
+
<input
|
|
134
|
+
id={webAuthnButtonId}
|
|
135
|
+
type="button"
|
|
136
|
+
className={kcClsx(
|
|
137
|
+
"kcButtonClass",
|
|
138
|
+
"kcButtonDefaultClass",
|
|
139
|
+
"kcButtonBlockClass",
|
|
140
|
+
"kcButtonLargeClass"
|
|
141
|
+
)}
|
|
142
|
+
value={msgStr("passkey-doAuthenticate")}
|
|
143
|
+
/>
|
|
144
|
+
</>
|
|
145
|
+
)}
|
|
101
146
|
</Template>
|
|
102
147
|
);
|
|
103
148
|
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
import { assert } from "tsafe/assert";
|
|
3
|
+
import { useInsertScriptTags } from "@keycloakify/login-ui/tools/useInsertScriptTags";
|
|
4
|
+
import { waitForElementMountedOnDom } from "@keycloakify/login-ui/tools/waitForElementMountedOnDom";
|
|
5
|
+
// NOTE: If you are in a Vite project you can use `import.meta.env.BASE_URL` directly, this is a shim to support Webpack.
|
|
6
|
+
import { BASE_URL } from "../../../kc.gen";
|
|
7
|
+
import { useI18n } from "../../i18n";
|
|
8
|
+
import { useKcContext } from "../../KcContext";
|
|
9
|
+
|
|
10
|
+
export function useScript(params: { webAuthnButtonId: string }) {
|
|
11
|
+
const { webAuthnButtonId } = params;
|
|
12
|
+
|
|
13
|
+
const { kcContext } = useKcContext();
|
|
14
|
+
assert(kcContext.pageId === "login-password.ftl");
|
|
15
|
+
|
|
16
|
+
const { msgStr, isFetchingTranslations } = useI18n();
|
|
17
|
+
|
|
18
|
+
const { insertScriptTags } = useInsertScriptTags({
|
|
19
|
+
effectId: "LoginPassword",
|
|
20
|
+
scriptTags: [
|
|
21
|
+
{
|
|
22
|
+
type: "module",
|
|
23
|
+
textContent: () => `
|
|
24
|
+
import { authenticateByWebAuthn } from "${BASE_URL}keycloak-theme/login/js/webauthnAuthenticate.js";
|
|
25
|
+
import { initAuthenticate } from "${BASE_URL}keycloak-theme/login/js/passkeysConditionalAuth.js";
|
|
26
|
+
|
|
27
|
+
const authButton = document.getElementById("${webAuthnButtonId}");
|
|
28
|
+
const input = {
|
|
29
|
+
isUserIdentified : ${kcContext.isUserIdentified},
|
|
30
|
+
challenge : ${JSON.stringify(kcContext.challenge)},
|
|
31
|
+
userVerification : ${JSON.stringify(kcContext.userVerification)},
|
|
32
|
+
rpId : ${JSON.stringify(kcContext.rpId)},
|
|
33
|
+
createTimeout : ${kcContext.createTimeout}
|
|
34
|
+
};
|
|
35
|
+
authButton.addEventListener("click", () => {
|
|
36
|
+
authenticateByWebAuthn({
|
|
37
|
+
...input,
|
|
38
|
+
errmsg : ${JSON.stringify(msgStr("webauthn-unsupported-browser-text"))}
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
initAuthenticate({
|
|
43
|
+
...input,
|
|
44
|
+
errmsg : ${JSON.stringify(msgStr("passkey-unsupported-browser-text"))}
|
|
45
|
+
});
|
|
46
|
+
`
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
if (isFetchingTranslations) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
(async () => {
|
|
57
|
+
await waitForElementMountedOnDom({
|
|
58
|
+
elementId: webAuthnButtonId
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
insertScriptTags();
|
|
62
|
+
})();
|
|
63
|
+
}, [isFetchingTranslations]);
|
|
64
|
+
}
|
|
@@ -29,9 +29,9 @@ export function Page() {
|
|
|
29
29
|
|
|
30
30
|
const [isLoginButtonDisabled, setIsLoginButtonDisabled] = useState(false);
|
|
31
31
|
|
|
32
|
-
const
|
|
32
|
+
const webAuthnButtonId = "authenticateWebAuthnButton";
|
|
33
33
|
|
|
34
|
-
useScript({
|
|
34
|
+
useScript({ webAuthnButtonId });
|
|
35
35
|
|
|
36
36
|
return (
|
|
37
37
|
<Template
|
|
@@ -195,7 +195,7 @@ export function Page() {
|
|
|
195
195
|
<input type="hidden" id="userHandle" name="userHandle" />
|
|
196
196
|
<input type="hidden" id="error" name="error" />
|
|
197
197
|
</form>
|
|
198
|
-
{authenticators !== undefined &&
|
|
198
|
+
{authenticators !== undefined && authenticators.authenticators.length !== 0 && (
|
|
199
199
|
<>
|
|
200
200
|
<form id="authn_select" className={kcClsx("kcFormClass")}>
|
|
201
201
|
{authenticators.authenticators.map((authenticator, i) => (
|
|
@@ -211,17 +211,17 @@ export function Page() {
|
|
|
211
211
|
</>
|
|
212
212
|
)}
|
|
213
213
|
<br /> {/* We use a br here because kcMarginTopClass is not defined in login v1 */}
|
|
214
|
-
<
|
|
215
|
-
id={
|
|
216
|
-
|
|
214
|
+
<input
|
|
215
|
+
id={webAuthnButtonId}
|
|
216
|
+
type="button"
|
|
217
217
|
className={kcClsx(
|
|
218
|
-
"
|
|
219
|
-
"
|
|
220
|
-
|
|
218
|
+
"kcButtonClass",
|
|
219
|
+
"kcButtonDefaultClass",
|
|
220
|
+
"kcButtonBlockClass",
|
|
221
|
+
"kcButtonLargeClass"
|
|
221
222
|
)}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
</a>
|
|
223
|
+
value={msgStr("passkey-doAuthenticate")}
|
|
224
|
+
/>
|
|
225
225
|
</>
|
|
226
226
|
)}
|
|
227
227
|
</Template>
|
|
@@ -7,8 +7,8 @@ import { BASE_URL } from "../../../kc.gen";
|
|
|
7
7
|
import { useI18n } from "../../i18n";
|
|
8
8
|
import { useKcContext } from "../../KcContext";
|
|
9
9
|
|
|
10
|
-
export function useScript(params: {
|
|
11
|
-
const {
|
|
10
|
+
export function useScript(params: { webAuthnButtonId: string }) {
|
|
11
|
+
const { webAuthnButtonId } = params;
|
|
12
12
|
|
|
13
13
|
const { kcContext } = useKcContext();
|
|
14
14
|
assert(kcContext.pageId === "login-username.ftl");
|
|
@@ -24,7 +24,7 @@ export function useScript(params: { authButtonId: string }) {
|
|
|
24
24
|
import { authenticateByWebAuthn } from "${BASE_URL}keycloak-theme/login/js/webauthnAuthenticate.js";
|
|
25
25
|
import { initAuthenticate } from "${BASE_URL}keycloak-theme/login/js/passkeysConditionalAuth.js";
|
|
26
26
|
|
|
27
|
-
const authButton = document.getElementById("${
|
|
27
|
+
const authButton = document.getElementById("${webAuthnButtonId}");
|
|
28
28
|
const input = {
|
|
29
29
|
isUserIdentified : ${kcContext.isUserIdentified},
|
|
30
30
|
challenge : ${JSON.stringify(kcContext.challenge)},
|
|
@@ -55,7 +55,7 @@ export function useScript(params: { authButtonId: string }) {
|
|
|
55
55
|
|
|
56
56
|
(async () => {
|
|
57
57
|
await waitForElementMountedOnDom({
|
|
58
|
-
elementId:
|
|
58
|
+
elementId: webAuthnButtonId
|
|
59
59
|
});
|
|
60
60
|
|
|
61
61
|
insertScriptTags();
|
|
@@ -17,9 +17,9 @@ export function Page() {
|
|
|
17
17
|
|
|
18
18
|
const { msg, msgStr, advancedMsg } = useI18n();
|
|
19
19
|
|
|
20
|
-
const
|
|
20
|
+
const webAuthnButtonId = "authenticateWebAuthnButton";
|
|
21
21
|
|
|
22
|
-
useScript({
|
|
22
|
+
useScript({ webAuthnButtonId });
|
|
23
23
|
|
|
24
24
|
return (
|
|
25
25
|
<Template
|
|
@@ -171,7 +171,7 @@ export function Page() {
|
|
|
171
171
|
)}
|
|
172
172
|
<div id="kc-form-buttons" className={kcClsx("kcFormButtonsClass")}>
|
|
173
173
|
<input
|
|
174
|
-
id={
|
|
174
|
+
id={webAuthnButtonId}
|
|
175
175
|
type="button"
|
|
176
176
|
autoFocus
|
|
177
177
|
value={msgStr("webauthn-doAuthenticate")}
|
|
@@ -7,8 +7,8 @@ import { BASE_URL } from "../../../kc.gen";
|
|
|
7
7
|
import { useKcContext } from "../../KcContext";
|
|
8
8
|
import { useI18n } from "../../i18n";
|
|
9
9
|
|
|
10
|
-
export function useScript(params: {
|
|
11
|
-
const {
|
|
10
|
+
export function useScript(params: { webAuthnButtonId: string }) {
|
|
11
|
+
const { webAuthnButtonId } = params;
|
|
12
12
|
|
|
13
13
|
const { kcContext } = useKcContext();
|
|
14
14
|
assert(kcContext.pageId === "webauthn-authenticate.ftl");
|
|
@@ -23,7 +23,7 @@ export function useScript(params: { authButtonId: string }) {
|
|
|
23
23
|
textContent: () => `
|
|
24
24
|
|
|
25
25
|
import { authenticateByWebAuthn } from "${BASE_URL}keycloak-theme/login/js/webauthnAuthenticate.js";
|
|
26
|
-
const authButton = document.getElementById('${
|
|
26
|
+
const authButton = document.getElementById('${webAuthnButtonId}');
|
|
27
27
|
authButton.addEventListener("click", function() {
|
|
28
28
|
const input = {
|
|
29
29
|
isUserIdentified : ${kcContext.isUserIdentified},
|
|
@@ -47,7 +47,7 @@ export function useScript(params: { authButtonId: string }) {
|
|
|
47
47
|
|
|
48
48
|
(async () => {
|
|
49
49
|
await waitForElementMountedOnDom({
|
|
50
|
-
elementId:
|
|
50
|
+
elementId: webAuthnButtonId
|
|
51
51
|
});
|
|
52
52
|
|
|
53
53
|
insertScriptTags();
|
|
@@ -14,9 +14,9 @@ export function Page() {
|
|
|
14
14
|
|
|
15
15
|
const { msg, msgStr } = useI18n();
|
|
16
16
|
|
|
17
|
-
const
|
|
17
|
+
const webAuthnButtonId = "authenticateWebAuthnButton";
|
|
18
18
|
|
|
19
|
-
useScript({
|
|
19
|
+
useScript({ webAuthnButtonId });
|
|
20
20
|
|
|
21
21
|
return (
|
|
22
22
|
<Template
|
|
@@ -51,7 +51,7 @@ export function Page() {
|
|
|
51
51
|
"kcButtonBlockClass",
|
|
52
52
|
"kcButtonLargeClass"
|
|
53
53
|
)}
|
|
54
|
-
id={
|
|
54
|
+
id={webAuthnButtonId}
|
|
55
55
|
value={msgStr("doRegisterSecurityKey")}
|
|
56
56
|
/>
|
|
57
57
|
|
|
@@ -7,8 +7,8 @@ import { BASE_URL } from "../../../kc.gen";
|
|
|
7
7
|
import { useKcContext } from "../../KcContext";
|
|
8
8
|
import { useI18n } from "../../i18n";
|
|
9
9
|
|
|
10
|
-
export function useScript(params: {
|
|
11
|
-
const {
|
|
10
|
+
export function useScript(params: { webAuthnButtonId: string }) {
|
|
11
|
+
const { webAuthnButtonId } = params;
|
|
12
12
|
|
|
13
13
|
const { kcContext } = useKcContext();
|
|
14
14
|
assert(kcContext.pageId === "webauthn-register.ftl");
|
|
@@ -22,7 +22,7 @@ export function useScript(params: { authButtonId: string }) {
|
|
|
22
22
|
type: "module",
|
|
23
23
|
textContent: () => `
|
|
24
24
|
import { registerByWebAuthn } from "${BASE_URL}keycloak-theme/login/js/webauthnRegister.js";
|
|
25
|
-
const registerButton = document.getElementById('${
|
|
25
|
+
const registerButton = document.getElementById('${webAuthnButtonId}');
|
|
26
26
|
registerButton.addEventListener("click", function() {
|
|
27
27
|
const input = {
|
|
28
28
|
challenge : '${kcContext.challenge}',
|
|
@@ -55,7 +55,7 @@ export function useScript(params: { authButtonId: string }) {
|
|
|
55
55
|
|
|
56
56
|
(async () => {
|
|
57
57
|
await waitForElementMountedOnDom({
|
|
58
|
-
elementId:
|
|
58
|
+
elementId: webAuthnButtonId
|
|
59
59
|
});
|
|
60
60
|
|
|
61
61
|
insertScriptTags();
|
package/package.json
CHANGED
|
@@ -196,6 +196,16 @@ export declare namespace KcContext {
|
|
|
196
196
|
iconClasses?: string;
|
|
197
197
|
}[];
|
|
198
198
|
};
|
|
199
|
+
enableWebAuthnConditionalUI?: boolean;
|
|
200
|
+
authenticators?: {
|
|
201
|
+
authenticators: WebauthnAuthenticate.WebauthnAuthenticator[];
|
|
202
|
+
};
|
|
203
|
+
challenge: string;
|
|
204
|
+
userVerification: WebauthnAuthenticate["userVerification"];
|
|
205
|
+
rpId: string;
|
|
206
|
+
createTimeout: number | string;
|
|
207
|
+
isUserIdentified: "true" | "false";
|
|
208
|
+
shouldDisplayAuthenticators?: boolean;
|
|
199
209
|
};
|
|
200
210
|
|
|
201
211
|
export type Register = Common & {
|
|
@@ -355,6 +365,16 @@ export declare namespace KcContext {
|
|
|
355
365
|
showTryAnotherWayLink?: boolean;
|
|
356
366
|
attemptedUsername?: string;
|
|
357
367
|
};
|
|
368
|
+
enableWebAuthnConditionalUI?: boolean;
|
|
369
|
+
authenticators?: {
|
|
370
|
+
authenticators: WebauthnAuthenticate.WebauthnAuthenticator[];
|
|
371
|
+
};
|
|
372
|
+
challenge: string;
|
|
373
|
+
userVerification: WebauthnAuthenticate["userVerification"];
|
|
374
|
+
rpId: string;
|
|
375
|
+
createTimeout: number | string;
|
|
376
|
+
isUserIdentified: "true" | "false";
|
|
377
|
+
shouldDisplayAuthenticators?: boolean;
|
|
358
378
|
};
|
|
359
379
|
|
|
360
380
|
export type WebauthnAuthenticate = Common & {
|
|
@@ -208,7 +208,17 @@ export const kcContextMocks = [
|
|
|
208
208
|
},
|
|
209
209
|
usernameHidden: false,
|
|
210
210
|
login: {},
|
|
211
|
-
registrationDisabled: false
|
|
211
|
+
registrationDisabled: false,
|
|
212
|
+
enableWebAuthnConditionalUI: false,
|
|
213
|
+
authenticators: {
|
|
214
|
+
authenticators: []
|
|
215
|
+
},
|
|
216
|
+
challenge: "",
|
|
217
|
+
userVerification: "not specified",
|
|
218
|
+
rpId: "",
|
|
219
|
+
createTimeout: "0",
|
|
220
|
+
isUserIdentified: "false",
|
|
221
|
+
shouldDisplayAuthenticators: false
|
|
212
222
|
}),
|
|
213
223
|
id<KcContext.Register>({
|
|
214
224
|
...kcContextCommonMock,
|
|
@@ -348,7 +358,17 @@ export const kcContextMocks = [
|
|
|
348
358
|
realm: {
|
|
349
359
|
...kcContextCommonMock.realm,
|
|
350
360
|
resetPasswordAllowed: true
|
|
351
|
-
}
|
|
361
|
+
},
|
|
362
|
+
enableWebAuthnConditionalUI: false,
|
|
363
|
+
authenticators: {
|
|
364
|
+
authenticators: []
|
|
365
|
+
},
|
|
366
|
+
challenge: "",
|
|
367
|
+
userVerification: "not specified",
|
|
368
|
+
rpId: "",
|
|
369
|
+
createTimeout: "0",
|
|
370
|
+
isUserIdentified: "false",
|
|
371
|
+
shouldDisplayAuthenticators: false
|
|
352
372
|
}),
|
|
353
373
|
id<KcContext.WebauthnAuthenticate>({
|
|
354
374
|
...kcContextCommonMock,
|