mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-13 10:57:32 +00:00
helper functions
This commit is contained in:
@@ -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>
|
||||||
);
|
);
|
||||||
|
@@ -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
|
||||||
|
@@ -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(":");
|
||||||
|
@@ -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" };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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")
|
||||||
|
@@ -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;
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user