import { useRef, useState } 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 type { KcContext } from "../kcContext"; import type { I18n } from "../i18n"; import { assert } from "tsafe/assert"; import { is } from "tsafe/is"; import { typeGuard } from "tsafe/typeGuard"; export default function WebauthnAuthenticate(props: PageProps, I18n>) { const { kcContext, i18n, doUseDefaultCss, Template, classes } = props; const { getClassName } = useGetClassName({ doUseDefaultCss, classes }); const { url } = kcContext; const { msg, msgStr } = i18n; const { authenticators, challenge, shouldDisplayAuthenticators, userVerification, rpId } = kcContext; const createTimeout = Number(kcContext.createTimeout); const isUserIdentified = kcContext.isUserIdentified == "true"; const formElementRef = useRef(null); const webAuthnAuthenticate = useConstCallback(async () => { if (!isUserIdentified) { return; } const submitForm = async (): Promise => { const formElement = formElementRef.current; if (formElement === null) { await new Promise(resolve => setTimeout(resolve, 100)); return submitForm(); } 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(""); return ( ); }