@keycloakify/login-ui 250004.2.2 → 250004.3.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 +4 -0
- package/keycloak-theme/login/pages/login-username/Page.tsx +55 -2
- package/keycloak-theme/login/pages/login-username/useScript.tsx +64 -0
- package/keycloak-theme/public/login/js/passkeysConditionalAuth.js +1 -1
- package/package.json +1 -1
- package/src/core/KcContext/KcContext.ts +5 -0
|
@@ -265,6 +265,10 @@ export declare namespace KcContext {
|
|
|
265
265
|
};
|
|
266
266
|
usernameHidden?: boolean;
|
|
267
267
|
social?: Login["social"];
|
|
268
|
+
enableWebAuthnConditionalUI?: boolean;
|
|
269
|
+
authenticators?: {
|
|
270
|
+
authenticators: WebauthnAuthenticate.WebauthnAuthenticator[];
|
|
271
|
+
};
|
|
268
272
|
};
|
|
269
273
|
type LoginPassword = Common & {
|
|
270
274
|
pageId: "login-password.ftl";
|
|
@@ -5,6 +5,7 @@ import { useKcClsx } from "@keycloakify/login-ui/useKcClsx";
|
|
|
5
5
|
import { useKcContext } from "../../KcContext";
|
|
6
6
|
import { useI18n } from "../../i18n";
|
|
7
7
|
import { Template } from "../../components/Template";
|
|
8
|
+
import { useScript } from "./useScript";
|
|
8
9
|
|
|
9
10
|
export function Page() {
|
|
10
11
|
const { kcContext } = useKcContext();
|
|
@@ -12,13 +13,26 @@ export function Page() {
|
|
|
12
13
|
|
|
13
14
|
const { kcClsx } = useKcClsx();
|
|
14
15
|
|
|
15
|
-
const {
|
|
16
|
-
|
|
16
|
+
const {
|
|
17
|
+
social,
|
|
18
|
+
realm,
|
|
19
|
+
url,
|
|
20
|
+
usernameHidden,
|
|
21
|
+
login,
|
|
22
|
+
registrationDisabled,
|
|
23
|
+
messagesPerField,
|
|
24
|
+
enableWebAuthnConditionalUI,
|
|
25
|
+
authenticators
|
|
26
|
+
} = kcContext;
|
|
17
27
|
|
|
18
28
|
const { msg, msgStr } = useI18n();
|
|
19
29
|
|
|
20
30
|
const [isLoginButtonDisabled, setIsLoginButtonDisabled] = useState(false);
|
|
21
31
|
|
|
32
|
+
const authButtonId = "authenticateWebAuthnButton";
|
|
33
|
+
|
|
34
|
+
useScript({ authButtonId });
|
|
35
|
+
|
|
22
36
|
return (
|
|
23
37
|
<Template
|
|
24
38
|
displayMessage={!messagesPerField.existsError("username")}
|
|
@@ -171,6 +185,45 @@ export function Page() {
|
|
|
171
185
|
)}
|
|
172
186
|
</div>
|
|
173
187
|
</div>
|
|
188
|
+
{enableWebAuthnConditionalUI && (
|
|
189
|
+
<>
|
|
190
|
+
<form id="webauth" action={url.loginAction} method="post">
|
|
191
|
+
<input type="hidden" id="clientDataJSON" name="clientDataJSON" />
|
|
192
|
+
<input type="hidden" id="authenticatorData" name="authenticatorData" />
|
|
193
|
+
<input type="hidden" id="signature" name="signature" />
|
|
194
|
+
<input type="hidden" id="credentialId" name="credentialId" />
|
|
195
|
+
<input type="hidden" id="userHandle" name="userHandle" />
|
|
196
|
+
<input type="hidden" id="error" name="error" />
|
|
197
|
+
</form>
|
|
198
|
+
|
|
199
|
+
{authenticators !== undefined && Object.keys(authenticators).length !== 0 && (
|
|
200
|
+
<>
|
|
201
|
+
<form id="authn_select" className={kcClsx("kcFormClass")}>
|
|
202
|
+
{authenticators.authenticators.map((authenticator, i) => (
|
|
203
|
+
<input
|
|
204
|
+
key={i}
|
|
205
|
+
type="hidden"
|
|
206
|
+
name="authn_use_chk"
|
|
207
|
+
readOnly
|
|
208
|
+
value={authenticator.credentialId}
|
|
209
|
+
/>
|
|
210
|
+
))}
|
|
211
|
+
</form>
|
|
212
|
+
</>
|
|
213
|
+
)}
|
|
214
|
+
<a
|
|
215
|
+
id={authButtonId}
|
|
216
|
+
href="#"
|
|
217
|
+
className={kcClsx(
|
|
218
|
+
"kcButtonSecondaryClass",
|
|
219
|
+
"kcButtonBlockClass",
|
|
220
|
+
"kcMarginTopClass"
|
|
221
|
+
)}
|
|
222
|
+
>
|
|
223
|
+
{msg("passkey-doAuthenticate")}
|
|
224
|
+
</a>
|
|
225
|
+
</>
|
|
226
|
+
)}
|
|
174
227
|
</Template>
|
|
175
228
|
);
|
|
176
229
|
}
|
|
@@ -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: { authButtonId: string }) {
|
|
11
|
+
const { authButtonId } = params;
|
|
12
|
+
|
|
13
|
+
const { kcContext } = useKcContext();
|
|
14
|
+
assert(kcContext.pageId === "login-username.ftl");
|
|
15
|
+
|
|
16
|
+
const { msgStr, isFetchingTranslations } = useI18n();
|
|
17
|
+
|
|
18
|
+
const { insertScriptTags } = useInsertScriptTags({
|
|
19
|
+
effectId: "LoginRecoveryAuthnCodeConfigLoginUsername",
|
|
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("${authButtonId}");
|
|
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: authButtonId
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
insertScriptTags();
|
|
62
|
+
})();
|
|
63
|
+
}, [isFetchingTranslations]);
|
|
64
|
+
}
|
package/package.json
CHANGED
|
@@ -327,6 +327,11 @@ export declare namespace KcContext {
|
|
|
327
327
|
};
|
|
328
328
|
usernameHidden?: boolean;
|
|
329
329
|
social?: Login["social"];
|
|
330
|
+
|
|
331
|
+
enableWebAuthnConditionalUI?: boolean;
|
|
332
|
+
authenticators?: {
|
|
333
|
+
authenticators: WebauthnAuthenticate.WebauthnAuthenticator[];
|
|
334
|
+
};
|
|
330
335
|
};
|
|
331
336
|
|
|
332
337
|
export type LoginPassword = Common & {
|