helper functions

This commit is contained in:
Max Peintner
2025-05-20 14:10:18 +02:00
parent 1ffb996815
commit 93b333837d
6 changed files with 143 additions and 80 deletions

View File

@@ -6,6 +6,7 @@ import { VerifyRedirectButton } from "@/components/verify-redirect-button";
import { sendEmailCode } from "@/lib/server/verify"; import { sendEmailCode } from "@/lib/server/verify";
import { getServiceUrlFromHeaders } from "@/lib/service-url"; import { getServiceUrlFromHeaders } from "@/lib/service-url";
import { loadMostRecentSession } from "@/lib/session"; import { loadMostRecentSession } from "@/lib/session";
import { checkUserVerification } from "@/lib/verify-helper";
import { import {
getBrandingSettings, getBrandingSettings,
getUserByID, getUserByID,
@@ -96,14 +97,23 @@ export default async function Page(props: { searchParams: Promise<any> }) {
id = userId ?? sessionFactors?.factors?.user?.id; id = userId ?? sessionFactors?.factors?.user?.id;
if (!id) {
throw Error("Failed to get user id");
}
let authMethods: AuthenticationMethodType[] | null = null; let authMethods: AuthenticationMethodType[] | null = null;
if (human?.email?.isVerified) { if (human?.email?.isVerified) {
const authMethodsResponse = await listAuthenticationMethodTypes(userId); const authMethodsResponse = await listAuthenticationMethodTypes({
serviceUrl,
userId,
});
if (authMethodsResponse.authMethodTypes) { if (authMethodsResponse.authMethodTypes) {
authMethods = authMethodsResponse.authMethodTypes; authMethods = authMethodsResponse.authMethodTypes;
} }
} }
const hasValidUserVerificationCheck = await checkUserVerification(id);
const params = new URLSearchParams({ const params = new URLSearchParams({
userId: userId, userId: userId,
initial: "true", // defines that a code is not required and is therefore not shown in the UI 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<any> }) {
) )
)} )}
{id && {/* show a button to setup auth method for the user otherwise show the UI for reverifying */}
(human?.email?.isVerified ? ( {human?.email?.isVerified && hasValidUserVerificationCheck ? (
// show page for already verified users // show page for already verified users
<VerifyRedirectButton <VerifyRedirectButton
userId={id} userId={id}
loginName={loginName} loginName={loginName}
organization={organization} organization={organization}
requestId={requestId} requestId={requestId}
authMethods={authMethods} authMethods={authMethods}
/> />
) : ( ) : (
// check if auth methods are set // check if auth methods are set
<VerifyForm <VerifyForm
loginName={loginName} loginName={loginName}
organization={organization} organization={organization}
userId={id} userId={id}
code={code} code={code}
isInvite={invite === "true"} isInvite={invite === "true"}
requestId={requestId} requestId={requestId}
/> />
))} )}
</div> </div>
</DynamicTheme> </DynamicTheme>
); );

View File

