@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.
@@ -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
- <div id="kc-form">
21
- <div id="kc-form-wrapper">
22
- {kcContext.realm.password && (
23
- <form
24
- id="kc-form-login"
25
- onSubmit={() => {
26
- setIsLoginButtonDisabled(true);
27
- return true;
28
- }}
29
- action={kcContext.url.loginAction}
30
- method="post"
31
- >
32
- {!kcContext.usernameHidden && (
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="username" className={kcClsx("kcLabelClass")}>
35
- {!kcContext.realm.loginWithEmailAllowed
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
- <input
42
- tabIndex={2}
43
- id="username"
44
- className={kcClsx("kcInputClass")}
45
- name="username"
46
- defaultValue={kcContext.login.username ?? ""}
47
- type="text"
48
- autoFocus
49
- autoComplete="username"
50
- aria-invalid={kcContext.messagesPerField.existsError(
51
- "username",
52
- "password"
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
- <div className={kcClsx("kcFormGroupClass")}>
74
- <label htmlFor="password" className={kcClsx("kcLabelClass")}>
75
- {msg("password")}
76
- </label>
77
- <PasswordWrapper passwordInputId="password">
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
- tabIndex={3}
80
- id="password"
81
- className={kcClsx("kcInputClass")}
82
- name="password"
83
- type="password"
84
- autoComplete="current-password"
85
- aria-invalid={kcContext.messagesPerField.existsError(
86
- "username",
87
- "password"
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
- </div>
168
+ </form>
169
+ )}
170
+ </div>
171
+ </div>
136
172
 
137
- <div id="kc-form-buttons" className={kcClsx("kcFormGroupClass")}>
138
- <input
139
- type="hidden"
140
- id="id-hidden-input"
141
- name="credentialId"
142
- value={kcContext.auth.selectedCredential}
143
- />
144
- <input
145
- tabIndex={7}
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
- </div>
162
- </div>
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 authButtonId = "authenticateWebAuthnButton";
29
+ const webAuthnButtonId = "authenticateWebAuthnButton";
30
30
 
31
- useScript({ authButtonId });
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={authButtonId}
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: { authButtonId: string }) {
11
- const { authButtonId } = params;
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("${authButtonId}");
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: authButtonId
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 authButtonId = "authenticateWebAuthnButton";
32
+ const webAuthnButtonId = "authenticateWebAuthnButton";
33
33
 
34
- useScript({ authButtonId });
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 && Object.keys(authenticators).length !== 0 && (
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
- <a
215
- id={authButtonId}
216
- href="#"
214
+ <input
215
+ id={webAuthnButtonId}
216
+ type="button"
217
217
  className={kcClsx(
218
- "kcButtonSecondaryClass",
219
- "kcButtonBlockClass"
220
- //"kcMarginTopClass"
218
+ "kcButtonClass",
219
+ "kcButtonDefaultClass",
220
+ "kcButtonBlockClass",
221
+ "kcButtonLargeClass"
221
222
  )}
222
- >
223
- {msg("passkey-doAuthenticate")}
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: { authButtonId: string }) {
11
- const { authButtonId } = params;
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("${authButtonId}");
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: authButtonId
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 authButtonId = "authenticateWebAuthnButton";
20
+ const webAuthnButtonId = "authenticateWebAuthnButton";
21
21
 
22
- useScript({ authButtonId });
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={authButtonId}
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: { authButtonId: string }) {
11
- const { authButtonId } = params;
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('${authButtonId}');
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: authButtonId
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 authButtonId = "authenticateWebAuthnButton";
17
+ const webAuthnButtonId = "authenticateWebAuthnButton";
18
18
 
19
- useScript({ authButtonId });
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={authButtonId}
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: { authButtonId: string }) {
11
- const { authButtonId } = params;
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('${authButtonId}');
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: authButtonId
58
+ elementId: webAuthnButtonId
59
59
  });
60
60
 
61
61
  insertScriptTags();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@keycloakify/login-ui",
3
- "version": "250004.4.1",
3
+ "version": "250004.5.0",
4
4
  "description": "React implementation of Keycloak Login v2 for Keycloakify",
5
5
  "repository": {
6
6
  "type": "git",
@@ -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,