@keycloakify/login-ui 250004.2.3 → 250004.3.2
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 +9 -0
- package/core/KcContext/kcContextMocks.js +1 -1
- package/core/kcClsx.d.ts +1 -1
- package/core/kcClsx.js +1 -0
- package/keycloak-theme/login/pages/login-username/Page.tsx +55 -2
- package/keycloak-theme/login/pages/login-username/useScript.tsx +64 -0
- package/package.json +1 -1
- package/src/core/KcContext/KcContext.ts +10 -0
- package/src/core/KcContext/kcContextMocks.ts +7 -1
- package/src/core/kcClsx.ts +2 -0
|
@@ -265,6 +265,15 @@ 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
|
+
};
|
|
272
|
+
isUserIdentified: "true" | "false";
|
|
273
|
+
challenge: string;
|
|
274
|
+
userVerification: UserVerificationRequirement | "not specified";
|
|
275
|
+
rpId: string;
|
|
276
|
+
createTimeout: string | number;
|
|
268
277
|
};
|
|
269
278
|
type LoginPassword = Common & {
|
|
270
279
|
pageId: "login-password.ftl";
|
|
@@ -219,7 +219,7 @@ export const kcContextMocks = [
|
|
|
219
219
|
} })),
|
|
220
220
|
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
221
|
displayInfo: true
|
|
222
|
-
}, usernameHidden: false, login: {}, registrationDisabled: false })),
|
|
222
|
+
}, usernameHidden: false, login: {}, registrationDisabled: false, challenge: "", userVerification: "not specified", rpId: "", createTimeout: "0", isUserIdentified: "false" })),
|
|
223
223
|
id(Object.assign(Object.assign({}, kcContextCommonMock), { pageId: "login-password.ftl", url: loginUrl, realm: Object.assign(Object.assign({}, kcContextCommonMock.realm), { resetPasswordAllowed: true }) })),
|
|
224
224
|
id(Object.assign(Object.assign({}, kcContextCommonMock), { pageId: "webauthn-authenticate.ftl", url: loginUrl, authenticators: {
|
|
225
225
|
authenticators: []
|
package/core/kcClsx.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type ClassKey = "kcBodyClass" | "kcHeaderWrapperClass" | "kcLocaleWrapperClass" | "kcInfoAreaWrapperClass" | "kcFormButtonsWrapperClass" | "kcFormOptionsWrapperClass" | "kcCheckboxInputClass" | "kcLocaleDropDownClass" | "kcLocaleListItemClass" | "kcContentWrapperClass" | "kcLogoIdP-facebook" | "kcAuthenticatorOTPClass" | "kcLogoIdP-bitbucket" | "kcAuthenticatorWebAuthnClass" | "kcWebAuthnDefaultIcon" | "kcLogoIdP-stackoverflow" | "kcSelectAuthListItemClass" | "kcLogoIdP-microsoft" | "kcLoginOTPListItemHeaderClass" | "kcLocaleItemClass" | "kcLoginOTPListItemIconBodyClass" | "kcInputHelperTextAfterClass" | "kcFormClass" | "kcSelectAuthListClass" | "kcInputClassRadioCheckboxLabelDisabled" | "kcSelectAuthListItemIconClass" | "kcRecoveryCodesWarning" | "kcFormSettingClass" | "kcWebAuthnBLE" | "kcInputWrapperClass" | "kcSelectAuthListItemArrowIconClass" | "kcFeedbackAreaClass" | "kcFormPasswordVisibilityButtonClass" | "kcLogoIdP-google" | "kcCheckLabelClass" | "kcSelectAuthListItemFillClass" | "kcAuthenticatorDefaultClass" | "kcLogoIdP-gitlab" | "kcFormAreaClass" | "kcFormButtonsClass" | "kcInputClassRadioLabel" | "kcAuthenticatorWebAuthnPasswordlessClass" | "kcSelectAuthListItemHeadingClass" | "kcInfoAreaClass" | "kcContainerClass" | "kcSelectAuthListItemTitle" | "kcHtmlClass" | "kcLoginOTPListItemTitleClass" | "kcLogoIdP-openshift-v4" | "kcWebAuthnUnknownIcon" | "kcFormSocialAccountNameClass" | "kcLogoIdP-openshift-v3" | "kcLoginOTPListInputClass" | "kcWebAuthnUSB" | "kcInputClassRadio" | "kcWebAuthnKeyIcon" | "kcFeedbackInfoIcon" | "kcCommonLogoIdP" | "kcRecoveryCodesActions" | "kcFormGroupHeader" | "kcFormSocialAccountSectionClass" | "kcLogoIdP-instagram" | "kcAlertClass" | "kcHeaderClass" | "kcLabelWrapperClass" | "kcFormPasswordVisibilityIconShow" | "kcFormSocialAccountLinkClass" | "kcLocaleMainClass" | "kcInputGroup" | "kcTextareaClass" | "kcButtonBlockClass" | "kcButtonClass" | "kcWebAuthnNFC" | "kcLocaleClass" | "kcInputClassCheckboxInput" | "kcFeedbackErrorIcon" | "kcInputLargeClass" | "kcInputErrorMessageClass" | "kcRecoveryCodesList" | "kcFormSocialAccountListClass" | "kcAlertTitleClass" | "kcAuthenticatorPasswordClass" | "kcCheckInputClass" | "kcLogoIdP-linkedin" | "kcLogoIdP-twitter" | "kcFeedbackWarningIcon" | "kcResetFlowIcon" | "kcSelectAuthListItemIconPropertyClass" | "kcFeedbackSuccessIcon" | "kcLoginOTPListClass" | "kcSrOnlyClass" | "kcFormSocialAccountListGridClass" | "kcButtonDefaultClass" | "kcFormGroupErrorClass" | "kcSelectAuthListItemDescriptionClass" | "kcSelectAuthListItemBodyClass" | "kcWebAuthnInternal" | "kcSelectAuthListItemArrowClass" | "kcCheckClass" | "kcContentClass" | "kcLogoClass" | "kcLoginOTPListItemIconClass" | "kcLoginClass" | "kcSignUpClass" | "kcButtonLargeClass" | "kcFormCardClass" | "kcLocaleListClass" | "kcInputClass" | "kcFormGroupClass" | "kcLogoIdP-paypal" | "kcInputClassCheckbox" | "kcRecoveryCodesConfirmation" | "kcFormPasswordVisibilityIconHide" | "kcInputClassRadioInput" | "kcFormSocialAccountListButtonClass" | "kcInputClassCheckboxLabel" | "kcFormOptionsClass" | "kcFormHeaderClass" | "kcFormSocialAccountGridItem" | "kcButtonPrimaryClass" | "kcInputHelperTextBeforeClass" | "kcLogoIdP-github" | "kcLabelClass";
|
|
1
|
+
export type ClassKey = "kcBodyClass" | "kcHeaderWrapperClass" | "kcLocaleWrapperClass" | "kcInfoAreaWrapperClass" | "kcFormButtonsWrapperClass" | "kcFormOptionsWrapperClass" | "kcCheckboxInputClass" | "kcLocaleDropDownClass" | "kcLocaleListItemClass" | "kcContentWrapperClass" | "kcLogoIdP-facebook" | "kcAuthenticatorOTPClass" | "kcLogoIdP-bitbucket" | "kcAuthenticatorWebAuthnClass" | "kcWebAuthnDefaultIcon" | "kcLogoIdP-stackoverflow" | "kcSelectAuthListItemClass" | "kcLogoIdP-microsoft" | "kcLoginOTPListItemHeaderClass" | "kcLocaleItemClass" | "kcLoginOTPListItemIconBodyClass" | "kcInputHelperTextAfterClass" | "kcFormClass" | "kcSelectAuthListClass" | "kcInputClassRadioCheckboxLabelDisabled" | "kcSelectAuthListItemIconClass" | "kcRecoveryCodesWarning" | "kcFormSettingClass" | "kcWebAuthnBLE" | "kcInputWrapperClass" | "kcSelectAuthListItemArrowIconClass" | "kcFeedbackAreaClass" | "kcFormPasswordVisibilityButtonClass" | "kcLogoIdP-google" | "kcCheckLabelClass" | "kcSelectAuthListItemFillClass" | "kcAuthenticatorDefaultClass" | "kcLogoIdP-gitlab" | "kcFormAreaClass" | "kcFormButtonsClass" | "kcInputClassRadioLabel" | "kcAuthenticatorWebAuthnPasswordlessClass" | "kcSelectAuthListItemHeadingClass" | "kcInfoAreaClass" | "kcContainerClass" | "kcSelectAuthListItemTitle" | "kcHtmlClass" | "kcLoginOTPListItemTitleClass" | "kcLogoIdP-openshift-v4" | "kcWebAuthnUnknownIcon" | "kcFormSocialAccountNameClass" | "kcLogoIdP-openshift-v3" | "kcLoginOTPListInputClass" | "kcWebAuthnUSB" | "kcInputClassRadio" | "kcWebAuthnKeyIcon" | "kcFeedbackInfoIcon" | "kcCommonLogoIdP" | "kcRecoveryCodesActions" | "kcFormGroupHeader" | "kcFormSocialAccountSectionClass" | "kcLogoIdP-instagram" | "kcAlertClass" | "kcHeaderClass" | "kcLabelWrapperClass" | "kcFormPasswordVisibilityIconShow" | "kcFormSocialAccountLinkClass" | "kcLocaleMainClass" | "kcInputGroup" | "kcTextareaClass" | "kcButtonBlockClass" | "kcButtonClass" | "kcWebAuthnNFC" | "kcLocaleClass" | "kcInputClassCheckboxInput" | "kcFeedbackErrorIcon" | "kcInputLargeClass" | "kcInputErrorMessageClass" | "kcRecoveryCodesList" | "kcFormSocialAccountListClass" | "kcAlertTitleClass" | "kcAuthenticatorPasswordClass" | "kcCheckInputClass" | "kcLogoIdP-linkedin" | "kcLogoIdP-twitter" | "kcFeedbackWarningIcon" | "kcResetFlowIcon" | "kcSelectAuthListItemIconPropertyClass" | "kcFeedbackSuccessIcon" | "kcLoginOTPListClass" | "kcSrOnlyClass" | "kcFormSocialAccountListGridClass" | "kcButtonDefaultClass" | "kcFormGroupErrorClass" | "kcSelectAuthListItemDescriptionClass" | "kcSelectAuthListItemBodyClass" | "kcWebAuthnInternal" | "kcSelectAuthListItemArrowClass" | "kcCheckClass" | "kcContentClass" | "kcLogoClass" | "kcLoginOTPListItemIconClass" | "kcLoginClass" | "kcSignUpClass" | "kcButtonLargeClass" | "kcFormCardClass" | "kcLocaleListClass" | "kcInputClass" | "kcFormGroupClass" | "kcLogoIdP-paypal" | "kcInputClassCheckbox" | "kcRecoveryCodesConfirmation" | "kcFormPasswordVisibilityIconHide" | "kcInputClassRadioInput" | "kcFormSocialAccountListButtonClass" | "kcInputClassCheckboxLabel" | "kcFormOptionsClass" | "kcFormHeaderClass" | "kcFormSocialAccountGridItem" | "kcButtonPrimaryClass" | "kcButtonSecondaryClass" | "kcInputHelperTextBeforeClass" | "kcLogoIdP-github" | "kcLabelClass";
|
|
2
2
|
export declare const getKcClsx: (params: {
|
|
3
3
|
doUseDefaultCss: boolean;
|
|
4
4
|
classes: Partial<Record<ClassKey, string>> | undefined;
|
package/core/kcClsx.js
CHANGED
|
@@ -121,6 +121,7 @@ export const { getKcClsx } = createGetKcClsx({
|
|
|
121
121
|
kcFormHeaderClass: "login-pf-header",
|
|
122
122
|
kcFormSocialAccountGridItem: "pf-l-grid__item",
|
|
123
123
|
kcButtonPrimaryClass: "pf-m-primary",
|
|
124
|
+
kcButtonSecondaryClass: "pf-m-secondary",
|
|
124
125
|
kcInputHelperTextBeforeClass: "pf-c-form__helper-text pf-c-form__helper-text-before",
|
|
125
126
|
"kcLogoIdP-github": "fa fa-github",
|
|
126
127
|
kcLabelClass: "pf-c-form__label pf-c-form__label-text"
|
|
@@ -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
|
+
{authenticators !== undefined && Object.keys(authenticators).length !== 0 && (
|
|
199
|
+
<>
|
|
200
|
+
<form id="authn_select" className={kcClsx("kcFormClass")}>
|
|
201
|
+
{authenticators.authenticators.map((authenticator, i) => (
|
|
202
|
+
<input
|
|
203
|
+
key={i}
|
|
204
|
+
type="hidden"
|
|
205
|
+
name="authn_use_chk"
|
|
206
|
+
readOnly
|
|
207
|
+
value={authenticator.credentialId}
|
|
208
|
+
/>
|
|
209
|
+
))}
|
|
210
|
+
</form>
|
|
211
|
+
</>
|
|
212
|
+
)}
|
|
213
|
+
<br /> {/* We use a br here because kcMarginTopClass is not defined in login v1 */}
|
|
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,16 @@ 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
|
+
};
|
|
335
|
+
isUserIdentified: "true" | "false";
|
|
336
|
+
challenge: string;
|
|
337
|
+
userVerification: UserVerificationRequirement | "not specified";
|
|
338
|
+
rpId: string;
|
|
339
|
+
createTimeout: string | number;
|
|
330
340
|
};
|
|
331
341
|
|
|
332
342
|
export type LoginPassword = Common & {
|
|
@@ -333,7 +333,13 @@ export const kcContextMocks = [
|
|
|
333
333
|
},
|
|
334
334
|
usernameHidden: false,
|
|
335
335
|
login: {},
|
|
336
|
-
registrationDisabled: false
|
|
336
|
+
registrationDisabled: false,
|
|
337
|
+
|
|
338
|
+
challenge: "",
|
|
339
|
+
userVerification: "not specified",
|
|
340
|
+
rpId: "",
|
|
341
|
+
createTimeout: "0",
|
|
342
|
+
isUserIdentified: "false"
|
|
337
343
|
}),
|
|
338
344
|
id<KcContext.LoginPassword>({
|
|
339
345
|
...kcContextCommonMock,
|
package/src/core/kcClsx.ts
CHANGED
|
@@ -121,6 +121,7 @@ export type ClassKey =
|
|
|
121
121
|
| "kcFormHeaderClass"
|
|
122
122
|
| "kcFormSocialAccountGridItem"
|
|
123
123
|
| "kcButtonPrimaryClass"
|
|
124
|
+
| "kcButtonSecondaryClass"
|
|
124
125
|
| "kcInputHelperTextBeforeClass"
|
|
125
126
|
| "kcLogoIdP-github"
|
|
126
127
|
| "kcLabelClass";
|
|
@@ -249,6 +250,7 @@ export const { getKcClsx } = createGetKcClsx<ClassKey>({
|
|
|
249
250
|
kcFormHeaderClass: "login-pf-header",
|
|
250
251
|
kcFormSocialAccountGridItem: "pf-l-grid__item",
|
|
251
252
|
kcButtonPrimaryClass: "pf-m-primary",
|
|
253
|
+
kcButtonSecondaryClass: "pf-m-secondary",
|
|
252
254
|
kcInputHelperTextBeforeClass: "pf-c-form__helper-text pf-c-form__helper-text-before",
|
|
253
255
|
"kcLogoIdP-github": "fa fa-github",
|
|
254
256
|
kcLabelClass: "pf-c-form__label pf-c-form__label-text"
|