@@ -9,7 +9,7 @@ import { idpTypeToIdentityProviderType, idpTypeToSlug } from "../idp";
import { PasskeysType } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; import { PasskeysType } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
import { UserState } from "@zitadel/proto/zitadel/user/v2/user_pb"; import { UserState } from "@zitadel/proto/zitadel/user/v2/user_pb";
import { getServiceUrlFromHeaders } from "../service-url"; import { getServiceUrlFromHeaders } from "../service-url";
import { checkInvite } from "../verify-helper"; import { checkEmailVerified, checkUserVerification } from "../verify-helper";
import { import {
getActiveIdentityProviders, getActiveIdentityProviders,
getIDPByID, 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. // this can be expected to be an invite as users created in console have a password set.
if (!methods.authMethodTypes || !methods.authMethodTypes.length) { if (!methods.authMethodTypes || !methods.authMethodTypes.length) {
// redirect to /verify invite if no auth method is set and email is not verified // redirect to /verify invite if no auth method is set and email is not verified
const inviteCheck = checkInvite( const inviteCheck = checkEmailVerified(
session, session,
humanUser, humanUser,
session.factors.user.organizationId, session.factors.user.organizationId,
@@ -268,6 +268,30 @@ export async function sendLoginname(command: SendLoginnameCommand) {
return inviteCheck; 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({ const paramsAuthenticatorSetup = new URLSearchParams({
loginName: session.factors?.user?.loginName, loginName: session.factors?.user?.loginName,
userId: session.factors?.user?.id, // verify needs user id userId: session.factors?.user?.id, // verify needs user id

View File

@@ -9,14 +9,14 @@ import {
registerPasskey, registerPasskey,
verifyPasskeyRegistration as zitadelVerifyPasskeyRegistration, verifyPasskeyRegistration as zitadelVerifyPasskeyRegistration,
} from "@/lib/zitadel"; } 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 { Checks } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
import { import {
RegisterPasskeyResponse, RegisterPasskeyResponse,
VerifyPasskeyRegistrationRequestSchema, VerifyPasskeyRegistrationRequestSchema,
} from "@zitadel/proto/zitadel/user/v2/user_service_pb"; } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
import crypto from "crypto"; import { headers } from "next/headers";
import { cookies, headers } from "next/headers";
import { userAgent } from "next/server"; import { userAgent } from "next/server";
import { getNextUrl } from "../client"; import { getNextUrl } from "../client";
import { import {
@@ -24,9 +24,11 @@ import {
getSessionCookieById, getSessionCookieById,
getSessionCookieByLoginName, getSessionCookieByLoginName,
} from "../cookies"; } from "../cookies";
import { getFingerprintId } from "../fingerprint";
import { getServiceUrlFromHeaders } from "../service-url"; import { getServiceUrlFromHeaders } from "../service-url";
import { checkEmailVerification } from "../verify-helper"; import {
checkEmailVerification,
checkUserVerification,
} from "../verify-helper";
import { setSessionAndUpdateCookie } from "./cookie"; import { setSessionAndUpdateCookie } from "./cookie";
type VerifyPasskeyCommand = { type VerifyPasskeyCommand = {
@@ -40,6 +42,22 @@ type RegisterPasskeyCommand = {
sessionId: string; sessionId: string;
}; };
function isSessionValid(session: Partial<Session>): {
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( export async function registerPasskeyLink(
command: RegisterPasskeyCommand, command: RegisterPasskeyCommand,
): Promise<RegisterPasskeyResponse | { error: string }> { ): Promise<RegisterPasskeyResponse | { error: string }> {
@@ -64,37 +82,30 @@ export async function registerPasskeyLink(
return { error: "Could not determine user from session" }; return { error: "Could not determine user from session" };
} }
const authmethods = await listAuthenticationMethodTypes({ const sessionValid = isSessionValid(session.session);
serviceUrl,
userId: session?.session?.factors?.user?.id,
});
// if the user has no authmethods set, we need to check if the user was verified if (!sessionValid) {
// users are redirected from /authenticator/set to /password/set const authmethods = await listAuthenticationMethodTypes({
if (authmethods.authMethodTypes.length !== 0) { serviceUrl,
return { userId: session.session.factors.user.id,
error: });
"You have to provide a code or have a valid User Verification Check",
};
}
// check if a verification was done earlier // if the user has no authmethods set, we need to check if the user was verified
const cookiesList = await cookies(); if (authmethods.authMethodTypes.length !== 0) {
const userAgentId = await getFingerprintId(); return {
error:
"You have to authenticate or have a valid User Verification Check",
};
}
const verificationCheck = crypto // check if a verification was done earlier
.createHash("sha256") const hasValidUserVerificationCheck = await checkUserVerification(
.update(`${user.userId}:${userAgentId}`) session.session.factors.user.id,
.digest("hex"); );
const cookieValue = await cookiesList.get("verificationCheck")?.value; if (!hasValidUserVerificationCheck) {
return { error: "User Verification Check has to be done" };
if (!cookieValue) { }
return { error: "User Verification Check has to be done" };
}
if (cookieValue !== verificationCheck) {
return { error: "User Verification Check has to be done" };
} }
const [hostname, port] = host.split(":"); const [hostname, port] = host.split(":");

View File

@@ -25,16 +25,15 @@ import {
import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
import { User, UserState } from "@zitadel/proto/zitadel/user/v2/user_pb"; import { User, UserState } from "@zitadel/proto/zitadel/user/v2/user_pb";
import { SetPasswordRequestSchema } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; import { SetPasswordRequestSchema } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
import crypto from "crypto"; import { headers } from "next/headers";
import { cookies, headers } from "next/headers";
import { getNextUrl } from "../client"; import { getNextUrl } from "../client";
import { getSessionCookieById, getSessionCookieByLoginName } from "../cookies"; import { getSessionCookieById, getSessionCookieByLoginName } from "../cookies";
import { getFingerprintId } from "../fingerprint";
import { getServiceUrlFromHeaders } from "../service-url"; import { getServiceUrlFromHeaders } from "../service-url";
import { import {
checkEmailVerification, checkEmailVerification,
checkMFAFactors, checkMFAFactors,
checkPasswordChangeRequired, checkPasswordChangeRequired,
checkUserVerification,
} from "../verify-helper"; } from "../verify-helper";
type ResetPasswordCommand = { 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 // 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) { if (authmethods.authMethodTypes.length !== 0) {
return { return {
error: error:
@@ -336,21 +334,11 @@ export async function changePassword(command: {
} }
// check if a verification was done earlier // check if a verification was done earlier
const cookiesList = await cookies(); const hasValidUserVerificationCheck = await checkUserVerification(
const userAgentId = await getFingerprintId(); user.userId,
);
const verificationCheck = crypto if (!hasValidUserVerificationCheck) {
.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) {
return { error: "User Verification Check has to be done" }; return { error: "User Verification Check has to be done" };
} }
} }

View File

@@ -21,7 +21,7 @@ import { User } from "@zitadel/proto/zitadel/user/v2/user_pb";
import { cookies, headers } from "next/headers"; import { cookies, headers } from "next/headers";
import { getNextUrl } from "../client"; import { getNextUrl } from "../client";
import { getSessionCookieByLoginName } from "../cookies"; import { getSessionCookieByLoginName } from "../cookies";
import { getFingerprintId } from "../fingerprint"; import { getOrSetFingerprintId } from "../fingerprint";
import { getServiceUrlFromHeaders } from "../service-url"; import { getServiceUrlFromHeaders } from "../service-url";
import { loadMostRecentSession } from "../session"; import { loadMostRecentSession } from "../session";
import { checkMFAFactors } from "../verify-helper"; import { checkMFAFactors } from "../verify-helper";
@@ -197,11 +197,9 @@ export async function sendVerification(command: VerifyUserByEmailCommand) {
params.set("loginName", session.factors?.user?.loginName); 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 cookiesList = await cookies();
const userAgentId = await getOrSetFingerprintId();
const userAgentId = await getFingerprintId();
const verificationCheck = crypto const verificationCheck = crypto
.createHash("sha256") .createHash("sha256")

View File

@@ -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 { PasswordExpirySettings } from "@zitadel/proto/zitadel/settings/v2/password_settings_pb";
import { HumanUser } from "@zitadel/proto/zitadel/user/v2/user_pb"; import { HumanUser } from "@zitadel/proto/zitadel/user/v2/user_pb";
import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
import crypto from "crypto";
import moment from "moment"; import moment from "moment";
import { cookies } from "next/headers";
import { getOrSetFingerprintId } from "./fingerprint";
import { getUserByID } from "./zitadel"; import { getUserByID } from "./zitadel";
export function checkPasswordChangeRequired( export function checkPasswordChangeRequired(
@@ -44,7 +47,7 @@ export function checkPasswordChangeRequired(
} }
} }
export function checkInvite( export function checkEmailVerified(
session: Session, session: Session,
humanUser?: HumanUser, humanUser?: HumanUser,
organization?: string, organization?: string,
@@ -248,3 +251,32 @@ export async function checkMFAFactors(
return { redirect: `/mfa/set?` + params }; return { redirect: `/mfa/set?` + params };
} }
} }
export async function checkUserVerification(userId: string): Promise<boolean> {
// 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;
}