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/README.md +4 -0
- package/bin/keycloakify/generateFtl/generateFtl.d.ts +1 -1
- package/bin/keycloakify/generateFtl/generateFtl.js +1 -0
- package/bin/keycloakify/generateFtl/generateFtl.js.map +1 -1
- package/bin/tsconfig.tsbuildinfo +1 -1
- package/lib/components/KcApp.js +3 -0
- package/lib/components/KcApp.js.map +1 -1
- package/lib/components/KcProps.d.ts +8 -3
- package/lib/components/KcProps.js +2 -2
- package/lib/components/KcProps.js.map +1 -1
- package/lib/components/WebauthnAuthenticate.d.ts +10 -0
- package/lib/components/WebauthnAuthenticate.js +119 -0
- package/lib/components/WebauthnAuthenticate.js.map +1 -0
- package/lib/getKcContext/KcContextBase.d.ts +27 -1
- package/lib/getKcContext/KcContextBase.js.map +1 -1
- package/lib/getKcContext/kcContextMocks/kcContextMocks.js +5 -0
- package/lib/getKcContext/kcContextMocks/kcContextMocks.js.map +1 -1
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +9 -4
- package/src/bin/keycloakify/generateFtl/generateFtl.ts +1 -0
- package/src/lib/components/KcApp.tsx +3 -0
- package/src/lib/components/KcProps.ts +10 -0
- package/src/lib/components/WebauthnAuthenticate.tsx +204 -0
- package/src/lib/getKcContext/KcContextBase.ts +30 -0
- package/src/lib/getKcContext/kcContextMocks/kcContextMocks.ts +21 -0
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "keycloakify",
|
3
|
-
"version": "6.
|
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.
|
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.
|
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.
|
1302
|
+
"tsafe": "^1.1.2",
|
1298
1303
|
"tss-react": "^4.3.4",
|
1299
1304
|
"zod": "^3.17.10"
|
1300
1305
|
}
|
@@ -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",
|