Add webauthn-register.ftl page

This commit is contained in:
Joseph Garrone 2024-05-10 21:12:35 +02:00
parent 5cfb289736
commit 652643f189
5 changed files with 309 additions and 1 deletions

View File

@ -3,6 +3,7 @@ export const loginThemePageIds = [
"login-username.ftl",
"login-password.ftl",
"webauthn-authenticate.ftl",
"webauthn-register.ftl",
"register.ftl",
"register-user-profile.ftl",
"info.ftl",

View File

@ -19,6 +19,7 @@ const LoginOtp = lazy(() => import("keycloakify/login/pages/LoginOtp"));
const LoginPassword = lazy(() => import("keycloakify/login/pages/LoginPassword"));
const LoginUsername = lazy(() => import("keycloakify/login/pages/LoginUsername"));
const WebauthnAuthenticate = lazy(() => import("keycloakify/login/pages/WebauthnAuthenticate"));
const WebauthnRegister = lazy(() => import("keycloakify/login/pages/WebauthnRegister"));
const LoginUpdatePassword = lazy(() => import("keycloakify/login/pages/LoginUpdatePassword"));
const LoginUpdateProfile = lazy(() => import("keycloakify/login/pages/LoginUpdateProfile"));
const LoginIdpLinkConfirm = lazy(() => import("keycloakify/login/pages/LoginIdpLinkConfirm"));
@ -70,6 +71,8 @@ export default function Fallback(props: FallbackProps) {
return <LoginPassword kcContext={kcContext} {...rest} />;
case "webauthn-authenticate.ftl":
return <WebauthnAuthenticate kcContext={kcContext} {...rest} />;
case "webauthn-register.ftl":
return <WebauthnRegister kcContext={kcContext} {...rest} />;
case "login-update-password.ftl":
return <LoginUpdatePassword kcContext={kcContext} {...rest} />;
case "login-update-profile.ftl":

View File

@ -23,6 +23,7 @@ export type KcContext =
| KcContext.LoginOtp
| KcContext.LoginUsername
| KcContext.WebauthnAuthenticate
| KcContext.WebauthnRegister
| KcContext.LoginPassword
| KcContext.LoginUpdatePassword
| KcContext.LoginUpdateProfile
@ -362,6 +363,24 @@ export declare namespace KcContext {
};
}
export type WebauthnRegister = Common & {
pageId: "webauthn-register.ftl";
challenge: string;
userid: string;
username: string;
signatureAlgorithms: string[];
rpEntityName: string;
rpId: string;
attestationConveyancePreference: string;
authenticatorAttachment: string;
requireResidentKey: string;
userVerificationRequirement: string;
createTimeout: string;
excludeCredentialIds: string;
isSetRetry?: boolean;
isAppInitiatedAction?: boolean;
};
export type LoginUpdatePassword = Common & {
pageId: "login-update-password.ftl";
username: string;

View File

@ -235,7 +235,7 @@ export default function WebauthnAuthenticate(props: PageProps<Extract<KcContext,
type="button"
onClick={() => {
assert("webAuthnAuthenticate" in window);
assert(window.webAuthnAuthenticate instanceof Function);
assert(typeof window.webAuthnAuthenticate === "function");
window.webAuthnAuthenticate();
}}
autoFocus

View File

@ -0,0 +1,285 @@
import { useEffect } from "react";
import { clsx } from "keycloakify/tools/clsx";
import type { PageProps } from "keycloakify/login/pages/PageProps";
import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
import { assert } from "tsafe/assert";
import { createUseInsertScriptTags } from "keycloakify/tools/useInsertScriptTags";
import type { KcContext } from "../kcContext";
import type { I18n } from "../i18n";
const { useInsertScriptTags } = createUseInsertScriptTags();
export default function WebauthnRegister(props: PageProps<Extract<KcContext, { pageId: "webauthn-register.ftl" }>, I18n>) {
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
const { getClassName } = useGetClassName({ doUseDefaultCss, classes });
const {
url,
challenge,
userid,
username,
signatureAlgorithms,
rpEntityName,
rpId,
attestationConveyancePreference,
authenticatorAttachment,
requireResidentKey,
userVerificationRequirement,
createTimeout,
excludeCredentialIds,
isSetRetry,
isAppInitiatedAction
} = kcContext;
const { msg, msgStr } = i18n;
const { insertScriptTags } = useInsertScriptTags({
"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": `
function registerSecurityKey() {
// Check if WebAuthn is supported by this browser
if (!window.PublicKeyCredential) {
$("#error").val("${msgStr("webauthn-unsupported-browser-text")}");
$("#register").submit();
return;
}
// mandatory parameters
let challenge = "${challenge}";
let userid = "${userid}";
let username = "${username}";
let signatureAlgorithms =${JSON.stringify(signatureAlgorithms)};
let pubKeyCredParams = getPubKeyCredParams(signatureAlgorithms);
let rpEntityName = "${rpEntityName}";
let rp = {name: rpEntityName};
let publicKey = {
challenge: base64url.decode(challenge, {loose: true}),
rp: rp,
user: {
id: base64url.decode(userid, {loose: true}),
name: username,
displayName: username
},
pubKeyCredParams: pubKeyCredParams,
};
// optional parameters
let rpId = "${rpId}";
publicKey.rp.id = rpId;
let attestationConveyancePreference = "${attestationConveyancePreference}";
if (attestationConveyancePreference !== 'not specified') publicKey.attestation = attestationConveyancePreference;
let authenticatorSelection = {};
let isAuthenticatorSelectionSpecified = false;
let authenticatorAttachment = "${authenticatorAttachment}";
if (authenticatorAttachment !== 'not specified') {
authenticatorSelection.authenticatorAttachment = authenticatorAttachment;
isAuthenticatorSelectionSpecified = true;
}
let requireResidentKey = "${requireResidentKey}";
if (requireResidentKey !== 'not specified') {
if (requireResidentKey === 'Yes')
authenticatorSelection.requireResidentKey = true;
else
authenticatorSelection.requireResidentKey = false;
isAuthenticatorSelectionSpecified = true;
}
let userVerificationRequirement = "${userVerificationRequirement}";
if (userVerificationRequirement !== 'not specified') {
authenticatorSelection.userVerification = userVerificationRequirement;
isAuthenticatorSelectionSpecified = true;
}
if (isAuthenticatorSelectionSpecified) publicKey.authenticatorSelection = authenticatorSelection;
let createTimeout = ${createTimeout};
if (createTimeout !== 0) publicKey.timeout = createTimeout * 1000;
let excludeCredentialIds = "${excludeCredentialIds}";
let excludeCredentials = getExcludeCredentials(excludeCredentialIds);
if (excludeCredentials.length > 0) publicKey.excludeCredentials = excludeCredentials;
navigator.credentials.create({publicKey})
.then(function (result) {
window.result = result;
let clientDataJSON = result.response.clientDataJSON;
let attestationObject = result.response.attestationObject;
let publicKeyCredentialId = result.rawId;
$("#clientDataJSON").val(base64url.encode(new Uint8Array(clientDataJSON), {pad: false}));
$("#attestationObject").val(base64url.encode(new Uint8Array(attestationObject), {pad: false}));
$("#publicKeyCredentialId").val(base64url.encode(new Uint8Array(publicKeyCredentialId), {pad: false}));
if (typeof result.response.getTransports === "function") {
let transports = result.response.getTransports();
if (transports) {
$("#transports").val(getTransportsAsString(transports));
}
} else {
console.log("Your browser is not able to recognize supported transport media for the authenticator.");
}
let initLabel = "WebAuthn Authenticator (Default Label)";
let labelResult = window.prompt("Please input your registered authenticator's label", initLabel);
if (labelResult === null) labelResult = initLabel;
$("#authenticatorLabel").val(labelResult);
$("#register").submit();
})
.catch(function (err) {
$("#error").val(err);
$("#register").submit();
});
}
function getPubKeyCredParams(signatureAlgorithmsList) {
let pubKeyCredParams = [];
if (signatureAlgorithmsList === []) {
pubKeyCredParams.push({type: "public-key", alg: -7});
return pubKeyCredParams;
}
for (let i = 0; i < signatureAlgorithmsList.length; i++) {
pubKeyCredParams.push({
type: "public-key",
alg: signatureAlgorithmsList[i]
});
}
return pubKeyCredParams;
}
function getExcludeCredentials(excludeCredentialIds) {
let excludeCredentials = [];
if (excludeCredentialIds === "") return excludeCredentials;
let excludeCredentialIdsList = excludeCredentialIds.split(',');
for (let i = 0; i < excludeCredentialIdsList.length; i++) {
excludeCredentials.push({
type: "public-key",
id: base64url.decode(excludeCredentialIdsList[i],
{loose: true})
});
}
return excludeCredentials;
}
function getTransportsAsString(transportsList) {
if (transportsList === '' || transportsList.constructor !== Array) return "";
let transportsString = "";
for (let i = 0; i < transportsList.length; i++) {
transportsString += transportsList[i] + ",";
}
return transportsString.slice(0, -1);
}
`
}
]
});
useEffect(() => {
insertScriptTags();
}, []);
return (
<Template
{...{ kcContext, i18n, doUseDefaultCss, classes }}
headerNode={
<>
<span className={getClassName("kcWebAuthnKeyIcon")} />
{msg("webauthn-registration-title")}
</>
}
>
<form id="register" className={getClassName("kcFormClass")} action={url.loginAction} method="post">
<div className={getClassName("kcFormGroupClass")}>
<input type="hidden" id="clientDataJSON" name="clientDataJSON" />
<input type="hidden" id="attestationObject" name="attestationObject" />
<input type="hidden" id="publicKeyCredentialId" name="publicKeyCredentialId" />
<input type="hidden" id="authenticatorLabel" name="authenticatorLabel" />
<input type="hidden" id="transports" name="transports" />
<input type="hidden" id="error" name="error" />
<LogoutOtherSessions {...{ i18n, getClassName }} />
</div>
</form>
<input
type="submit"
className={clsx(
getClassName("kcButtonClass"),
getClassName("kcButtonPrimaryClass"),
getClassName("kcButtonBlockClass"),
getClassName("kcButtonLargeClass")
)}
id="registerWebAuthn"
value={msgStr("doRegisterSecurityKey")}
onClick={() => {
assert("registerSecurityKey" in window);
assert(typeof window.registerSecurityKey === "function");
window.registerSecurityKey();
}}
/>
{!isSetRetry && isAppInitiatedAction && (
<form action={url.loginAction} className={getClassName("kcFormClass")} id="kc-webauthn-settings-form" method="post">
<button
type="submit"
className={clsx(
getClassName("kcButtonClass"),
getClassName("kcButtonDefaultClass"),
getClassName("kcButtonBlockClass"),
getClassName("kcButtonLargeClass")
)}
id="cancelWebAuthnAIA"
name="cancel-aia"
value="true"
>
{msg("doCancel")}
</button>
</form>
)}
</Template>
);
}
function LogoutOtherSessions(props: { i18n: I18n; getClassName: ReturnType<typeof useGetClassName>["getClassName"] }) {
const { getClassName, i18n } = props;
const { msg } = i18n;
return (
<div id="kc-form-options" className={getClassName("kcFormOptionsClass")}>
<div className={getClassName("kcFormOptionsWrapperClass")}>
<div className="checkbox">
<label>
<input type="checkbox" id="logout-sessions" name="logout-sessions" value="on" checked />
{msg("logoutOtherSessions")}
</label>
</div>
</div>
</div>
);
}