diff --git a/apps/login/app/(login)/passkey/login/page.tsx b/apps/login/app/(login)/passkey/login/page.tsx new file mode 100644 index 00000000000..a48578e6596 --- /dev/null +++ b/apps/login/app/(login)/passkey/login/page.tsx @@ -0,0 +1,57 @@ +import { getSession, server } from "#/lib/zitadel"; +import Alert, { AlertType } from "#/ui/Alert"; +import LoginPasskey from "#/ui/LoginPasskey"; +import RegisterPasskey from "#/ui/RegisterPasskey"; +import UserAvatar from "#/ui/UserAvatar"; +import { getMostRecentCookieWithLoginname } from "#/utils/cookies"; + +export default async function Page({ + searchParams, +}: { + searchParams: Record; +}) { + const { loginName, prompt } = searchParams; + + const sessionFactors = await loadSession(loginName); + + async function loadSession(loginName?: string) { + const recent = await getMostRecentCookieWithLoginname(loginName); + return getSession(server, recent.id, recent.token).then((response) => { + if (response?.session) { + return response.session; + } + }); + } + const title = !!prompt + ? "Authenticate with a passkey" + : "Use your passkey to confirm it's really you"; + const description = !!prompt + ? "When set up, you will be able to authenticate without a password." + : "Your device will ask for your fingerprint, face, or screen lock"; + + return ( +
+

{title}

+ + {sessionFactors && ( + + )} +

{description}

+ + {!sessionFactors && ( +
+ + Could not get the context of the user. Make sure to enter the + username first or provide a loginName as searchParam. + +
+ )} + + {sessionFactors?.id && } +
+ ); +} diff --git a/apps/login/app/passkeys/route.ts b/apps/login/app/passkeys/route.ts index 9bd442c73ca..3cbd4b71bcd 100644 --- a/apps/login/app/passkeys/route.ts +++ b/apps/login/app/passkeys/route.ts @@ -20,13 +20,15 @@ export async function POST(request: NextRequest) { sessionCookie.token ); + const domain: string = request.nextUrl.hostname; + const userId = session?.session?.factors?.user?.id; if (userId) { return createPasskeyRegistrationLink(userId) .then((resp) => { const code = resp.code; - return registerPasskey(userId, code).then((resp) => { + return registerPasskey(userId, code, domain).then((resp) => { return NextResponse.json(resp); }); }) diff --git a/apps/login/app/session/route.ts b/apps/login/app/session/route.ts index c143bb66b8b..f27d80660de 100644 --- a/apps/login/app/session/route.ts +++ b/apps/login/app/session/route.ts @@ -20,7 +20,15 @@ export async function POST(request: NextRequest) { if (body) { const { loginName, password } = body; - const createdSession = await createSession(server, loginName, password); + const domain: string = request.nextUrl.hostname; + + const createdSession = await createSession( + server, + loginName, + password, + domain + ); + if (createdSession) { return getSession( server, diff --git a/apps/login/lib/zitadel.ts b/apps/login/lib/zitadel.ts index 6190634bf46..9322a97e12b 100644 --- a/apps/login/lib/zitadel.ts +++ b/apps/login/lib/zitadel.ts @@ -78,12 +78,13 @@ export async function getPasswordComplexitySettings( export async function createSession( server: ZitadelServer, loginName: string, - password: string | undefined + password: string | undefined, + domain: string ): Promise { const sessionService = session.getSession(server); return password ? sessionService.createSession( - { checks: { user: { loginName }, password: { password } } }, + { checks: { user: { loginName }, password: { password } }, domain }, {} ) : sessionService.createSession({ checks: { user: { loginName } } }, {}); @@ -236,6 +237,7 @@ export async function verifyPasskeyRegistration( { passkeyId, passkeyName, + publicKeyCredential, userId, }, @@ -251,12 +253,15 @@ export async function verifyPasskeyRegistration( */ export async function registerPasskey( userId: string, - code: { id: string; code: string } + code: { id: string; code: string }, + domain: string ): Promise { const userservice = user.getUser(server); return userservice.registerPasskey({ userId, code, + domain, + // authenticator: }); } diff --git a/apps/login/ui/LoginPasskey.tsx b/apps/login/ui/LoginPasskey.tsx new file mode 100644 index 00000000000..3b067038eb9 --- /dev/null +++ b/apps/login/ui/LoginPasskey.tsx @@ -0,0 +1,153 @@ +"use client"; + +import { useState } from "react"; +import { Button, ButtonVariants } from "./Button"; +import { useForm } from "react-hook-form"; +import { useRouter } from "next/navigation"; +import { Spinner } from "./Spinner"; +import Alert from "./Alert"; +import { RegisterPasskeyResponse } from "@zitadel/server"; +import { coerceToArrayBuffer, coerceToBase64Url } from "#/utils/base64"; +type Inputs = {}; + +type Props = { + sessionId: string; +}; + +export default function LoginPasskey({ sessionId }: Props) { + const { login, handleSubmit, formState } = useForm({ + mode: "onBlur", + }); + + const [error, setError] = useState(""); + + const [loading, setLoading] = useState(false); + + const router = useRouter(); + + async function submitLogin( + passkeyId: string, + passkeyName: string, + publicKeyCredential: any, + sessionId: string + ) { + setLoading(true); + const res = await fetch("/passkeys/verify", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + passkeyId, + passkeyName, + publicKeyCredential, + sessionId, + }), + }); + + const response = await res.json(); + + setLoading(false); + if (!res.ok) { + setError(response.details); + return Promise.reject(response.details); + } + return response; + } + + function submitLoginAndContinue(value: Inputs): Promise { + navigator.credentials + .get({ + publicKey: resp.publicKeyCredentialCreationOptions, + }) + .then((assertedCredential: any) => { + if (assertedCredential) { + let authData = new Uint8Array( + assertedCredential.response.authenticatorData + ); + let clientDataJSON = new Uint8Array( + assertedCredential.response.clientDataJSON + ); + let rawId = new Uint8Array(assertedCredential.rawId); + let sig = new Uint8Array(assertedCredential.response.signature); + let userHandle = new Uint8Array( + assertedCredential.response.userHandle + ); + + let data = JSON.stringify({ + 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(passkeyId, "", data, sessionId); + } else { + setLoading(false); + setError("An error on retrieving passkey"); + return null; + } + }) + .catch((error) => { + console.error(error); + setLoading(false); + // setError(error); + + return null; + }); + } + // return router.push(`/accounts`); + } + + const { errors } = formState; + + return ( +
+ {error && ( +
+ {error} +
+ )} + +
+ {isPrompt ? ( + + ) : ( + + )} + + + +
+
+ ); +} diff --git a/apps/login/ui/RegisterPasskey.tsx b/apps/login/ui/RegisterPasskey.tsx index f5d0cb53830..800a57879e9 100644 --- a/apps/login/ui/RegisterPasskey.tsx +++ b/apps/login/ui/RegisterPasskey.tsx @@ -95,7 +95,7 @@ export default function RegisterPasskey({ sessionId, isPrompt }: Props) { resp.publicKeyCredentialCreationOptions.publicKey.user.id = coerceToArrayBuffer( resp.publicKeyCredentialCreationOptions.publicKey.user.id, - "challenge" + "userid" ); if ( resp.publicKeyCredentialCreationOptions.publicKey.excludeCredentials @@ -140,7 +140,9 @@ export default function RegisterPasskey({ sessionId, isPrompt }: Props) { ), }, }; - return submitVerify(passkeyId, "", data, sessionId); + return submitVerify(passkeyId, "", data, sessionId).then(() => { + router.push("/accounts"); + }); } else { setLoading(false); setError("An error on registering passkey");