keycloakify 6.6.3 → 6.7.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "keycloakify",
3
- "version": "6.6.3",
3
+ "version": "6.7.1",
4
4
  "description": "Keycloak theme generator for Reacts app",
5
5
  "repository": {
6
6
  "type": "git",
@@ -82,6 +82,7 @@
82
82
  "src/lib/components/Template.tsx",
83
83
  "src/lib/components/Terms.tsx",
84
84
  "src/lib/components/UpdateUserProfile.tsx",
85
+ "src/lib/components/WebauthnAuthenticate.tsx",
85
86
  "src/lib/components/shared/UserProfileCommons.tsx",
86
87
  "src/lib/getKcContext/KcContextBase.ts",
87
88
  "src/lib/getKcContext/getKcContext.ts",
@@ -505,6 +506,9 @@
505
506
  "lib/components/UpdateUserProfile.d.ts",
506
507
  "lib/components/UpdateUserProfile.js",
507
508
  "lib/components/UpdateUserProfile.js.map",
509
+ "lib/components/WebauthnAuthenticate.d.ts",
510
+ "lib/components/WebauthnAuthenticate.js",
511
+ "lib/components/WebauthnAuthenticate.js.map",
508
512
  "lib/components/shared/UserProfileCommons.d.ts",
509
513
  "lib/components/shared/UserProfileCommons.js",
510
514
  "lib/components/shared/UserProfileCommons.js.map",
@@ -1286,15 +1290,16 @@
1286
1290
  "@octokit/rest": "^18.12.0",
1287
1291
  "cheerio": "^1.0.0-rc.5",
1288
1292
  "cli-select": "^1.1.2",
1289
- "evt": "^2.4.4",
1293
+ "evt": "^2.4.5",
1290
1294
  "memoizee": "^0.4.15",
1291
1295
  "minimal-polyfills": "^2.2.2",
1292
1296
  "minimist": "^1.2.6",
1293
1297
  "path-browserify": "^1.0.1",
1294
- "powerhooks": "^0.20.20",
1298
+ "powerhooks": "^0.20.21",
1295
1299
  "react-markdown": "^5.0.3",
1300
+ "rfc4648": "^1.5.2",
1296
1301
  "scripting-tools": "^0.19.13",
1297
- "tsafe": "^1.1.1",
1302
+ "tsafe": "^1.1.2",
1298
1303
  "tss-react": "^4.3.4",
1299
1304
  "zod": "^3.17.10"
1300
1305
  }
