Update WebauthnAuthenticate

This commit is contained in:
Joseph Garrone
2024-09-09 08:49:59 +02:00
parent 7456750828
commit 72c31776d7
3 changed files with 87 additions and 137 deletions

View File

@ -357,10 +357,9 @@ export declare namespace KcContext {
// I hate this:
userVerification: UserVerificationRequirement | "not specified";
rpId: string;
createTimeout: string;
createTimeout: string | number;
isUserIdentified: "true" | "false";
shouldDisplayAuthenticators: boolean;
login: {};
realm: {
password: boolean;
registrationAllowed: boolean;
@ -395,7 +394,7 @@ export declare namespace KcContext {
authenticatorAttachment: string;
requireResidentKey: string;
userVerificationRequirement: string;
createTimeout: number;
createTimeout: number | string;
excludeCredentialIds: string;
isSetRetry?: boolean;
isAppInitiatedAction?: boolean;
@ -586,7 +585,7 @@ export declare namespace KcContext {
challenge: string;
userVerification: string;
rpId: string;
createTimeout: number;
createTimeout: number | string;
authenticators?: {
authenticators: WebauthnAuthenticate.WebauthnAuthenticator[];

View File

@ -1,8 +1,7 @@
import { useEffect, Fragment } from "react";
import { assert } from "keycloakify/tools/assert";
import { Fragment } from "react";
import { clsx } from "keycloakify/tools/clsx";
import { useInsertScriptTags } from "keycloakify/tools/useInsertScriptTags";
import { getKcClsx } from "keycloakify/login/lib/kcClsx";
import { useScript } from "keycloakify/login/pages/WebauthnAuthenticate.useScript";
import type { PageProps } from "keycloakify/login/pages/PageProps";
import type { KcContext } from "../KcContext";
import type { I18n } from "../i18n";
@ -12,136 +11,25 @@ export default function WebauthnAuthenticate(props: PageProps<Extract<KcContext,
const { kcClsx } = getKcClsx({ doUseDefaultCss, classes });
const {
url,
isUserIdentified,
challenge,
userVerification,
rpId,
createTimeout,
messagesPerField,
realm,
registrationDisabled,
authenticators,
shouldDisplayAuthenticators
} = kcContext;
const { url, realm, registrationDisabled, authenticators, shouldDisplayAuthenticators } = kcContext;
const { msg, msgStr, advancedMsg } = i18n;
const { insertScriptTags } = useInsertScriptTags({
componentOrHookName: "WebauthnAuthenticate",
scriptTags: [
{
type: "text/javascript",
src: `${url.resourcesCommonPath}/node_modules/jquery/dist/jquery.min.js`
},
{
type: "text/javascript",
src: `${url.resourcesPath}/js/base64url.js`
},
{
type: "text/javascript",
textContent: `
const authButtonId = "authenticateWebAuthnButton";
function webAuthnAuthenticate() {
let isUserIdentified = ${isUserIdentified};
if (!isUserIdentified) {
doAuthenticate([]);
return;
}
checkAllowCredentials();
}
function checkAllowCredentials() {
let allowCredentials = [];
let authn_use = document.forms['authn_select'].authn_use_chk;
if (authn_use !== undefined) {
if (authn_use.length === undefined) {
allowCredentials.push({
id: base64url.decode(authn_use.value, {loose: true}),
type: 'public-key',
});
} else {
for (let i = 0; i < authn_use.length; i++) {
allowCredentials.push({
id: base64url.decode(authn_use[i].value, {loose: true}),
type: 'public-key',
});
}
}
}
doAuthenticate(allowCredentials);
}
function doAuthenticate(allowCredentials) {
// Check if WebAuthn is supported by this browser
if (!window.PublicKeyCredential) {
$("#error").val("${msgStr("webauthn-unsupported-browser-text")}");
$("#webauth").submit();
return;
}
let challenge = "${challenge}";
let userVerification = "${userVerification}";
let rpId = "${rpId}";
let publicKey = {
rpId : rpId,
challenge: base64url.decode(challenge, { loose: true })
};
let createTimeout = ${createTimeout};
if (createTimeout !== 0) publicKey.timeout = createTimeout * 1000;
if (allowCredentials.length) {
publicKey.allowCredentials = allowCredentials;
}
if (userVerification !== 'not specified') publicKey.userVerification = userVerification;
navigator.credentials.get({publicKey})
.then((result) => {
window.result = result;
let clientDataJSON = result.response.clientDataJSON;
let authenticatorData = result.response.authenticatorData;
let signature = result.response.signature;
$("#clientDataJSON").val(base64url.encode(new Uint8Array(clientDataJSON), { pad: false }));
$("#authenticatorData").val(base64url.encode(new Uint8Array(authenticatorData), { pad: false }));
$("#signature").val(base64url.encode(new Uint8Array(signature), { pad: false }));
$("#credentialId").val(result.id);
if(result.response.userHandle) {
$("#userHandle").val(base64url.encode(new Uint8Array(result.response.userHandle), { pad: false }));
}
$("#webauth").submit();
})
.catch((err) => {
$("#error").val(err);
$("#webauth").submit();
})
;
}
`
}
]
useScript({
authButtonId,
kcContext,
i18n
});
useEffect(() => {
insertScriptTags();
}, []);
return (
<Template
kcContext={kcContext}
i18n={i18n}
doUseDefaultCss={doUseDefaultCss}
classes={classes}
displayMessage={!messagesPerField.existsError("username")}
displayInfo={realm.password && realm.registrationAllowed && !registrationDisabled}
displayInfo={realm.registrationAllowed && !registrationDisabled}
infoNode={
<div id="kc-registration">
<span>
@ -179,7 +67,7 @@ export default function WebauthnAuthenticate(props: PageProps<Extract<KcContext,
)}
<div className={kcClsx("kcFormOptionsClass")}>
{authenticators.authenticators.map((authenticator, i) => (
<div key={i} id="kc-webauthn-authenticator" className={kcClsx("kcSelectAuthListItemClass")}>
<div key={i} id={`kc-webauthn-authenticator-item-${i}`} className={kcClsx("kcSelectAuthListItemClass")}>
<div className={kcClsx("kcSelectAuthListItemIconClass")}>
<i
className={clsx(
@ -195,12 +83,15 @@ export default function WebauthnAuthenticate(props: PageProps<Extract<KcContext,
/>
</div>
<div className={kcClsx("kcSelectAuthListItemArrowIconClass")}>
<div id="kc-webauthn-authenticator-label" className={kcClsx("kcSelectAuthListItemHeadingClass")}>
<div
id={`kc-webauthn-authenticator-label-${i}`}
className={kcClsx("kcSelectAuthListItemHeadingClass")}
>
{advancedMsg(authenticator.label)}
</div>
{authenticator.transports.displayNameProperties?.length && (
<div
id="kc-webauthn-authenticator-transport"
id={`kc-webauthn-authenticator-transport-${i}`}
className={kcClsx("kcSelectAuthListItemDescriptionClass")}
>
{authenticator.transports.displayNameProperties
@ -217,8 +108,10 @@ export default function WebauthnAuthenticate(props: PageProps<Extract<KcContext,
</div>
)}
<div className={kcClsx("kcSelectAuthListItemDescriptionClass")}>
<span id="kc-webauthn-authenticator-created-label">{msg("webauthn-createdAt-label")}</span>
<span id="kc-webauthn-authenticator-created">{authenticator.createdAt}</span>
<span id={`kc-webauthn-authenticator-createdlabel-${i}`}>
{msg("webauthn-createdAt-label")}
</span>
<span id={`kc-webauthn-authenticator-created-${i}`}>{authenticator.createdAt}</span>
</div>
<div className={kcClsx("kcSelectAuthListItemFillClass")} />
</div>
@ -229,16 +122,10 @@ export default function WebauthnAuthenticate(props: PageProps<Extract<KcContext,
)}
</>
)}
<div id="kc-form-buttons" className={kcClsx("kcFormButtonsClass")}>
<input
id="authenticateWebAuthnButton"
id={authButtonId}
type="button"
onClick={() => {
assert("webAuthnAuthenticate" in window);
assert(typeof window.webAuthnAuthenticate === "function");
window.webAuthnAuthenticate();
}}
autoFocus
value={msgStr("webauthn-doAuthenticate")}
className={kcClsx("kcButtonClass", "kcButtonPrimaryClass", "kcButtonBlockClass", "kcButtonLargeClass")}

View File

@ -0,0 +1,64 @@
import { useEffect } from "react";
import { useInsertScriptTags } from "keycloakify/tools/useInsertScriptTags";
import { assert } from "keycloakify/tools/assert";
import { KcContext } from "keycloakify/login/KcContext/KcContext";
type KcContextLike = {
url: {
resourcesPath: string;
};
isUserIdentified: "true" | "false";
challenge: string;
userVerification: KcContext.WebauthnAuthenticate["userVerification"];
rpId: string;
createTimeout: number | string;
};
assert<keyof KcContextLike extends keyof KcContext.WebauthnAuthenticate ? true : false>();
assert<KcContext.WebauthnAuthenticate extends KcContextLike ? true : false>();
type I18nLike = {
msgStr: (key: "webauthn-unsupported-browser-text") => string;
isFetchingTranslations: boolean;
};
export function useScript(params: { authButtonId: string; kcContext: KcContextLike; i18n: I18nLike }) {
const { authButtonId, kcContext, i18n } = params;
const { url, isUserIdentified, challenge, userVerification, rpId, createTimeout } = kcContext;
const { msgStr, isFetchingTranslations } = i18n;
const { insertScriptTags } = useInsertScriptTags({
componentOrHookName: "LoginRecoveryAuthnCodeConfig",
scriptTags: [
{
type: "module",
textContent: () => `
import { authenticateByWebAuthn } from "${url.resourcesPath}/js/webauthnAuthenticate.js";
const authButton = document.getElementById('${authButtonId}');
authButton.addEventListener("click", function() {
const input = {
isUserIdentified : ${isUserIdentified},
challenge : '${challenge}',
userVerification : '${userVerification}',
rpId : '${rpId}',
createTimeout : ${createTimeout},
errmsg : ${JSON.stringify(msgStr("webauthn-unsupported-browser-text"))}
};
authenticateByWebAuthn(input);
});
`
}
]
});
useEffect(() => {
if (isFetchingTranslations) {
return;
}
insertScriptTags();
}, [isFetchingTranslations]);
}