Complete rework of WebauthnRegister

This commit is contained in:
Joseph Garrone 2024-09-10 09:57:47 +02:00
parent 72c31776d7
commit 50e38b6a10
5 changed files with 107 additions and 202 deletions

View File

@ -345,8 +345,7 @@ export const kcContextMocks = [
rpId: "",
createTimeout: "0",
isUserIdentified: "false",
shouldDisplayAuthenticators: false,
login: {}
shouldDisplayAuthenticators: false
}),
id<KcContext.LoginUpdatePassword>({
...kcContextCommonMock,

View File

@ -11,7 +11,7 @@ type KcContextLike = {
challenge: string;
userVerification: string;
rpId: string;
createTimeout: number;
createTimeout: number | string;
};
assert<keyof KcContextLike extends keyof KcContext.LoginPasskeysConditionalAuthenticate ? true : false>();
@ -41,9 +41,9 @@ export function useScript(params: { authButtonId: string; kcContext: KcContextLi
const authButton = document.getElementById("${authButtonId}");
const input = {
isUserIdentified : ${isUserIdentified},
challenge : '${challenge}',
userVerification : '${userVerification}',
rpId : '${rpId}',
challenge : ${JSON.stringify(challenge)},
userVerification : ${JSON.stringify(userVerification)},
rpId : ${JSON.stringify(rpId)},
createTimeout : ${createTimeout}
};
authButton.addEventListener("click", () => {

View File

@ -30,7 +30,7 @@ export function useScript(params: { authButtonId: string; kcContext: KcContextLi
const { msgStr, isFetchingTranslations } = i18n;
const { insertScriptTags } = useInsertScriptTags({
componentOrHookName: "LoginRecoveryAuthnCodeConfig",
componentOrHookName: "WebauthnAuthenticate",
scriptTags: [
{
type: "module",

View File

@ -1,7 +1,5 @@
import { useEffect } from "react";
import { assert } from "keycloakify/tools/assert";
import { getKcClsx, type KcClsx } from "keycloakify/login/lib/kcClsx";
import { useInsertScriptTags } from "keycloakify/tools/useInsertScriptTags";
import { useScript } from "keycloakify/login/pages/WebauthnRegister.useScript";
import type { PageProps } from "keycloakify/login/pages/PageProps";
import type { KcContext } from "../KcContext";
import type { I18n } from "../i18n";
@ -11,198 +9,18 @@ export default function WebauthnRegister(props: PageProps<Extract<KcContext, { p
const { kcClsx } = getKcClsx({ doUseDefaultCss, classes });
const {
url,
challenge,
userid,
username,
signatureAlgorithms,
rpEntityName,
rpId,
attestationConveyancePreference,
authenticatorAttachment,
requireResidentKey,
userVerificationRequirement,
createTimeout,
excludeCredentialIds,
isSetRetry,
isAppInitiatedAction
} = kcContext;
const { url, isSetRetry, isAppInitiatedAction } = kcContext;
const { msg, msgStr } = i18n;
const { insertScriptTags } = useInsertScriptTags({
componentOrHookName: "WebauthnRegister",
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() {
const authButtonId = "authenticateWebAuthnButton";
// 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.length === 0) {
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 === '' || Array.isArray(transportsList)) return "";
let transportsString = "";
for (let i = 0; i < transportsList.length; i++) {
transportsString += transportsList[i] + ",";
}
return transportsString.slice(0, -1);
}
`
}
]
useScript({
authButtonId,
kcContext,
i18n
});
useEffect(() => {
insertScriptTags();
}, []);
return (
<Template
kcContext={kcContext}
@ -230,13 +48,8 @@ export default function WebauthnRegister(props: PageProps<Extract<KcContext, { p
<input
type="submit"
className={kcClsx("kcButtonClass", "kcButtonPrimaryClass", "kcButtonBlockClass", "kcButtonLargeClass")}
id="registerWebAuthn"
id={authButtonId}
value={msgStr("doRegisterSecurityKey")}
onClick={() => {
assert("registerSecurityKey" in window);
assert(typeof window.registerSecurityKey === "function");
window.registerSecurityKey();
}}
/>
{!isSetRetry && isAppInitiatedAction && (

View File

@ -0,0 +1,93 @@
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;
};
challenge: string;
userid: string;
username: string;
signatureAlgorithms: string[];
rpEntityName: string;
rpId: string;
attestationConveyancePreference: string;
authenticatorAttachment: string;
requireResidentKey: string;
userVerificationRequirement: string;
createTimeout: number | string;
excludeCredentialIds: string;
};
assert<keyof KcContextLike extends keyof KcContext.WebauthnRegister ? true : false>();
assert<KcContext.WebauthnRegister extends KcContextLike ? true : false>();
type I18nLike = {
msgStr: (key: "webauthn-registration-init-label" | "webauthn-registration-init-label-prompt" | "webauthn-unsupported-browser-text") => string;
isFetchingTranslations: boolean;
};
export function useScript(params: { authButtonId: string; kcContext: KcContextLike; i18n: I18nLike }) {
const { authButtonId, kcContext, i18n } = params;
const {
url,
challenge,
userid,
username,
signatureAlgorithms,
rpEntityName,
rpId,
attestationConveyancePreference,
authenticatorAttachment,
requireResidentKey,
userVerificationRequirement,
createTimeout,
excludeCredentialIds
} = kcContext;
const { msgStr, isFetchingTranslations } = i18n;
const { insertScriptTags } = useInsertScriptTags({
componentOrHookName: "LoginRecoveryAuthnCodeConfig",
scriptTags: [
{
type: "module",
textContent: () => `
import { registerByWebAuthn } from "${url.resourcesPath}/js/webauthnRegister.js";
const registerButton = document.getElementById('${authButtonId}');
registerButton.addEventListener("click", function() {
const input = {
challenge : '${challenge}',
userid : '${userid}',
username : '${username}',
signatureAlgorithms : ${JSON.stringify(signatureAlgorithms)},
rpEntityName : ${JSON.stringify(rpEntityName)},
rpId : ${JSON.stringify(rpId)},
attestationConveyancePreference : ${JSON.stringify(attestationConveyancePreference)},
authenticatorAttachment : ${JSON.stringify(authenticatorAttachment)},
requireResidentKey : ${JSON.stringify(requireResidentKey)},
userVerificationRequirement : ${JSON.stringify(userVerificationRequirement)},
createTimeout : ${createTimeout},
excludeCredentialIds : ${JSON.stringify(excludeCredentialIds)},
initLabel : ${JSON.stringify(msgStr("webauthn-registration-init-label"))},
initLabelPrompt : ${JSON.stringify(msgStr("webauthn-registration-init-label-prompt"))},
errmsg : ${JSON.stringify(msgStr("webauthn-unsupported-browser-text"))}
};
registerByWebAuthn(input);
});
`
}
]
});
useEffect(() => {
if (isFetchingTranslations) {
return;
}
insertScriptTags();
}, [isFetchingTranslations]);
}