From 8e23e2d583c7f5da504c6d2b0b3131c334fa0b16 Mon Sep 17 00:00:00 2001 From: peintnermax Date: Wed, 17 Apr 2024 15:58:32 +0200 Subject: [PATCH] provide domain for u2f --- apps/login/app/(login)/otp/[method]/page.tsx | 1 - apps/login/app/api/session/route.ts | 14 +- apps/login/ui/VerifyU2F.tsx | 259 ++++++++++++++++++- 3 files changed, 259 insertions(+), 15 deletions(-) diff --git a/apps/login/app/(login)/otp/[method]/page.tsx b/apps/login/app/(login)/otp/[method]/page.tsx index 6d87d070f26..157801e07b9 100644 --- a/apps/login/app/(login)/otp/[method]/page.tsx +++ b/apps/login/app/(login)/otp/[method]/page.tsx @@ -48,7 +48,6 @@ export default async function Page({ sessionId={sessionId} authRequestId={authRequestId} organization={organization} - submit={submit === "true"} > )} diff --git a/apps/login/app/api/session/route.ts b/apps/login/app/api/session/route.ts index 80f873c4d5d..0525520dd43 100644 --- a/apps/login/app/api/session/route.ts +++ b/apps/login/app/api/session/route.ts @@ -92,6 +92,10 @@ export async function PUT(request: NextRequest) { const domain: string = request.nextUrl.hostname; + if (challenges && challenges.webAuthN && !challenges.webAuthN.domain) { + challenges.webAuthN.domain = domain; + } + return recentPromise .then(async (recent) => { if ( @@ -140,15 +144,7 @@ export async function PUT(request: NextRequest) { authFactors = response.result; } } - if (challenges && challenges.o && session.factors?.user?.id) { - const response = await listHumanAuthFactors( - server, - session.factors?.user?.id - ); - if (response.result && response.result.length) { - authFactors = response.result; - } - } + return NextResponse.json({ sessionId: session.id, factors: session.factors, diff --git a/apps/login/ui/VerifyU2F.tsx b/apps/login/ui/VerifyU2F.tsx index cc38bbc88d6..ad76c0efcea 100644 --- a/apps/login/ui/VerifyU2F.tsx +++ b/apps/login/ui/VerifyU2F.tsx @@ -1,16 +1,265 @@ +"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 | undefined; - sessionId: string | undefined; + loginName?: string; + sessionId?: string; authRequestId?: string; organization?: string; - submit: boolean; }; export default function VerifyU2F({ loginName, + sessionId, authRequestId, organization, - submit, }: Props) { - return
Verify U2F
; + const [error, setError] = useState(""); + const [loading, setLoading] = useState(false); + + const router = useRouter(); + + const initialized = useRef(false); + + 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() { + setLoading(true); + const res = await fetch("/api/session", { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + loginName, + sessionId, + organization, + challenges: { + webAuthN: { + domain: "", + userVerificationRequirement: 1, + }, + }, + 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 ? ( + + ) : ( + + )} + + + +
+
+ ); }