@@ -15,6 +15,7 @@ export const pageIds = [
15
15
  "login.ftl",
16
16
  "login-username.ftl",
17
17
  "login-password.ftl",
18
+ "webauthn-authenticate.ftl",
18
19
  "register.ftl",
19
20
  "register-user-profile.ftl",
20
21
  "info.ftl",
@@ -15,6 +15,7 @@ const Terms = lazy(() => import("./Terms"));
15
15
  const LoginOtp = lazy(() => import("./LoginOtp"));
16
16
  const LoginPassword = lazy(() => import("./LoginPassword"));
17
17
  const LoginUsername = lazy(() => import("./LoginUsername"));
18
+ const WebauthnAuthenticate = lazy(() => import("./WebauthnAuthenticate"));
18
19
  const LoginUpdatePassword = lazy(() => import("./LoginUpdatePassword"));
19
20
  const LoginUpdateProfile = lazy(() => import("./LoginUpdateProfile"));
20
21
  const LoginIdpLinkConfirm = lazy(() => import("./LoginIdpLinkConfirm"));
@@ -73,6 +74,8 @@ const KcApp = memo(
73
74
  return <LoginUsername {...{ kcContext, ...props }} />;
74
75
  case "login-password.ftl":
75
76
  return <LoginPassword {...{ kcContext, ...props }} />;
77
+ case "webauthn-authenticate.ftl":
78
+ return <WebauthnAuthenticate {...{ kcContext, ...props }} />;
76
79
  case "login-update-password.ftl":
77
80
  return <LoginUpdatePassword {...{ kcContext, ...props }} />;
78
81
  case "login-update-profile.ftl":
@@ -84,6 +84,7 @@ export type KcProps = KcPropsGeneric<
84
84
  | "kcFormSocialAccountDoubleListClass"
85
85
  | "kcFormSocialAccountListLinkClass"
86
86
  | "kcWebAuthnKeyIcon"
87
+ | "kcWebAuthnDefaultIcon"
87
88
  | "kcFormClass"
88
89
  | "kcFormGroupErrorClass"
89
90
  | "kcLabelClass"
@@ -105,12 +106,16 @@ export type KcProps = KcPropsGeneric<
105
106
  | "kcSrOnlyClass"
106
107
  | "kcSelectAuthListClass"
107
108
  | "kcSelectAuthListItemClass"
109
+ | "kcSelectAuthListItemFillClass"
108
110
  | "kcSelectAuthListItemInfoClass"
109
111
  | "kcSelectAuthListItemLeftClass"
110
112
  | "kcSelectAuthListItemBodyClass"
111
113
  | "kcSelectAuthListItemDescriptionClass"
112
114
  | "kcSelectAuthListItemHeadingClass"
113
115
  | "kcSelectAuthListItemHelpTextClass"
116
+ | "kcSelectAuthListItemIconPropertyClass"
117
+ | "kcSelectAuthListItemIconClass"
118
+ | "kcSelectAuthListItemTitle"
114
119
  | "kcAuthenticatorDefaultClass"
115
120
  | "kcAuthenticatorPasswordClass"
116
121
  | "kcAuthenticatorOTPClass"
@@ -138,6 +143,7 @@ export const defaultKcProps = {
138
143
  "kcFormSocialAccountDoubleListClass": ["login-pf-social-double-col"],
139
144
  "kcFormSocialAccountListLinkClass": ["login-pf-social-link"],
140
145
  "kcWebAuthnKeyIcon": ["pficon", "pficon-key"],
146
+ "kcWebAuthnDefaultIcon": ["pficon", "pficon-key"],
141
147
 
142
148
  "kcFormClass": ["form-horizontal"],
143
149
  "kcFormGroupErrorClass": ["has-error"],
@@ -173,6 +179,10 @@ export const defaultKcProps = {
173
179
  // css classes for select-authenticator form
174
180
  "kcSelectAuthListClass": ["list-group", "list-view-pf"],
175
181
  "kcSelectAuthListItemClass": ["list-group-item", "list-view-pf-stacked"],
182
+ "kcSelectAuthListItemFillClass": ["pf-l-split__item", "pf-m-fill"],
183
+ "kcSelectAuthListItemIconPropertyClass": ["fa-2x", "select-auth-box-icon-properties"],
184
+ "kcSelectAuthListItemIconClass": ["pf-l-split__item", "select-auth-box-icon"],
185
+ "kcSelectAuthListItemTitle": ["select-auth-box-paragraph"],
176
186
  "kcSelectAuthListItemInfoClass": ["list-view-pf-main-info"],
177
187
  "kcSelectAuthListItemLeftClass": ["list-view-pf-left"],
178
188
  "kcSelectAuthListItemBodyClass": ["list-view-pf-body"],
@@ -0,0 +1,204 @@
1
+ import React, { useRef, useState, memo } from "react";
2
+ import Template from "./Template";
3
+ import type { KcProps } from "./KcProps";
4
+ import type { KcContextBase } from "../getKcContext/KcContextBase";
5
+ import { useCssAndCx } from "../tools/useCssAndCx";
6
+ import type { I18n, MessageKeyBase } from "../i18n";
7
+ import { base64url } from "rfc4648";
8
+ import { useConstCallback } from "powerhooks/useConstCallback";
9
+
10
+ const WebauthnAuthenticate = memo(
11
+ ({
12
+ kcContext,
13
+ i18n,
14
+ doFetchDefaultThemeResources = true,
15
+ ...props
16
+ }: { kcContext: KcContextBase.WebauthnAuthenticate; i18n: I18n; doFetchDefaultThemeResources?: boolean } & KcProps) => {
17
+ const { url } = kcContext;
18
+
19
+ const { msg, msgStr } = i18n;
20
+
21
+ const { authenticators, challenge, shouldDisplayAuthenticators, userVerification, rpId } = kcContext;
22
+ const createTimeout = Number(kcContext.createTimeout);
23
+ const isUserIdentified = kcContext.isUserIdentified == "true";
24
+
25
+ const { cx } = useCssAndCx();
26
+
27
+ const webAuthnAuthenticate = useConstCallback(async () => {
28
+ if (!isUserIdentified) {
29
+ return;
30
+ }
31
+ const allowCredentials = authenticators.authenticators.map(
32
+ authenticator =>
33
+ ({
34
+ id: base64url.parse(authenticator.credentialId, { loose: true }),
35
+ type: "public-key"
36
+ } as PublicKeyCredentialDescriptor)
37
+ );
38
+ // Check if WebAuthn is supported by this browser
39
+ if (!window.PublicKeyCredential) {
40
+ setError(msgStr("webauthn-unsupported-browser-text"));
41
+ submitForm();
42
+ return;
43
+ }
44
+
45
+ const publicKey: PublicKeyCredentialRequestOptions = {
46
+ rpId,
47
+ challenge: base64url.parse(challenge, { loose: true })
48
+ };
49
+
50
+ if (createTimeout !== 0) {
51
+ publicKey.timeout = createTimeout * 1000;
52
+ }
53
+
54
+ if (allowCredentials.length) {
55
+ publicKey.allowCredentials = allowCredentials;
56
+ }
57
+
58
+ if (userVerification !== "not specified") {
59
+ publicKey.userVerification = userVerification;
60
+ }
61
+
62
+ try {
63
+ const resultRaw = await navigator.credentials.get({ publicKey });
64
+ if (!resultRaw || resultRaw.type != "public-key") return;
65
+ const result = resultRaw as PublicKeyCredential;
66
+ if (!("authenticatorData" in result.response)) return;
67
+ const response = result.response as AuthenticatorAssertionResponse;
68
+ const clientDataJSON = response.clientDataJSON;
69
+ const authenticatorData = response.authenticatorData;
70
+ const signature = response.signature;
71
+
72
+ setClientDataJSON(base64url.stringify(new Uint8Array(clientDataJSON), { pad: false }));
73
+ setAuthenticatorData(base64url.stringify(new Uint8Array(authenticatorData), { pad: false }));
74
+ setSignature(base64url.stringify(new Uint8Array(signature), { pad: false }));
75
+ setCredentialId(result.id);
76
+ setUserHandle(base64url.stringify(new Uint8Array(response.userHandle!), { pad: false }));
77
+ submitForm();
78
+ } catch (err) {
79
+ setError(String(err));
80
+ submitForm();
81
+ }
82
+ });
83
+
84
+ const webAuthForm = useRef<HTMLFormElement>(null);
85
+ const submitForm = useConstCallback(() => {
86
+ webAuthForm.current!.submit();
87
+ });
88
+
89
+ const [clientDataJSON, setClientDataJSON] = useState("");
90
+ const [authenticatorData, setAuthenticatorData] = useState("");
91
+ const [signature, setSignature] = useState("");
92
+ const [credentialId, setCredentialId] = useState("");
93
+ const [userHandle, setUserHandle] = useState("");
94
+ const [error, setError] = useState("");
95
+
96
+ return (
97
+ <Template
98
+ {...{ kcContext, i18n, doFetchDefaultThemeResources, ...props }}
99
+ headerNode={msg("webauthn-login-title")}
100
+ formNode={
101
+ <div id="kc-form-webauthn" className={cx(props.kcFormClass)}>
102
+ <form id="webauth" action={url.loginAction} ref={webAuthForm} method="post">
103
+ <input type="hidden" id="clientDataJSON" name="clientDataJSON" value={clientDataJSON} />
104
+ <input type="hidden" id="authenticatorData" name="authenticatorData" value={authenticatorData} />
105
+ <input type="hidden" id="signature" name="signature" value={signature} />
106
+ <input type="hidden" id="credentialId" name="credentialId" value={credentialId} />
107
+ <input type="hidden" id="userHandle" name="userHandle" value={userHandle} />
108
+ <input type="hidden" id="error" name="error" value={error} />
109
+ </form>
110
+ <div className={cx(props.kcFormGroupClass)}>
111
+ {authenticators &&
112
+ (() => (
113
+ <form id="authn_select" className={cx(props.kcFormClass)}>
114
+ {authenticators.authenticators.map(authenticator => (
115
+ <input
116
+ type="hidden"
117
+ name="authn_use_chk"
118
+ value={authenticator.credentialId}
119
+ key={authenticator.credentialId}
120
+ />
121
+ ))}
122
+ </form>
123
+ ))()}
124
+ {authenticators &&
125
+ shouldDisplayAuthenticators &&
126
+ (() => (
127
+ <>
128
+ {authenticators.authenticators.length > 1 && (
129
+ <p className={cx(props.kcSelectAuthListItemTitle)}>{msg("webauthn-available-authenticators")}</p>
130
+ )}
131
+ <div className={cx(props.kcFormClass)}>
132
+ {authenticators.authenticators.map(authenticator => (
133
+ <div id="kc-webauthn-authenticator" className={cx(props.kcSelectAuthListItemClass)}>
134
+ <div className={cx(props.kcSelectAuthListItemIconClass)}>
135
+ <i
136
+ className={cx(
137
+ props[authenticator.transports.iconClass] ?? props.kcWebAuthnDefaultIcon,
138
+ props.kcSelectAuthListItemIconPropertyClass
139
+ )}
140
+ />
141
+ </div>
142
+ <div className={cx(props.kcSelectAuthListItemBodyClass)}>
143
+ <div
144
+ id="kc-webauthn-authenticator-label"
145
+ className={cx(props.kcSelectAuthListItemHeadingClass)}
146
+ >
147
+ {authenticator.label}
148
+ </div>
149
+
150
+ {authenticator.transports && authenticator.transports.displayNameProperties.length && (
151
+ <div
152
+ id="kc-webauthn-authenticator-transport"
153
+ className={cx(props.kcSelectAuthListItemDescriptionClass)}
154
+ >
155
+ {authenticator.transports.displayNameProperties.map(
156
+ (transport: MessageKeyBase, index: number) => (
157
+ <>
158
+ <span>{msg(transport)}</span>
159
+ {index < authenticator.transports.displayNameProperties.length - 1 && (
160
+ <span>{", "}</span>
161
+ )}
162
+ </>
163
+ )
164
+ )}
165
+ </div>
166
+ )}
167
+
168
+ <div className={cx(props.kcSelectAuthListItemDescriptionClass)}>
169
+ <span id="kc-webauthn-authenticator-created-label">
170
+ {msg("webauthn-createdAt-label")}
171
+ </span>
172
+ <span id="kc-webauthn-authenticator-created">{authenticator.createdAt}</span>
173
+ </div>
174
+ </div>
175
+ <div className={cx(props.kcSelectAuthListItemFillClass)} />
176
+ </div>
177
+ ))}
178
+ </div>
179
+ </>
180
+ ))()}
181
+ <div id="kc-form-buttons" className={cx(props.kcFormButtonsClass)}>
182
+ <input
183
+ id="authenticateWebAuthnButton"
184
+ type="button"
185
+ onClick={webAuthnAuthenticate}
186
+ autoFocus={true}
187
+ value={msgStr("webauthn-doAuthenticate")}
188
+ className={cx(
189
+ props.kcButtonClass,
190
+ props.kcButtonPrimaryClass,
191
+ props.kcButtonBlockClass,
192
+ props.kcButtonLargeClass
193
+ )}
194
+ />
195
+ </div>
196
+ </div>
197
+ </div>
198
+ }
199
+ />
200
+ );
201
+ }
202
+ );
203
+
204
+ export default WebauthnAuthenticate;
@@ -2,6 +2,7 @@ import type { PageId } from "../../bin/keycloakify/generateFtl";
2
2
  import { assert } from "tsafe/assert";
3
3
  import type { Equals } from "tsafe";
4
4
  import type { MessageKeyBase } from "../i18n";
5
+ import type { KcTemplateClassKey } from "../components/KcProps";
5
6
 
6
7
  type ExtractAfterStartingWith<Prefix extends string, StrEnum> = StrEnum extends `${Prefix}${infer U}` ? U : never;
7
8
 
@@ -20,6 +21,7 @@ export type KcContextBase =
20
21
  | KcContextBase.Terms
21
22
  | KcContextBase.LoginOtp
22
23
  | KcContextBase.LoginUsername
24
+ | KcContextBase.WebauthnAuthenticate
23
25
  | KcContextBase.LoginPassword
24
26
  | KcContextBase.LoginUpdatePassword
25
27
  | KcContextBase.LoginUpdateProfile
@@ -31,6 +33,16 @@ export type KcContextBase =
31
33
  | KcContextBase.UpdateUserProfile
32
34
  | KcContextBase.IdpReviewUserProfile;
33
35
 
36
+ export type WebauthnAuthenticator = {
37
+ credentialId: string;
38
+ transports: {
39
+ iconClass: KcTemplateClassKey;
40
+ displayNameProperties: MessageKeyBase[];
41
+ };
42
+ label: string;
43
+ createdAt: string;
44
+ };
45
+
34
46
  export declare namespace KcContextBase {
35
47
  export type Common = {
36
48
  url: {
@@ -253,6 +265,24 @@ export declare namespace KcContextBase {
253
265
  };
254
266
  };
255
267
 
268
+ export type WebauthnAuthenticate = Common & {
269
+ pageId: "webauthn-authenticate.ftl";
270
+ authenticators: {
271
+ authenticators: WebauthnAuthenticator[];
272
+ };
273
+ challenge: string;
274
+ // I hate this:
275
+ userVerification: UserVerificationRequirement | "not specified";
276
+ rpId: string;
277
+ createTimeout: string;
278
+ isUserIdentified: "true" | "false";
279
+ shouldDisplayAuthenticators: boolean;
280
+ social: {
281
+ displayInfo: boolean;
282
+ };
283
+ login: {};
284
+ };
285
+
256
286
  export type LoginUpdatePassword = Common & {
257
287
  pageId: "login-update-password.ftl";
258
288
  username: string;
@@ -393,6 +393,27 @@ export const kcContextMocks: KcContextBase[] = [
393
393
  },
394
394
  "login": {}
395
395
  }),
396
+ id<KcContextBase.WebauthnAuthenticate>({
397
+ ...kcContextCommonMock,
398
+ "pageId": "webauthn-authenticate.ftl",
399
+ "url": loginUrl,
400
+ "authenticators": {
401
+ "authenticators": []
402
+ },
403
+ "realm": {
404
+ ...kcContextCommonMock.realm
405
+ },
406
+ "challenge": "",
407
+ "userVerification": "not specified",
408
+ "rpId": "",
409
+ "createTimeout": "0",
410
+ "isUserIdentified": "false",
411
+ "shouldDisplayAuthenticators": false,
412
+ "social": {
413
+ "displayInfo": false
414
+ },
415
+ "login": {}
416
+ }),
396
417
  id<KcContextBase.LoginUpdatePassword>({
397
418
  ...kcContextCommonMock,
398
419
  "pageId": "login-update-password.ftl",