mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-12 17:37:32 +00:00
Merge pull request #464 from zitadel/verify-check-cookie
fix: Improve Invite flow by checking for same user agent and time limit
This commit is contained in:
@@ -181,6 +181,7 @@
|
||||
"description": "Geben Sie den Code ein, der in der Bestätigungs-E-Mail angegeben ist.",
|
||||
"noCodeReceived": "Keinen Code erhalten?",
|
||||
"resendCode": "Code erneut senden",
|
||||
"codeSent": "Ein Code wurde gerade an Ihre E-Mail-Adresse gesendet.",
|
||||
"submit": "Weiter"
|
||||
}
|
||||
},
|
||||
|
@@ -181,6 +181,7 @@
|
||||
"description": "Enter the Code provided in the verification email.",
|
||||
"noCodeReceived": "Didn't receive a code?",
|
||||
"resendCode": "Resend code",
|
||||
"codeSent": "A code has just been sent to your email address.",
|
||||
"submit": "Continue"
|
||||
}
|
||||
},
|
||||
|
@@ -181,6 +181,7 @@
|
||||
"description": "Introduce el código proporcionado en el correo electrónico de verificación.",
|
||||
"noCodeReceived": "¿No recibiste un código?",
|
||||
"resendCode": "Reenviar código",
|
||||
"codeSent": "Se ha enviado un código a tu dirección de correo electrónico.",
|
||||
"submit": "Continuar"
|
||||
}
|
||||
},
|
||||
|
@@ -181,6 +181,7 @@
|
||||
"description": "Inserisci il codice fornito nell'email di verifica.",
|
||||
"noCodeReceived": "Non hai ricevuto un codice?",
|
||||
"resendCode": "Invia di nuovo il codice",
|
||||
"codeSent": "Un codice è stato appena inviato al tuo indirizzo email.",
|
||||
"submit": "Continua"
|
||||
}
|
||||
},
|
||||
|
@@ -181,6 +181,7 @@
|
||||
"description": "Wprowadź kod z wiadomości weryfikacyjnej.",
|
||||
"noCodeReceived": "Nie otrzymałeś kodu?",
|
||||
"resendCode": "Wyślij kod ponownie",
|
||||
"codeSent": "Kod został właśnie wysłany na twój adres e-mail.",
|
||||
"submit": "Kontynuuj"
|
||||
}
|
||||
},
|
||||
|
@@ -181,6 +181,7 @@
|
||||
"description": "Введите код из письма подтверждения.",
|
||||
"noCodeReceived": "Не получили код?",
|
||||
"resendCode": "Отправить код повторно",
|
||||
"codeSent": "Код отправлен на ваш email.",
|
||||
"submit": "Продолжить"
|
||||
}
|
||||
},
|
||||
|
@@ -181,6 +181,7 @@
|
||||
"description": "输入验证邮件中的验证码。",
|
||||
"noCodeReceived": "没有收到验证码?",
|
||||
"resendCode": "重发验证码",
|
||||
"codeSent": "刚刚发送了一封包含验证码的电子邮件。",
|
||||
"submit": "继续"
|
||||
}
|
||||
},
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { Alert } from "@/components/alert";
|
||||
import { Alert, AlertType } from "@/components/alert";
|
||||
import { DynamicTheme } from "@/components/dynamic-theme";
|
||||
import { UserAvatar } from "@/components/user-avatar";
|
||||
import { VerifyForm } from "@/components/verify-form";
|
||||
@@ -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,
|
||||
@@ -22,7 +23,7 @@ export default async function Page(props: { searchParams: Promise<any> }) {
|
||||
const t = await getTranslations({ locale, namespace: "verify" });
|
||||
const tError = await getTranslations({ locale, namespace: "error" });
|
||||
|
||||
const { userId, loginName, code, organization, requestId, invite } =
|
||||
const { userId, loginName, code, organization, requestId, invite, send } =
|
||||
searchParams;
|
||||
|
||||
const _headers = await headers();
|
||||
@@ -43,7 +44,7 @@ export default async function Page(props: { searchParams: Promise<any> }) {
|
||||
let human: HumanUser | undefined;
|
||||
let id: string | undefined;
|
||||
|
||||
const doSend = invite !== "true";
|
||||
const doSend = send === "true";
|
||||
|
||||
const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? "";
|
||||
|
||||
@@ -61,7 +62,7 @@ export default async function Page(props: { searchParams: Promise<any> }) {
|
||||
serviceUrl,
|
||||
userId: sessionFactors?.factors?.user?.id,
|
||||
urlTemplate:
|
||||
`${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}&invite=true` +
|
||||
`${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}` +
|
||||
(requestId ? `&requestId=${requestId}` : ""),
|
||||
}).catch((error) => {
|
||||
console.error("Could not resend verification email", error);
|
||||
@@ -74,7 +75,7 @@ export default async function Page(props: { searchParams: Promise<any> }) {
|
||||
serviceUrl,
|
||||
userId,
|
||||
urlTemplate:
|
||||
`${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}&invite=true` +
|
||||
`${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}` +
|
||||
(requestId ? `&requestId=${requestId}` : ""),
|
||||
}).catch((error) => {
|
||||
console.error("Could not resend verification email", error);
|
||||
@@ -96,14 +97,23 @@ export default async function Page(props: { searchParams: Promise<any> }) {
|
||||
|
||||
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
|
||||
@@ -138,6 +148,12 @@ export default async function Page(props: { searchParams: Promise<any> }) {
|
||||
</>
|
||||
)}
|
||||
|
||||
{id && send && (
|
||||
<div className="py-4 w-full">
|
||||
<Alert type={AlertType.INFO}>{t("verify.codeSent")}</Alert>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{sessionFactors ? (
|
||||
<UserAvatar
|
||||
loginName={loginName ?? sessionFactors.factors?.user?.loginName}
|
||||
@@ -155,27 +171,27 @@ export default async function Page(props: { searchParams: Promise<any> }) {
|
||||
)
|
||||
)}
|
||||
|
||||
{id &&
|
||||
(human?.email?.isVerified ? (
|
||||
// show page for already verified users
|
||||
<VerifyRedirectButton
|
||||
userId={id}
|
||||
loginName={loginName}
|
||||
organization={organization}
|
||||
requestId={requestId}
|
||||
authMethods={authMethods}
|
||||
/>
|
||||
) : (
|
||||
// check if auth methods are set
|
||||
<VerifyForm
|
||||
loginName={loginName}
|
||||
organization={organization}
|
||||
userId={id}
|
||||
code={code}
|
||||
isInvite={invite === "true"}
|
||||
requestId={requestId}
|
||||
/>
|
||||
))}
|
||||
{/* 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
|
||||
<VerifyRedirectButton
|
||||
userId={id}
|
||||
loginName={loginName}
|
||||
organization={organization}
|
||||
requestId={requestId}
|
||||
authMethods={authMethods}
|
||||
/>
|
||||
) : (
|
||||
// check if auth methods are set
|
||||
<VerifyForm
|
||||
loginName={loginName}
|
||||
organization={organization}
|
||||
userId={id}
|
||||
code={code}
|
||||
isInvite={invite === "true"}
|
||||
requestId={requestId}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</DynamicTheme>
|
||||
);
|
||||
|
@@ -83,6 +83,16 @@ export function RegisterPasskey({
|
||||
return;
|
||||
}
|
||||
|
||||
if ("error" in resp && resp.error) {
|
||||
setError(resp.error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!("passkeyId" in resp)) {
|
||||
setError("An error on registering passkey");
|
||||
return;
|
||||
}
|
||||
|
||||
const passkeyId = resp.passkeyId;
|
||||
const options: CredentialCreationOptions =
|
||||
(resp.publicKeyCredentialCreationOptions as CredentialCreationOptions) ??
|
||||
@@ -92,6 +102,7 @@ export function RegisterPasskey({
|
||||
setError("An error on registering passkey");
|
||||
return;
|
||||
}
|
||||
|
||||
options.publicKey.challenge = coerceToArrayBuffer(
|
||||
options.publicKey.challenge,
|
||||
"challenge",
|
||||
|
@@ -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,31 @@ export async function sendLoginname(command: SendLoginnameCommand) {
|
||||
return inviteCheck;
|
||||
}
|
||||
|
||||
// check if user was verified recently
|
||||
const isUserVerified = await checkUserVerification(
|
||||
session.factors.user.id,
|
||||
);
|
||||
if (!isUserVerified) {
|
||||
const params = new URLSearchParams({
|
||||
loginName: session.factors?.user?.loginName as string,
|
||||
send: "true", // set this to true to request a new code immediately
|
||||
});
|
||||
|
||||
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
|
||||
|
@@ -5,10 +5,12 @@ import {
|
||||
getLoginSettings,
|
||||
getSession,
|
||||
getUserByID,
|
||||
listAuthenticationMethodTypes,
|
||||
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,
|
||||
@@ -23,7 +25,10 @@ import {
|
||||
getSessionCookieByLoginName,
|
||||
} from "../cookies";
|
||||
import { getServiceUrlFromHeaders } from "../service-url";
|
||||
import { checkEmailVerification } from "../verify-helper";
|
||||
import {
|
||||
checkEmailVerification,
|
||||
checkUserVerification,
|
||||
} from "../verify-helper";
|
||||
import { setSessionAndUpdateCookie } from "./cookie";
|
||||
|
||||
type VerifyPasskeyCommand = {
|
||||
@@ -37,9 +42,25 @@ type RegisterPasskeyCommand = {
|
||||
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(
|
||||
command: RegisterPasskeyCommand,
|
||||
): Promise<RegisterPasskeyResponse> {
|
||||
): Promise<RegisterPasskeyResponse | { error: string }> {
|
||||
const { sessionId } = command;
|
||||
|
||||
const _headers = await headers();
|
||||
@@ -57,6 +78,36 @@ export async function registerPasskeyLink(
|
||||
sessionToken: sessionCookie.token,
|
||||
});
|
||||
|
||||
if (!session?.session?.factors?.user?.id) {
|
||||
return { error: "Could not determine user from session" };
|
||||
}
|
||||
|
||||
const sessionValid = isSessionValid(session.session);
|
||||
|
||||
if (!sessionValid) {
|
||||
const authmethods = await listAuthenticationMethodTypes({
|
||||
serviceUrl,
|
||||
userId: session.session.factors.user.id,
|
||||
});
|
||||
|
||||
// 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",
|
||||
};
|
||||
}
|
||||
|
||||
// check if a verification was done earlier
|
||||
const hasValidUserVerificationCheck = await checkUserVerification(
|
||||
session.session.factors.user.id,
|
||||
);
|
||||
|
||||
if (!hasValidUserVerificationCheck) {
|
||||
return { error: "User Verification Check has to be done" };
|
||||
}
|
||||
}
|
||||
|
||||
const [hostname, port] = host.split(":");
|
||||
|
||||
if (!hostname) {
|
||||
|
@@ -13,7 +13,6 @@ import {
|
||||
listAuthenticationMethodTypes,
|
||||
listUsers,
|
||||
passwordReset,
|
||||
setPassword,
|
||||
setUserPassword,
|
||||
} from "@/lib/zitadel";
|
||||
import { ConnectError, create } from "@zitadel/client";
|
||||
@@ -25,10 +24,7 @@ import {
|
||||
} from "@zitadel/proto/zitadel/session/v2/session_service_pb";
|
||||
import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
|
||||
import { User, UserState } from "@zitadel/proto/zitadel/user/v2/user_pb";
|
||||
import {
|
||||
AuthenticationMethodType,
|
||||
SetPasswordRequestSchema,
|
||||
} from "@zitadel/proto/zitadel/user/v2/user_service_pb";
|
||||
import { SetPasswordRequestSchema } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
|
||||
import { headers } from "next/headers";
|
||||
import { getNextUrl } from "../client";
|
||||
import { getSessionCookieById, getSessionCookieByLoginName } from "../cookies";
|
||||
@@ -37,6 +33,7 @@ import {
|
||||
checkEmailVerification,
|
||||
checkMFAFactors,
|
||||
checkPasswordChangeRequired,
|
||||
checkUserVerification,
|
||||
} from "../verify-helper";
|
||||
|
||||
type ResetPasswordCommand = {
|
||||
@@ -297,6 +294,7 @@ export async function sendPassword(command: UpdateSessionCommand) {
|
||||
return { redirect: url };
|
||||
}
|
||||
|
||||
// this function lets users with code set a password or users with valid User Verification Check
|
||||
export async function changePassword(command: {
|
||||
code?: string;
|
||||
userId: string;
|
||||
@@ -316,11 +314,39 @@ export async function changePassword(command: {
|
||||
}
|
||||
const userId = user.userId;
|
||||
|
||||
if (user.state === UserState.INITIAL) {
|
||||
return { error: "User Initial State is not supported" };
|
||||
}
|
||||
|
||||
// check if the user has no password set in order to set a password
|
||||
if (!command.code) {
|
||||
const authmethods = await listAuthenticationMethodTypes({
|
||||
serviceUrl,
|
||||
userId,
|
||||
});
|
||||
|
||||
// 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 provide a code or have a valid User Verification Check",
|
||||
};
|
||||
}
|
||||
|
||||
// check if a verification was done earlier
|
||||
const hasValidUserVerificationCheck = await checkUserVerification(
|
||||
user.userId,
|
||||
);
|
||||
|
||||
if (!hasValidUserVerificationCheck) {
|
||||
return { error: "User Verification Check has to be done" };
|
||||
}
|
||||
}
|
||||
|
||||
return setUserPassword({
|
||||
serviceUrl,
|
||||
userId,
|
||||
password: command.password,
|
||||
user,
|
||||
code: command.code,
|
||||
});
|
||||
}
|
||||
@@ -366,67 +392,32 @@ export async function checkSessionAndSetPassword({
|
||||
return { error: "Could not load auth methods" };
|
||||
}
|
||||
|
||||
const requiredAuthMethodsForForceMFA = [
|
||||
AuthenticationMethodType.OTP_EMAIL,
|
||||
AuthenticationMethodType.OTP_SMS,
|
||||
AuthenticationMethodType.TOTP,
|
||||
AuthenticationMethodType.U2F,
|
||||
];
|
||||
|
||||
const hasNoMFAMethods = requiredAuthMethodsForForceMFA.every(
|
||||
(method) => !authmethods.authMethodTypes.includes(method),
|
||||
);
|
||||
|
||||
const loginSettings = await getLoginSettings({
|
||||
serviceUrl,
|
||||
organization: session.factors.user.organizationId,
|
||||
});
|
||||
|
||||
const forceMfa = !!(
|
||||
loginSettings?.forceMfa || loginSettings?.forceMfaLocalOnly
|
||||
);
|
||||
|
||||
// if the user has no MFA but MFA is enforced, we can set a password otherwise we use the token of the user
|
||||
if (forceMfa && hasNoMFAMethods) {
|
||||
return setPassword({ serviceUrl, payload }).catch((error) => {
|
||||
// throw error if failed precondition (ex. User is not yet initialized)
|
||||
if (error.code === 9 && error.message) {
|
||||
return { error: "Failed precondition" };
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
const transport = async (serviceUrl: string, token: string) => {
|
||||
return createServerTransport(token, {
|
||||
baseUrl: serviceUrl,
|
||||
});
|
||||
} else {
|
||||
const transport = async (serviceUrl: string, token: string) => {
|
||||
return createServerTransport(token, {
|
||||
baseUrl: serviceUrl,
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
const myUserService = async (serviceUrl: string, sessionToken: string) => {
|
||||
const transportPromise = await transport(serviceUrl, sessionToken);
|
||||
return createUserServiceClient(transportPromise);
|
||||
};
|
||||
const myUserService = async (serviceUrl: string, sessionToken: string) => {
|
||||
const transportPromise = await transport(serviceUrl, sessionToken);
|
||||
return createUserServiceClient(transportPromise);
|
||||
};
|
||||
|
||||
const selfService = await myUserService(
|
||||
serviceUrl,
|
||||
`${sessionCookie.token}`,
|
||||
);
|
||||
const selfService = await myUserService(serviceUrl, `${sessionCookie.token}`);
|
||||
|
||||
return selfService
|
||||
.setPassword(
|
||||
{
|
||||
userId: session.factors.user.id,
|
||||
newPassword: { password, changeRequired: false },
|
||||
},
|
||||
{},
|
||||
)
|
||||
.catch((error: ConnectError) => {
|
||||
console.log(error);
|
||||
if (error.code === 7) {
|
||||
return { error: "Session is not valid." };
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
return selfService
|
||||
.setPassword(
|
||||
{
|
||||
userId: session.factors.user.id,
|
||||
newPassword: { password, changeRequired: false },
|
||||
},
|
||||
{},
|
||||
)
|
||||
.catch((error: ConnectError) => {
|
||||
console.log(error);
|
||||
if (error.code === 7) {
|
||||
return { error: "Session is not valid." };
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
@@ -1,24 +1,26 @@
|
||||
"use server";
|
||||
|
||||
import {
|
||||
createInviteCode,
|
||||
getLoginSettings,
|
||||
getSession,
|
||||
getUserByID,
|
||||
listAuthenticationMethodTypes,
|
||||
resendEmailCode,
|
||||
resendInviteCode,
|
||||
verifyEmail,
|
||||
verifyInviteCode,
|
||||
verifyTOTPRegistration,
|
||||
sendEmailCode as zitadelSendEmailCode,
|
||||
} from "@/lib/zitadel";
|
||||
import crypto from "crypto";
|
||||
|
||||
import { create } from "@zitadel/client";
|
||||
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
|
||||
import { ChecksSchema } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
|
||||
import { User } from "@zitadel/proto/zitadel/user/v2/user_pb";
|
||||
import { headers } from "next/headers";
|
||||
import { cookies, headers } from "next/headers";
|
||||
import { getNextUrl } from "../client";
|
||||
import { getSessionCookieByLoginName } from "../cookies";
|
||||
import { getOrSetFingerprintId } from "../fingerprint";
|
||||
import { getServiceUrlFromHeaders } from "../service-url";
|
||||
import { loadMostRecentSession } from "../session";
|
||||
import { checkMFAFactors } from "../verify-helper";
|
||||
@@ -193,6 +195,24 @@ export async function sendVerification(command: VerifyUserByEmailCommand) {
|
||||
if (session.factors?.user?.loginName) {
|
||||
params.set("loginName", session.factors?.user?.loginName);
|
||||
}
|
||||
|
||||
// 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 getOrSetFingerprintId();
|
||||
|
||||
const verificationCheck = crypto
|
||||
.createHash("sha256")
|
||||
.update(`${user.userId}:${userAgentId}`)
|
||||
.digest("hex");
|
||||
|
||||
await cookiesList.set({
|
||||
name: "verificationCheck",
|
||||
value: verificationCheck,
|
||||
httpOnly: true,
|
||||
path: "/",
|
||||
maxAge: 300, // 5 minutes
|
||||
});
|
||||
|
||||
return { redirect: `/authenticator/set?${params}` };
|
||||
}
|
||||
|
||||
@@ -253,20 +273,26 @@ export async function resendVerification(command: resendVerifyEmailCommand) {
|
||||
|
||||
const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? "";
|
||||
|
||||
// create a new invite whenever the resend is called
|
||||
return command.isInvite
|
||||
? resendInviteCode({ serviceUrl, userId: command.userId })
|
||||
: resendEmailCode({
|
||||
? createInviteCode({
|
||||
serviceUrl,
|
||||
userId: command.userId,
|
||||
urlTemplate:
|
||||
`${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}&invite=true` +
|
||||
(command.requestId ? `&requestId=${command.requestId}` : ""),
|
||||
}) //resendInviteCode({ serviceUrl, userId: command.userId })
|
||||
: sendEmailCode({
|
||||
userId: command.userId,
|
||||
serviceUrl,
|
||||
urlTemplate:
|
||||
`${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}/password/set?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}` +
|
||||
`${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}` +
|
||||
(command.requestId ? `&requestId=${command.requestId}` : ""),
|
||||
});
|
||||
}
|
||||
|
||||
type sendEmailCommand = {
|
||||
serviceUrl: string;
|
||||
|
||||
userId: string;
|
||||
urlTemplate: string;
|
||||
};
|
||||
|
@@ -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 { getFingerprintIdCookie } 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,
|
||||
@@ -54,7 +57,7 @@ export function checkInvite(
|
||||
const paramsVerify = new URLSearchParams({
|
||||
loginName: session.factors?.user?.loginName as string,
|
||||
userId: session.factors?.user?.id as string, // verify needs user id
|
||||
invite: "true", // TODO: check - set this to true as we dont expect old email verification method here
|
||||
send: "true", // we request a new email code once the page is loaded
|
||||
});
|
||||
|
||||
if (organization || session.factors?.user?.organizationId) {
|
||||
@@ -84,6 +87,7 @@ export function checkEmailVerification(
|
||||
) {
|
||||
const params = new URLSearchParams({
|
||||
loginName: session.factors?.user?.loginName as string,
|
||||
send: "true", // set this to true as we dont expect old email codes to be valid anymore
|
||||
});
|
||||
|
||||
if (requestId) {
|
||||
@@ -248,3 +252,38 @@ export async function checkMFAFactors(
|
||||
return { redirect: `/mfa/set?` + params };
|
||||
}
|
||||
}
|
||||
|
||||
export async function checkUserVerification(userId: string): Promise<boolean> {
|
||||
// check if a verification was done earlier
|
||||
const cookiesList = await cookies();
|
||||
|
||||
// only read cookie to prevent issues on page.tsx
|
||||
const fingerPrintCookie = await getFingerprintIdCookie();
|
||||
|
||||
if (!fingerPrintCookie || !fingerPrintCookie.value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const verificationCheck = crypto
|
||||
.createHash("sha256")
|
||||
.update(`${userId}:${fingerPrintCookie.value}`)
|
||||
.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;
|
||||
}
|
||||
|
@@ -29,11 +29,7 @@ import {
|
||||
SearchQuery,
|
||||
SearchQuerySchema,
|
||||
} from "@zitadel/proto/zitadel/user/v2/query_pb";
|
||||
import {
|
||||
SendInviteCodeSchema,
|
||||
User,
|
||||
UserState,
|
||||
} from "@zitadel/proto/zitadel/user/v2/user_pb";
|
||||
import { SendInviteCodeSchema } from "@zitadel/proto/zitadel/user/v2/user_pb";
|
||||
import {
|
||||
AddHumanUserRequest,
|
||||
ResendEmailCodeRequest,
|
||||
@@ -1170,13 +1166,11 @@ export async function setUserPassword({
|
||||
serviceUrl,
|
||||
userId,
|
||||
password,
|
||||
user,
|
||||
code,
|
||||
}: {
|
||||
serviceUrl: string;
|
||||
userId: string;
|
||||
password: string;
|
||||
user: User;
|
||||
code?: string;
|
||||
}) {
|
||||
let payload = create(SetPasswordRequestSchema, {
|
||||
@@ -1186,22 +1180,6 @@ export async function setUserPassword({
|
||||
},
|
||||
});
|
||||
|
||||
// check if the user has no password set in order to set a password
|
||||
if (!code) {
|
||||
const authmethods = await listAuthenticationMethodTypes({
|
||||
serviceUrl,
|
||||
userId,
|
||||
});
|
||||
|
||||
// if the user has no authmethods set, we can set a password otherwise we need a code
|
||||
if (
|
||||
!(authmethods.authMethodTypes.length === 0) &&
|
||||
user.state !== UserState.INITIAL
|
||||
) {
|
||||
return { error: "Provide a code to set a password" };
|
||||
}
|
||||
}
|
||||
|
||||
if (code) {
|
||||
payload = {
|
||||
...payload,
|
||||
|
Reference in New Issue
Block a user