From 93b333837d82794a908b02f431c03e2dfd0d2882 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Tue, 20 May 2025 14:10:18 +0200 Subject: [PATCH] helper functions --- apps/login/src/app/(login)/verify/page.tsx | 54 +++++++++------- apps/login/src/lib/server/loginname.ts | 28 +++++++- apps/login/src/lib/server/passkeys.ts | 75 +++++++++++++--------- apps/login/src/lib/server/password.ts | 24 ++----- apps/login/src/lib/server/verify.ts | 8 +-- apps/login/src/lib/verify-helper.ts | 34 +++++++++- 6 files changed, 143 insertions(+), 80 deletions(-) diff --git a/apps/login/src/app/(login)/verify/page.tsx b/apps/login/src/app/(login)/verify/page.tsx index 198a46a5fe..5d7941322a 100644 --- a/apps/login/src/app/(login)/verify/page.tsx +++ b/apps/login/src/app/(login)/verify/page.tsx @@ -6,6 +6,7 @@ import { VerifyRedirectButton } from "@/components/verify-redirect-button"; import { sendEmailCode } from "@/lib/server/verify"; import { getServiceUrlFromHeaders } from "@/lib/service-url"; import { loadMostRecentSession } from "@/lib/session"; +import { checkUserVerification } from "@/lib/verify-helper"; import { getBrandingSettings, getUserByID, @@ -96,14 +97,23 @@ export default async function Page(props: { searchParams: Promise }) { id = userId ?? sessionFactors?.factors?.user?.id; + if (!id) { + throw Error("Failed to get user id"); + } + let authMethods: AuthenticationMethodType[] | null = null; if (human?.email?.isVerified) { - const authMethodsResponse = await listAuthenticationMethodTypes(userId); + const authMethodsResponse = await listAuthenticationMethodTypes({ + serviceUrl, + userId, + }); if (authMethodsResponse.authMethodTypes) { authMethods = authMethodsResponse.authMethodTypes; } } + const hasValidUserVerificationCheck = await checkUserVerification(id); + const params = new URLSearchParams({ userId: userId, initial: "true", // defines that a code is not required and is therefore not shown in the UI @@ -155,27 +165,27 @@ export default async function Page(props: { searchParams: Promise }) { ) )} - {id && - (human?.email?.isVerified ? ( - // show page for already verified users - - ) : ( - // check if auth methods are set - - ))} + {/* show a button to setup auth method for the user otherwise show the UI for reverifying */} + {human?.email?.isVerified && hasValidUserVerificationCheck ? ( + // show page for already verified users + + ) : ( + // check if auth methods are set + + )} ); diff --git a/apps/login/src/lib/server/loginname.ts b/apps/login/src/lib/server/loginname.ts index 2ea6004fdc..8754e77d56 100644 --- a/apps/login/src/lib/server/loginname.ts +++ b/apps/login/src/lib/server/loginname.ts @@ -9,7 +9,7 @@ import { idpTypeToIdentityProviderType, idpTypeToSlug } from "../idp"; import { PasskeysType } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; import { UserState } from "@zitadel/proto/zitadel/user/v2/user_pb"; import { getServiceUrlFromHeaders } from "../service-url"; -import { checkInvite } from "../verify-helper"; +import { checkEmailVerified, checkUserVerification } from "../verify-helper"; import { getActiveIdentityProviders, getIDPByID, @@ -257,7 +257,7 @@ export async function sendLoginname(command: SendLoginnameCommand) { // this can be expected to be an invite as users created in console have a password set. if (!methods.authMethodTypes || !methods.authMethodTypes.length) { // redirect to /verify invite if no auth method is set and email is not verified - const inviteCheck = checkInvite( + const inviteCheck = checkEmailVerified( session, humanUser, session.factors.user.organizationId, @@ -268,6 +268,30 @@ export async function sendLoginname(command: SendLoginnameCommand) { return inviteCheck; } + // check if user was verified + const isUserVerified = await checkUserVerification( + session.factors.user.id, + ); + if (!isUserVerified) { + const params = new URLSearchParams({ + loginName: session.factors?.user?.loginName as string, + }); + + if (command.requestId) { + params.append("requestId", command.requestId); + } + + if (command.organization || session.factors?.user?.organizationId) { + params.append( + "organization", + command.organization ?? + (session.factors?.user?.organizationId as string), + ); + } + + return { redirect: `/verify?` + params }; + } + const paramsAuthenticatorSetup = new URLSearchParams({ loginName: session.factors?.user?.loginName, userId: session.factors?.user?.id, // verify needs user id diff --git a/apps/login/src/lib/server/passkeys.ts b/apps/login/src/lib/server/passkeys.ts index c5ad990c61..3470629f24 100644 --- a/apps/login/src/lib/server/passkeys.ts +++ b/apps/login/src/lib/server/passkeys.ts @@ -9,14 +9,14 @@ import { registerPasskey, verifyPasskeyRegistration as zitadelVerifyPasskeyRegistration, } from "@/lib/zitadel"; -import { create, Duration } from "@zitadel/client"; +import { create, Duration, Timestamp, timestampDate } from "@zitadel/client"; +import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb"; import { Checks } from "@zitadel/proto/zitadel/session/v2/session_service_pb"; import { RegisterPasskeyResponse, VerifyPasskeyRegistrationRequestSchema, } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; -import crypto from "crypto"; -import { cookies, headers } from "next/headers"; +import { headers } from "next/headers"; import { userAgent } from "next/server"; import { getNextUrl } from "../client"; import { @@ -24,9 +24,11 @@ import { getSessionCookieById, getSessionCookieByLoginName, } from "../cookies"; -import { getFingerprintId } from "../fingerprint"; import { getServiceUrlFromHeaders } from "../service-url"; -import { checkEmailVerification } from "../verify-helper"; +import { + checkEmailVerification, + checkUserVerification, +} from "../verify-helper"; import { setSessionAndUpdateCookie } from "./cookie"; type VerifyPasskeyCommand = { @@ -40,6 +42,22 @@ type RegisterPasskeyCommand = { sessionId: string; }; +function isSessionValid(session: Partial): { + valid: boolean; + verifiedAt?: Timestamp; +} { + const validPassword = session?.factors?.password?.verifiedAt; + const validPasskey = session?.factors?.webAuthN?.verifiedAt; + const stillValid = session.expirationDate + ? timestampDate(session.expirationDate) > new Date() + : true; + + const verifiedAt = validPassword || validPasskey; + const valid = !!((validPassword || validPasskey) && stillValid); + + return { valid, verifiedAt }; +} + export async function registerPasskeyLink( command: RegisterPasskeyCommand, ): Promise { @@ -64,37 +82,30 @@ export async function registerPasskeyLink( return { error: "Could not determine user from session" }; } - const authmethods = await listAuthenticationMethodTypes({ - serviceUrl, - userId: session?.session?.factors?.user?.id, - }); + const sessionValid = isSessionValid(session.session); - // if the user has no authmethods set, we need to check if the user was verified - // users are redirected from /authenticator/set to /password/set - if (authmethods.authMethodTypes.length !== 0) { - return { - error: - "You have to provide a code or have a valid User Verification Check", - }; - } + if (!sessionValid) { + const authmethods = await listAuthenticationMethodTypes({ + serviceUrl, + userId: session.session.factors.user.id, + }); - // check if a verification was done earlier - const cookiesList = await cookies(); - const userAgentId = await getFingerprintId(); + // if the user has no authmethods set, we need to check if the user was verified + if (authmethods.authMethodTypes.length !== 0) { + return { + error: + "You have to authenticate or have a valid User Verification Check", + }; + } - const verificationCheck = crypto - .createHash("sha256") - .update(`${user.userId}:${userAgentId}`) - .digest("hex"); + // check if a verification was done earlier + const hasValidUserVerificationCheck = await checkUserVerification( + session.session.factors.user.id, + ); - const cookieValue = await cookiesList.get("verificationCheck")?.value; - - if (!cookieValue) { - return { error: "User Verification Check has to be done" }; - } - - if (cookieValue !== verificationCheck) { - return { error: "User Verification Check has to be done" }; + if (!hasValidUserVerificationCheck) { + return { error: "User Verification Check has to be done" }; + } } const [hostname, port] = host.split(":"); diff --git a/apps/login/src/lib/server/password.ts b/apps/login/src/lib/server/password.ts index 26adb56c7b..34859d419b 100644 --- a/apps/login/src/lib/server/password.ts +++ b/apps/login/src/lib/server/password.ts @@ -25,16 +25,15 @@ import { import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; import { User, UserState } from "@zitadel/proto/zitadel/user/v2/user_pb"; import { SetPasswordRequestSchema } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; -import crypto from "crypto"; -import { cookies, headers } from "next/headers"; +import { headers } from "next/headers"; import { getNextUrl } from "../client"; import { getSessionCookieById, getSessionCookieByLoginName } from "../cookies"; -import { getFingerprintId } from "../fingerprint"; import { getServiceUrlFromHeaders } from "../service-url"; import { checkEmailVerification, checkMFAFactors, checkPasswordChangeRequired, + checkUserVerification, } from "../verify-helper"; type ResetPasswordCommand = { @@ -327,7 +326,6 @@ export async function changePassword(command: { }); // if the user has no authmethods set, we need to check if the user was verified - // users are redirected from /authenticator/set to /password/set if (authmethods.authMethodTypes.length !== 0) { return { error: @@ -336,21 +334,11 @@ export async function changePassword(command: { } // check if a verification was done earlier - const cookiesList = await cookies(); - const userAgentId = await getFingerprintId(); + const hasValidUserVerificationCheck = await checkUserVerification( + user.userId, + ); - const verificationCheck = crypto - .createHash("sha256") - .update(`${user.userId}:${userAgentId}`) - .digest("hex"); - - const cookieValue = await cookiesList.get("verificationCheck")?.value; - - if (!cookieValue) { - return { error: "User Verification Check has to be done" }; - } - - if (cookieValue !== verificationCheck) { + if (!hasValidUserVerificationCheck) { return { error: "User Verification Check has to be done" }; } } diff --git a/apps/login/src/lib/server/verify.ts b/apps/login/src/lib/server/verify.ts index d56af5e582..db014431ac 100644 --- a/apps/login/src/lib/server/verify.ts +++ b/apps/login/src/lib/server/verify.ts @@ -21,7 +21,7 @@ import { User } from "@zitadel/proto/zitadel/user/v2/user_pb"; import { cookies, headers } from "next/headers"; import { getNextUrl } from "../client"; import { getSessionCookieByLoginName } from "../cookies"; -import { getFingerprintId } from "../fingerprint"; +import { getOrSetFingerprintId } from "../fingerprint"; import { getServiceUrlFromHeaders } from "../service-url"; import { loadMostRecentSession } from "../session"; import { checkMFAFactors } from "../verify-helper"; @@ -197,11 +197,9 @@ export async function sendVerification(command: VerifyUserByEmailCommand) { params.set("loginName", session.factors?.user?.loginName); } - // set hash of userId and userAgentId to prevent replay attacks, TODO: check on the /authenticator/set page - + // set hash of userId and userAgentId to prevent attacks, checks are done for users with invalid sessions and invalid userAgentId const cookiesList = await cookies(); - - const userAgentId = await getFingerprintId(); + const userAgentId = await getOrSetFingerprintId(); const verificationCheck = crypto .createHash("sha256") diff --git a/apps/login/src/lib/verify-helper.ts b/apps/login/src/lib/verify-helper.ts index 704d7bbef6..763f9dff45 100644 --- a/apps/login/src/lib/verify-helper.ts +++ b/apps/login/src/lib/verify-helper.ts @@ -4,7 +4,10 @@ import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings import { PasswordExpirySettings } from "@zitadel/proto/zitadel/settings/v2/password_settings_pb"; import { HumanUser } from "@zitadel/proto/zitadel/user/v2/user_pb"; import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; +import crypto from "crypto"; import moment from "moment"; +import { cookies } from "next/headers"; +import { getOrSetFingerprintId } from "./fingerprint"; import { getUserByID } from "./zitadel"; export function checkPasswordChangeRequired( @@ -44,7 +47,7 @@ export function checkPasswordChangeRequired( } } -export function checkInvite( +export function checkEmailVerified( session: Session, humanUser?: HumanUser, organization?: string, @@ -248,3 +251,32 @@ export async function checkMFAFactors( return { redirect: `/mfa/set?` + params }; } } + +export async function checkUserVerification(userId: string): Promise { + // check if a verification was done earlier + const cookiesList = await cookies(); + const userAgentId = await getOrSetFingerprintId(); + + const verificationCheck = crypto + .createHash("sha256") + .update(`${userId}:${userAgentId}`) + .digest("hex"); + + const cookieValue = await cookiesList.get("verificationCheck")?.value; + + if (!cookieValue) { + console.warn( + "User verification check cookie not found. User verification check failed.", + ); + return false; + } + + if (cookieValue !== verificationCheck) { + console.warn( + `User verification check failed. Expected ${verificationCheck} but got ${cookieValue}`, + ); + return false; + } + + return true; +}