From 1d87e8fe8b16902626b719aa1198d1252fa7c0d8 Mon Sep 17 00:00:00 2001 From: Joseph Garrone Date: Fri, 10 May 2024 18:30:48 +0200 Subject: [PATCH] update webauthn-autenticate.ftl --- src/login/kcContext/KcContext.ts | 10 +- src/login/pages/Login.tsx | 26 +- src/login/pages/WebauthnAuthenticate.tsx | 364 +++++++++++++---------- 3 files changed, 222 insertions(+), 178 deletions(-) diff --git a/src/login/kcContext/KcContext.ts b/src/login/kcContext/KcContext.ts index 8e85db6d..59840222 100644 --- a/src/login/kcContext/KcContext.ts +++ b/src/login/kcContext/KcContext.ts @@ -340,6 +340,14 @@ export declare namespace KcContext { displayInfo: boolean; }; login: {}; + realm: { + password: boolean; + registrationAllowed: boolean; + }; + registrationDisabled?: boolean; + url: { + registrationUrl?: string; + }; }; export namespace WebauthnAuthenticate { @@ -347,7 +355,7 @@ export declare namespace KcContext { credentialId: string; transports: { iconClass: string; - displayNameProperties: MessageKey[]; + displayNameProperties?: MessageKey[]; }; label: string; createdAt: string; diff --git a/src/login/pages/Login.tsx b/src/login/pages/Login.tsx index 89975e6e..e540d03b 100644 --- a/src/login/pages/Login.tsx +++ b/src/login/pages/Login.tsx @@ -25,23 +25,19 @@ export default function Login(props: PageProps - {realm.password && realm.registrationAllowed && !registrationDisabled && ( -
-
- - {msg("noAccount")}{" "} - - {msg("doRegister")} - - -
-
- )} - +
+
+ + {msg("noAccount")}{" "} + + {msg("doRegister")} + + +
+
} socialProvidersNode={ <> diff --git a/src/login/pages/WebauthnAuthenticate.tsx b/src/login/pages/WebauthnAuthenticate.tsx index 068de835..a8a126b8 100644 --- a/src/login/pages/WebauthnAuthenticate.tsx +++ b/src/login/pages/WebauthnAuthenticate.tsx @@ -1,204 +1,244 @@ -import { useRef, useState } from "react"; +import { useEffect, Fragment } from "react"; import { clsx } from "keycloakify/tools/clsx"; import type { MessageKey } from "keycloakify/login/i18n/i18n"; -import { base64url } from "rfc4648"; -import { useConstCallback } from "keycloakify/tools/useConstCallback"; 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"; -import { assert } from "tsafe/assert"; -import { is } from "tsafe/is"; -import { typeGuard } from "tsafe/typeGuard"; + +const { useInsertScriptTags } = createUseInsertScriptTags(); export default function WebauthnAuthenticate(props: PageProps, I18n>) { const { kcContext, i18n, doUseDefaultCss, Template, classes } = props; const { getClassName } = useGetClassName({ doUseDefaultCss, classes }); - const { url } = kcContext; + const { + url, + isUserIdentified, + challenge, + userVerification, + rpId, + createTimeout, + messagesPerField, + realm, + registrationDisabled, + authenticators, + shouldDisplayAuthenticators + } = kcContext; const { msg, msgStr } = i18n; - const { authenticators, challenge, shouldDisplayAuthenticators, userVerification, rpId } = kcContext; - const createTimeout = Number(kcContext.createTimeout); - const isUserIdentified = kcContext.isUserIdentified == "true"; + 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": ` - const formElementRef = useRef(null); + function webAuthnAuthenticate() { + let isUserIdentified = ${isUserIdentified}; + if (!isUserIdentified) { + doAuthenticate([]); + return; + } + checkAllowCredentials(); + } - const webAuthnAuthenticate = useConstCallback(async () => { - if (!isUserIdentified) { - return; - } + 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); + } - const submitForm = async (): Promise => { - const formElement = formElementRef.current; - if (formElement === null) { - await new Promise(resolve => setTimeout(resolve, 100)); - return submitForm(); + 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(); + }) + ; + } + + ` } - - formElement.submit(); - }; - - const allowCredentials = authenticators.authenticators.map( - authenticator => - ({ - id: base64url.parse(authenticator.credentialId, { loose: true }), - type: "public-key" - } as PublicKeyCredentialDescriptor) - ); - // Check if WebAuthn is supported by this browser - if (!window.PublicKeyCredential) { - setError(msgStr("webauthn-unsupported-browser-text")); - submitForm(); - return; - } - - const publicKey: PublicKeyCredentialRequestOptions = { - rpId, - challenge: base64url.parse(challenge, { loose: true }) - }; - - if (createTimeout !== 0) { - publicKey.timeout = createTimeout * 1000; - } - - if (allowCredentials.length) { - publicKey.allowCredentials = allowCredentials; - } - - if (userVerification !== "not specified") { - publicKey.userVerification = userVerification; - } - - try { - const result = await navigator.credentials.get({ publicKey }); - if (!result || result.type != "public-key") { - return; - } - assert(is(result)); - if (!("authenticatorData" in result.response)) { - return; - } - const response = result.response; - - const clientDataJSON = response.clientDataJSON; - - assert( - typeGuard(response, "signature" in response && response.authenticatorData instanceof ArrayBuffer), - "response not an AuthenticatorAssertionResponse" - ); - - const authenticatorData = response.authenticatorData; - const signature = response.signature; - - setClientDataJSON(base64url.stringify(new Uint8Array(clientDataJSON), { "pad": false })); - setAuthenticatorData(base64url.stringify(new Uint8Array(authenticatorData), { "pad": false })); - setSignature(base64url.stringify(new Uint8Array(signature), { "pad": false })); - setCredentialId(result.id); - setUserHandle(base64url.stringify(new Uint8Array(response.userHandle!), { "pad": false })); - } catch (err) { - setError(String(err)); - } - - submitForm(); + ] }); - const [clientDataJSON, setClientDataJSON] = useState(""); - const [authenticatorData, setAuthenticatorData] = useState(""); - const [signature, setSignature] = useState(""); - const [credentialId, setCredentialId] = useState(""); - const [userHandle, setUserHandle] = useState(""); - const [error, setError] = useState(""); + useEffect(() => { + insertScriptTags(); + }, []); return ( -