"use client"; import { useEffect, useRef, useState } from "react"; import { useRouter } from "next/navigation"; import { coerceToArrayBuffer, coerceToBase64Url } from "#/utils/base64"; import { Button, ButtonVariants } from "./Button"; import Alert from "./Alert"; import { Spinner } from "./Spinner"; import { Checks } from "@zitadel/server"; // either loginName or sessionId must be provided type Props = { loginName?: string; sessionId?: string; authRequestId?: string; altPassword: boolean; login?: boolean; organization?: string; }; export default function LoginPasskey({ loginName, sessionId, authRequestId, altPassword, organization, login = true, }: Props) { const [error, setError] = useState(""); const [loading, setLoading] = useState(false); const router = useRouter(); const initialized = useRef(false); // TODO: move this to server side useEffect(() => { if (!initialized.current) { initialized.current = true; setLoading(true); updateSessionForChallenge() .then((response) => { const pK = response.challenges.webAuthN.publicKeyCredentialRequestOptions .publicKey; if (pK) { submitLoginAndContinue(pK) .then(() => { setLoading(false); }) .catch((error) => { setError(error); setLoading(false); }); } else { setError("Could not request passkey challenge"); setLoading(false); } }) .catch((error) => { setError(error); setLoading(false); }); } }, []); async function updateSessionForChallenge( userVerificationRequirement: number = login ? 1 : 3 ) { setLoading(true); const res = await fetch("/api/session", { method: "PUT", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ loginName, sessionId, organization, challenges: { webAuthN: { domain: "", // USER_VERIFICATION_REQUIREMENT_UNSPECIFIED = 0; // USER_VERIFICATION_REQUIREMENT_REQUIRED = 1; - passkey login // USER_VERIFICATION_REQUIREMENT_PREFERRED = 2; // USER_VERIFICATION_REQUIREMENT_DISCOURAGED = 3; - mfa userVerificationRequirement: userVerificationRequirement, }, }, authRequestId, }), }); setLoading(false); if (!res.ok) { const error = await res.json(); throw error.details.details; } return res.json(); } async function submitLogin(data: any) { setLoading(true); const res = await fetch("/api/session", { method: "PUT", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ loginName, sessionId, organization, checks: { webAuthN: { credentialAssertionData: data }, } as Checks, authRequestId, }), }); const response = await res.json(); setLoading(false); if (!res.ok) { setError(response.details); return Promise.reject(response.details); } return response; } async function submitLoginAndContinue( publicKey: any ): Promise { publicKey.challenge = coerceToArrayBuffer( publicKey.challenge, "publicKey.challenge" ); publicKey.allowCredentials.map((listItem: any) => { listItem.id = coerceToArrayBuffer( listItem.id, "publicKey.allowCredentials.id" ); }); navigator.credentials .get({ publicKey, }) .then((assertedCredential: any) => { if (assertedCredential) { const authData = new Uint8Array( assertedCredential.response.authenticatorData ); const clientDataJSON = new Uint8Array( assertedCredential.response.clientDataJSON ); const rawId = new Uint8Array(assertedCredential.rawId); const sig = new Uint8Array(assertedCredential.response.signature); const userHandle = new Uint8Array( assertedCredential.response.userHandle ); const data = { id: assertedCredential.id, rawId: coerceToBase64Url(rawId, "rawId"), type: assertedCredential.type, response: { authenticatorData: coerceToBase64Url(authData, "authData"), clientDataJSON: coerceToBase64Url( clientDataJSON, "clientDataJSON" ), signature: coerceToBase64Url(sig, "sig"), userHandle: coerceToBase64Url(userHandle, "userHandle"), }, }; return submitLogin(data).then((resp) => { if (authRequestId && resp && resp.sessionId) { return router.push( `/login?` + new URLSearchParams({ sessionId: resp.sessionId, authRequest: authRequestId, }) ); } else { return router.push( `/signedin?` + new URLSearchParams( authRequestId ? { loginName: resp.factors.user.loginName, authRequestId, } : { loginName: resp.factors.user.loginName, } ) ); } }); } else { setLoading(false); setError("An error on retrieving passkey"); return null; } }) .catch((error) => { console.error(error); setLoading(false); // setError(error); return null; }); } return (
{error && (
{error}
)}
{altPassword ? ( ) : ( )}
); }