From f1f7d661cef813824496162ae0d718c49fe8fc97 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Fri, 20 Dec 2024 08:44:59 +0100 Subject: [PATCH] passkey actions cleanup --- apps/login/src/components/login-passkey.tsx | 34 +----- .../login/src/components/register-passkey.tsx | 7 +- apps/login/src/lib/server/passkeys.ts | 105 +++++++++++++++++- apps/login/src/lib/server/password.ts | 29 ++--- apps/login/src/lib/server/session.ts | 25 ++++- apps/login/src/lib/verify-helper.ts | 26 +++++ 6 files changed, 170 insertions(+), 56 deletions(-) diff --git a/apps/login/src/components/login-passkey.tsx b/apps/login/src/components/login-passkey.tsx index bf1ae01207..5e05cdb6a8 100644 --- a/apps/login/src/components/login-passkey.tsx +++ b/apps/login/src/components/login-passkey.tsx @@ -1,7 +1,7 @@ "use client"; import { coerceToArrayBuffer, coerceToBase64Url } from "@/helpers/base64"; -import { getNextUrl } from "@/lib/client"; +import { sendPasskey } from "@/lib/server/passkeys"; import { updateSession } from "@/lib/server/session"; import { create, JsonObject } from "@zitadel/client"; import { @@ -120,7 +120,7 @@ export function LoginPasskey({ async function submitLogin(data: JsonObject) { setLoading(true); - const response = await updateSession({ + const response = await sendPasskey({ loginName, sessionId, organization, @@ -142,7 +142,9 @@ export function LoginPasskey({ return; } - return response; + if (response && "redirect" in response && response.redirect) { + return router.push(response.redirect); + } } async function submitLoginAndContinue( @@ -192,31 +194,7 @@ export function LoginPasskey({ }, }; - return submitLogin(data).then(async (resp) => { - const url = - authRequestId && resp?.sessionId - ? await getNextUrl( - { - sessionId: resp.sessionId, - authRequestId: authRequestId, - organization: organization, - }, - loginSettings?.defaultRedirectUri, - ) - : resp?.factors?.user?.loginName - ? await getNextUrl( - { - loginName: resp.factors.user.loginName, - organization: organization, - }, - loginSettings?.defaultRedirectUri, - ) - : null; - - if (url) { - router.push(url); - } - }); + return submitLogin(data); }) .finally(() => { setLoading(false); diff --git a/apps/login/src/components/register-passkey.tsx b/apps/login/src/components/register-passkey.tsx index 9a71830261..e737168678 100644 --- a/apps/login/src/components/register-passkey.tsx +++ b/apps/login/src/components/register-passkey.tsx @@ -1,7 +1,10 @@ "use client"; import { coerceToArrayBuffer, coerceToBase64Url } from "@/helpers/base64"; -import { registerPasskeyLink, verifyPasskey } from "@/lib/server/passkeys"; +import { + registerPasskeyLink, + verifyPasskeyRegistration, +} from "@/lib/server/passkeys"; import { useTranslations } from "next-intl"; import { useRouter } from "next/navigation"; import { useState } from "react"; @@ -45,7 +48,7 @@ export function RegisterPasskey({ sessionId: string, ) { setLoading(true); - const response = await verifyPasskey({ + const response = await verifyPasskeyRegistration({ passkeyId, passkeyName, publicKeyCredential, diff --git a/apps/login/src/lib/server/passkeys.ts b/apps/login/src/lib/server/passkeys.ts index 181962cae1..f34702a61e 100644 --- a/apps/login/src/lib/server/passkeys.ts +++ b/apps/login/src/lib/server/passkeys.ts @@ -2,18 +2,28 @@ import { createPasskeyRegistrationLink, + getLoginSettings, getSession, + getUserByID, registerPasskey, - verifyPasskeyRegistration, + verifyPasskeyRegistration as zitadelVerifyPasskeyRegistration, } from "@/lib/zitadel"; -import { create } from "@zitadel/client"; +import { create, Duration } from "@zitadel/client"; +import { Checks } from "@zitadel/proto/zitadel/session/v2/session_service_pb"; import { RegisterPasskeyResponse, VerifyPasskeyRegistrationRequestSchema, } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; import { headers } from "next/headers"; import { userAgent } from "next/server"; -import { getSessionCookieById } from "../cookies"; +import { getNextUrl } from "../client"; +import { + getMostRecentSessionCookie, + getSessionCookieById, + getSessionCookieByLoginName, +} from "../cookies"; +import { checkEmailVerification } from "../verify-helper"; +import { setSessionAndUpdateCookie } from "./cookie"; type VerifyPasskeyCommand = { passkeyId: string; @@ -69,7 +79,7 @@ export async function registerPasskeyLink( return registerPasskey(userId, registerLink.code, hostname); } -export async function verifyPasskey(command: VerifyPasskeyCommand) { +export async function verifyPasskeyRegistration(command: VerifyPasskeyCommand) { // if no name is provided, try to generate one from the user agent let passkeyName = command.passkeyName; if (!!!passkeyName) { @@ -95,7 +105,7 @@ export async function verifyPasskey(command: VerifyPasskeyCommand) { throw new Error("Could not get session"); } - return verifyPasskeyRegistration( + return zitadelVerifyPasskeyRegistration( create(VerifyPasskeyRegistrationRequestSchema, { passkeyId: command.passkeyId, publicKeyCredential: command.publicKeyCredential, @@ -104,3 +114,88 @@ export async function verifyPasskey(command: VerifyPasskeyCommand) { }), ); } + +type SendPasskeyCommand = { + loginName?: string; + sessionId?: string; + organization?: string; + checks?: Checks; + authRequestId?: string; + lifetime?: Duration; +}; + +export async function sendPasskey(command: SendPasskeyCommand) { + let { loginName, sessionId, organization, checks, authRequestId } = command; + const recentSession = sessionId + ? await getSessionCookieById({ sessionId }) + : loginName + ? await getSessionCookieByLoginName({ loginName, organization }) + : await getMostRecentSessionCookie(); + + if (!recentSession) { + return { + error: "Could not find session", + }; + } + + const host = (await headers()).get("host"); + + if (!host) { + return { error: "Could not get host" }; + } + + const loginSettings = await getLoginSettings(organization); + + const lifetime = checks?.webAuthN + ? loginSettings?.multiFactorCheckLifetime // TODO different lifetime for webauthn u2f/passkey + : checks?.otpEmail || checks?.otpSms + ? loginSettings?.secondFactorCheckLifetime + : undefined; + + const session = await setSessionAndUpdateCookie( + recentSession, + checks, + undefined, + authRequestId, + lifetime, + ); + + if (!session || !session?.factors?.user?.id) { + return { error: "Could not update session" }; + } + + const userResponse = await getUserByID(session?.factors?.user?.id); + + if (!userResponse.user) { + return { error: "Could not find user" }; + } + + const humanUser = + userResponse.user.type.case === "human" + ? userResponse.user.type.value + : undefined; + + checkEmailVerification(session, humanUser, organization, authRequestId); + + const url = + authRequestId && session.id + ? await getNextUrl( + { + sessionId: session.id, + authRequestId: authRequestId, + organization: organization, + }, + loginSettings?.defaultRedirectUri, + ) + : session?.factors?.user?.loginName + ? await getNextUrl( + { + loginName: session.factors.user.loginName, + organization: organization, + }, + loginSettings?.defaultRedirectUri, + ) + : null; + + return { redirect: url }; +} diff --git a/apps/login/src/lib/server/password.ts b/apps/login/src/lib/server/password.ts index f35f71b6d9..c89cf02cea 100644 --- a/apps/login/src/lib/server/password.ts +++ b/apps/login/src/lib/server/password.ts @@ -30,7 +30,11 @@ import { import { headers } from "next/headers"; import { getNextUrl } from "../client"; import { getSessionCookieById, getSessionCookieByLoginName } from "../cookies"; -import { checkEmailVerification, checkMFAFactors } from "../verify-helper"; +import { + checkEmailVerification, + checkMFAFactors, + checkPasswordChangeRequired, +} from "../verify-helper"; type ResetPasswordCommand = { loginName: string; @@ -138,30 +142,19 @@ export async function sendPassword(command: UpdateSessionCommand) { const humanUser = user.type.case === "human" ? user.type.value : undefined; // check if the user has to change password first - if (humanUser?.passwordChangeRequired) { - const params = new URLSearchParams({ - loginName: session.factors?.user?.loginName, - }); - - if (command.organization || session.factors?.user?.organizationId) { - params.append("organization", session.factors?.user?.organizationId); - } - - if (command.authRequestId) { - params.append("authRequestId", command.authRequestId); - } - - return { redirect: "/password/change?" + params }; - } + checkPasswordChangeRequired( + session, + humanUser, + command.organization, + command.authRequestId, + ); // throw error if user is in initial state here and do not continue - if (user.state === UserState.INITIAL) { return { error: "Initial User not supported" }; } // check to see if user was verified - checkEmailVerification( session, humanUser, diff --git a/apps/login/src/lib/server/session.ts b/apps/login/src/lib/server/session.ts index 69aac95a10..566f4818d3 100644 --- a/apps/login/src/lib/server/session.ts +++ b/apps/login/src/lib/server/session.ts @@ -22,6 +22,7 @@ import { getSessionCookieByLoginName, removeSessionFromCookie, } from "../cookies"; +import { checkPasswordChangeRequired } from "../verify-helper"; type CreateNewSessionCommand = { userId: string; @@ -41,13 +42,15 @@ export async function createNewSessionForIdp(options: CreateNewSessionCommand) { throw new Error("No userId or loginName provided"); } - const user = await getUserByID(userId); + const userResponse = await getUserByID(userId); - if (!user) { + if (!userResponse || !userResponse.user) { return { error: "Could not find user" }; } - const loginSettings = await getLoginSettings(user.details?.resourceOwner); + const loginSettings = await getLoginSettings( + userResponse.user.details?.resourceOwner, + ); const session = await createSessionForIdpAndUpdateCookie( userId, @@ -60,6 +63,22 @@ export async function createNewSessionForIdp(options: CreateNewSessionCommand) { return { error: "Could not create session" }; } + const humanUser = + userResponse.user.type.case === "human" + ? userResponse.user.type.value + : undefined; + + // check if the user has to change password first + checkPasswordChangeRequired( + session, + humanUser, + session.factors.user.organizationId, + authRequestId, + ); + + // TODO: check if user has MFA methods + // checkMFAFactors(session, loginSettings, authMethods, organization, authRequestId); + const url = await getNextUrl( authRequestId && session.id ? { diff --git a/apps/login/src/lib/verify-helper.ts b/apps/login/src/lib/verify-helper.ts index 02a940f160..ec88807c71 100644 --- a/apps/login/src/lib/verify-helper.ts +++ b/apps/login/src/lib/verify-helper.ts @@ -3,6 +3,32 @@ import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings import { HumanUser } from "@zitadel/proto/zitadel/user/v2/user_pb"; import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; +export function checkPasswordChangeRequired( + session: Session, + humanUser: HumanUser | undefined, + organization?: string, + authRequestId?: string, +) { + if (humanUser?.passwordChangeRequired) { + const params = new URLSearchParams({ + loginName: session.factors?.user?.loginName as string, + }); + + if (organization || session.factors?.user?.organizationId) { + params.append( + "organization", + session.factors?.user?.organizationId as string, + ); + } + + if (authRequestId) { + params.append("authRequestId", authRequestId); + } + + return { redirect: "/password/change?" + params }; + } +} + export function checkEmailVerification( session: Session, humanUser?: HumanUser,