mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-24 23:18:04 +00:00
fix: verify email
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { Alert, AlertType } from "@/components/alert";
|
||||
import { resendVerification, sendVerification } from "@/lib/server/email";
|
||||
import { resendVerification, sendVerification } from "@/lib/server/verify";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { sendVerificationRedirectWithoutCheck } from "@/lib/server/email";
|
||||
import { sendVerificationRedirectWithoutCheck } from "@/lib/server/verify";
|
||||
import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useState } from "react";
|
||||
|
@@ -1,138 +0,0 @@
|
||||
"use server";
|
||||
|
||||
import {
|
||||
getUserByID,
|
||||
listAuthenticationMethodTypes,
|
||||
resendEmailCode,
|
||||
resendInviteCode,
|
||||
verifyEmail,
|
||||
verifyInviteCode,
|
||||
} from "@/lib/zitadel";
|
||||
import { create } from "@zitadel/client";
|
||||
import { ChecksSchema } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
|
||||
import { createSessionAndUpdateCookie } from "./cookie";
|
||||
|
||||
type VerifyUserByEmailCommand = {
|
||||
userId: string;
|
||||
code: string;
|
||||
isInvite: boolean;
|
||||
authRequestId?: string;
|
||||
};
|
||||
|
||||
export async function sendVerification(command: VerifyUserByEmailCommand) {
|
||||
const verifyResponse = command.isInvite
|
||||
? await verifyInviteCode(command.userId, command.code).catch(() => {
|
||||
return { error: "Could not verify invite" };
|
||||
})
|
||||
: await verifyEmail(command.userId, command.code).catch(() => {
|
||||
return { error: "Could not verify email" };
|
||||
});
|
||||
|
||||
if (!verifyResponse) {
|
||||
return { error: "Could not verify user" };
|
||||
}
|
||||
|
||||
const userResponse = await getUserByID(command.userId);
|
||||
|
||||
if (!userResponse || !userResponse.user) {
|
||||
return { error: "Could not load user" };
|
||||
}
|
||||
|
||||
const checks = create(ChecksSchema, {
|
||||
user: {
|
||||
search: {
|
||||
case: "loginName",
|
||||
value: userResponse.user.preferredLoginName,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const session = await createSessionAndUpdateCookie(
|
||||
checks,
|
||||
undefined,
|
||||
command.authRequestId,
|
||||
);
|
||||
|
||||
const authMethodResponse = await listAuthenticationMethodTypes(
|
||||
command.userId,
|
||||
);
|
||||
|
||||
if (!authMethodResponse || !authMethodResponse.authMethodTypes) {
|
||||
return { error: "Could not load possible authenticators" };
|
||||
}
|
||||
// if no authmethods are found on the user, redirect to set one up
|
||||
if (
|
||||
authMethodResponse &&
|
||||
authMethodResponse.authMethodTypes &&
|
||||
authMethodResponse.authMethodTypes.length == 0
|
||||
) {
|
||||
const params = new URLSearchParams({
|
||||
sessionId: session.id,
|
||||
});
|
||||
|
||||
if (session.factors?.user?.loginName) {
|
||||
params.set("loginName", session.factors?.user?.loginName);
|
||||
}
|
||||
return { redirect: `/authenticator/set?${params}` };
|
||||
}
|
||||
}
|
||||
|
||||
type resendVerifyEmailCommand = {
|
||||
userId: string;
|
||||
isInvite: boolean;
|
||||
};
|
||||
|
||||
export async function resendVerification(command: resendVerifyEmailCommand) {
|
||||
return command.isInvite
|
||||
? resendInviteCode(command.userId)
|
||||
: resendEmailCode(command.userId);
|
||||
}
|
||||
|
||||
export async function sendVerificationRedirectWithoutCheck(command: {
|
||||
userId: string;
|
||||
authRequestId?: string;
|
||||
}) {
|
||||
const userResponse = await getUserByID(command.userId);
|
||||
|
||||
if (!userResponse || !userResponse.user) {
|
||||
return { error: "Could not load user" };
|
||||
}
|
||||
|
||||
const checks = create(ChecksSchema, {
|
||||
user: {
|
||||
search: {
|
||||
case: "loginName",
|
||||
value: userResponse.user.preferredLoginName,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const session = await createSessionAndUpdateCookie(
|
||||
checks,
|
||||
undefined,
|
||||
command.authRequestId,
|
||||
);
|
||||
|
||||
const authMethodResponse = await listAuthenticationMethodTypes(
|
||||
command.userId,
|
||||
);
|
||||
|
||||
if (!authMethodResponse || !authMethodResponse.authMethodTypes) {
|
||||
return { error: "Could not load possible authenticators" };
|
||||
}
|
||||
// if no authmethods are found on the user, redirect to set one up
|
||||
if (
|
||||
authMethodResponse &&
|
||||
authMethodResponse.authMethodTypes &&
|
||||
authMethodResponse.authMethodTypes.length == 0
|
||||
) {
|
||||
const params = new URLSearchParams({
|
||||
sessionId: session.id,
|
||||
});
|
||||
|
||||
if (session.factors?.user?.loginName) {
|
||||
params.set("loginName", session.factors?.user?.loginName);
|
||||
}
|
||||
return { redirect: `/authenticator/set?${params}` };
|
||||
}
|
||||
}
|
@@ -17,6 +17,7 @@ import {
|
||||
import { create } from "@zitadel/client";
|
||||
import { createUserServiceClient } from "@zitadel/client/v2";
|
||||
import { createServerTransport } from "@zitadel/node";
|
||||
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
|
||||
import {
|
||||
Checks,
|
||||
ChecksSchema,
|
||||
@@ -168,108 +169,43 @@ export async function sendPassword(command: UpdateSessionCommand) {
|
||||
return { redirect: "/password/change?" + params };
|
||||
}
|
||||
|
||||
const availableMultiFactors = authMethods?.filter(
|
||||
(m: AuthenticationMethodType) =>
|
||||
m !== AuthenticationMethodType.PASSWORD &&
|
||||
m !== AuthenticationMethodType.PASSKEY,
|
||||
// throw error if user is in initial state here and do not continue
|
||||
|
||||
if (user.state === UserState.INITIAL) {
|
||||
return { error: "Initial User not supported" };
|
||||
}
|
||||
|
||||
// TODO add check to see if user was verified
|
||||
|
||||
if (!humanUser?.email?.isVerified) {
|
||||
const params = new URLSearchParams({
|
||||
loginName: session.factors?.user?.loginName as string,
|
||||
});
|
||||
|
||||
if (command.authRequestId) {
|
||||
params.append("authRequestId", command.authRequestId);
|
||||
}
|
||||
|
||||
if (command.organization || session.factors?.user?.organizationId) {
|
||||
params.append(
|
||||
"organization",
|
||||
command.organization ??
|
||||
(session.factors?.user?.organizationId as string),
|
||||
);
|
||||
}
|
||||
|
||||
return { redirect: `/verify` + params };
|
||||
}
|
||||
|
||||
checkMFAFactors(
|
||||
session,
|
||||
loginSettings,
|
||||
authMethods,
|
||||
command.organization,
|
||||
command.authRequestId,
|
||||
);
|
||||
|
||||
if (availableMultiFactors?.length == 1) {
|
||||
const params = new URLSearchParams({
|
||||
loginName: session.factors?.user.loginName,
|
||||
});
|
||||
|
||||
if (command.authRequestId) {
|
||||
params.append("authRequestId", command.authRequestId);
|
||||
}
|
||||
|
||||
if (command.organization || session.factors?.user?.organizationId) {
|
||||
params.append(
|
||||
"organization",
|
||||
command.organization ?? session.factors?.user?.organizationId,
|
||||
);
|
||||
}
|
||||
|
||||
const factor = availableMultiFactors[0];
|
||||
// if passwordless is other method, but user selected password as alternative, perform a login
|
||||
if (factor === AuthenticationMethodType.TOTP) {
|
||||
return { redirect: `/otp/time-based?` + params };
|
||||
} else if (factor === AuthenticationMethodType.OTP_SMS) {
|
||||
return { redirect: `/otp/sms?` + params };
|
||||
} else if (factor === AuthenticationMethodType.OTP_EMAIL) {
|
||||
return { redirect: `/otp/email?` + params };
|
||||
} else if (factor === AuthenticationMethodType.U2F) {
|
||||
return { redirect: `/u2f?` + params };
|
||||
}
|
||||
} else if (availableMultiFactors?.length >= 1) {
|
||||
const params = new URLSearchParams({
|
||||
loginName: session.factors.user.loginName,
|
||||
});
|
||||
|
||||
if (command.authRequestId) {
|
||||
params.append("authRequestId", command.authRequestId);
|
||||
}
|
||||
|
||||
if (command.organization || session.factors?.user?.organizationId) {
|
||||
params.append(
|
||||
"organization",
|
||||
command.organization ?? session.factors?.user?.organizationId,
|
||||
);
|
||||
}
|
||||
|
||||
return { redirect: `/mfa?` + params };
|
||||
}
|
||||
// TODO: check if handling of userstate INITIAL is needed
|
||||
else if (user.state === UserState.INITIAL) {
|
||||
return { error: "Initial User not supported" };
|
||||
} else if (
|
||||
(loginSettings?.forceMfa || loginSettings?.forceMfaLocalOnly) &&
|
||||
!availableMultiFactors.length
|
||||
) {
|
||||
const params = new URLSearchParams({
|
||||
loginName: session.factors.user.loginName,
|
||||
force: "true", // this defines if the mfa is forced in the settings
|
||||
checkAfter: "true", // this defines if the check is directly made after the setup
|
||||
});
|
||||
|
||||
if (command.authRequestId) {
|
||||
params.append("authRequestId", command.authRequestId);
|
||||
}
|
||||
|
||||
if (command.organization || session.factors?.user?.organizationId) {
|
||||
params.append(
|
||||
"organization",
|
||||
command.organization ?? session.factors?.user?.organizationId,
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: provide a way to setup passkeys on mfa page?
|
||||
return { redirect: `/mfa/set?` + params };
|
||||
}
|
||||
// TODO: implement passkey setup
|
||||
|
||||
// else if (
|
||||
// submitted.factors &&
|
||||
// !submitted.factors.webAuthN && // if session was not verified with a passkey
|
||||
// promptPasswordless && // if explicitly prompted due policy
|
||||
// !isAlternative // escaped if password was used as an alternative method
|
||||
// ) {
|
||||
// const params = new URLSearchParams({
|
||||
// loginName: submitted.factors.user.loginName,
|
||||
// prompt: "true",
|
||||
// });
|
||||
|
||||
// if (authRequestId) {
|
||||
// params.append("authRequestId", authRequestId);
|
||||
// }
|
||||
|
||||
// if (organization) {
|
||||
// params.append("organization", organization);
|
||||
// }
|
||||
|
||||
// return router.push(`/passkey/set?` + params);
|
||||
// }
|
||||
else if (command.authRequestId && session.id) {
|
||||
if (command.authRequestId && session.id) {
|
||||
const nextUrl = await getNextUrl(
|
||||
{
|
||||
sessionId: session.id,
|
||||
@@ -403,3 +339,110 @@ export async function checkSessionAndSetPassword({
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function checkMFAFactors(
|
||||
session: Session,
|
||||
loginSettings: LoginSettings | undefined,
|
||||
authMethods: AuthenticationMethodType[],
|
||||
organization?: string,
|
||||
authRequestId?: string,
|
||||
) {
|
||||
const availableMultiFactors = authMethods?.filter(
|
||||
(m: AuthenticationMethodType) =>
|
||||
m !== AuthenticationMethodType.PASSWORD &&
|
||||
m !== AuthenticationMethodType.PASSKEY,
|
||||
);
|
||||
|
||||
if (availableMultiFactors?.length == 1) {
|
||||
const params = new URLSearchParams({
|
||||
loginName: session.factors?.user?.loginName as string,
|
||||
});
|
||||
|
||||
if (authRequestId) {
|
||||
params.append("authRequestId", authRequestId);
|
||||
}
|
||||
|
||||
if (organization || session.factors?.user?.organizationId) {
|
||||
params.append(
|
||||
"organization",
|
||||
organization ?? (session.factors?.user?.organizationId as string),
|
||||
);
|
||||
}
|
||||
|
||||
const factor = availableMultiFactors[0];
|
||||
// if passwordless is other method, but user selected password as alternative, perform a login
|
||||
if (factor === AuthenticationMethodType.TOTP) {
|
||||
return { redirect: `/otp/time-based?` + params };
|
||||
} else if (factor === AuthenticationMethodType.OTP_SMS) {
|
||||
return { redirect: `/otp/sms?` + params };
|
||||
} else if (factor === AuthenticationMethodType.OTP_EMAIL) {
|
||||
return { redirect: `/otp/email?` + params };
|
||||
} else if (factor === AuthenticationMethodType.U2F) {
|
||||
return { redirect: `/u2f?` + params };
|
||||
}
|
||||
} else if (availableMultiFactors?.length >= 1) {
|
||||
const params = new URLSearchParams({
|
||||
loginName: session.factors?.user?.loginName as string,
|
||||
});
|
||||
|
||||
if (authRequestId) {
|
||||
params.append("authRequestId", authRequestId);
|
||||
}
|
||||
|
||||
if (organization || session.factors?.user?.organizationId) {
|
||||
params.append(
|
||||
"organization",
|
||||
organization ?? (session.factors?.user?.organizationId as string),
|
||||
);
|
||||
}
|
||||
|
||||
return { redirect: `/mfa?` + params };
|
||||
} else if (
|
||||
(loginSettings?.forceMfa || loginSettings?.forceMfaLocalOnly) &&
|
||||
!availableMultiFactors.length
|
||||
) {
|
||||
const params = new URLSearchParams({
|
||||
loginName: session.factors?.user?.loginName as string,
|
||||
force: "true", // this defines if the mfa is forced in the settings
|
||||
checkAfter: "true", // this defines if the check is directly made after the setup
|
||||
});
|
||||
|
||||
if (authRequestId) {
|
||||
params.append("authRequestId", authRequestId);
|
||||
}
|
||||
|
||||
if (organization || session.factors?.user?.organizationId) {
|
||||
params.append(
|
||||
"organization",
|
||||
organization ?? (session.factors?.user?.organizationId as string),
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: provide a way to setup passkeys on mfa page?
|
||||
return { redirect: `/mfa/set?` + params };
|
||||
}
|
||||
|
||||
// TODO: implement passkey setup
|
||||
|
||||
// else if (
|
||||
// submitted.factors &&
|
||||
// !submitted.factors.webAuthN && // if session was not verified with a passkey
|
||||
// promptPasswordless && // if explicitly prompted due policy
|
||||
// !isAlternative // escaped if password was used as an alternative method
|
||||
// ) {
|
||||
// const params = new URLSearchParams({
|
||||
// loginName: submitted.factors.user.loginName,
|
||||
// prompt: "true",
|
||||
// });
|
||||
|
||||
// if (authRequestId) {
|
||||
// params.append("authRequestId", authRequestId);
|
||||
// }
|
||||
|
||||
// if (organization) {
|
||||
// params.append("organization", organization);
|
||||
// }
|
||||
|
||||
// return router.push(`/passkey/set?` + params);
|
||||
// }
|
||||
}
|
||||
|
229
apps/login/src/lib/server/verify.ts
Normal file
229
apps/login/src/lib/server/verify.ts
Normal file
@@ -0,0 +1,229 @@
|
||||
"use server";
|
||||
|
||||
import {
|
||||
getLoginSettings,
|
||||
getUserByID,
|
||||
listAuthenticationMethodTypes,
|
||||
listUsers,
|
||||
resendEmailCode,
|
||||
resendInviteCode,
|
||||
verifyEmail,
|
||||
verifyInviteCode,
|
||||
} from "@/lib/zitadel";
|
||||
import { create } from "@zitadel/client";
|
||||
import { ChecksSchema } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
|
||||
import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
|
||||
import { User } from "@zitadel/proto/zitadel/user/v2/user_pb";
|
||||
import { getSessionCookieByLoginName } from "../cookies";
|
||||
import {
|
||||
createSessionAndUpdateCookie,
|
||||
setSessionAndUpdateCookie,
|
||||
} from "./cookie";
|
||||
|
||||
type VerifyUserByEmailCommand = {
|
||||
userId: string;
|
||||
code: string;
|
||||
isInvite: boolean;
|
||||
authRequestId?: string;
|
||||
};
|
||||
|
||||
export async function sendVerification(command: VerifyUserByEmailCommand) {
|
||||
const verifyResponse = command.isInvite
|
||||
? await verifyInviteCode(command.userId, command.code).catch(() => {
|
||||
return { error: "Could not verify invite" };
|
||||
})
|
||||
: await verifyEmail(command.userId, command.code).catch(() => {
|
||||
return { error: "Could not verify email" };
|
||||
});
|
||||
|
||||
if (!verifyResponse) {
|
||||
return { error: "Could not verify user" };
|
||||
}
|
||||
|
||||
const userResponse = await getUserByID(command.userId);
|
||||
|
||||
if (!userResponse || !userResponse.user) {
|
||||
return { error: "Could not load user" };
|
||||
}
|
||||
|
||||
const checks = create(ChecksSchema, {
|
||||
user: {
|
||||
search: {
|
||||
case: "loginName",
|
||||
value: userResponse.user.preferredLoginName,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const session = await createSessionAndUpdateCookie(
|
||||
checks,
|
||||
undefined,
|
||||
command.authRequestId,
|
||||
);
|
||||
|
||||
const authMethodResponse = await listAuthenticationMethodTypes(
|
||||
command.userId,
|
||||
);
|
||||
|
||||
if (!authMethodResponse || !authMethodResponse.authMethodTypes) {
|
||||
return { error: "Could not load possible authenticators" };
|
||||
}
|
||||
// if no authmethods are found on the user, redirect to set one up
|
||||
if (
|
||||
authMethodResponse &&
|
||||
authMethodResponse.authMethodTypes &&
|
||||
authMethodResponse.authMethodTypes.length == 0
|
||||
) {
|
||||
const params = new URLSearchParams({
|
||||
sessionId: session.id,
|
||||
});
|
||||
|
||||
if (session.factors?.user?.loginName) {
|
||||
params.set("loginName", session.factors?.user?.loginName);
|
||||
}
|
||||
return { redirect: `/authenticator/set?${params}` };
|
||||
}
|
||||
}
|
||||
|
||||
type resendVerifyEmailCommand = {
|
||||
userId: string;
|
||||
isInvite: boolean;
|
||||
};
|
||||
|
||||
export async function resendVerification(command: resendVerifyEmailCommand) {
|
||||
return command.isInvite
|
||||
? resendInviteCode(command.userId)
|
||||
: resendEmailCode(command.userId);
|
||||
}
|
||||
|
||||
type SendVerificationRedirectWithoutCheckCommand =
|
||||
| {
|
||||
loginName: string;
|
||||
organization?: string;
|
||||
authRequestId?: string;
|
||||
}
|
||||
| {
|
||||
userId: string;
|
||||
authRequestId?: string;
|
||||
};
|
||||
|
||||
export async function sendVerificationRedirectWithoutCheck(
|
||||
command: SendVerificationRedirectWithoutCheckCommand,
|
||||
) {
|
||||
if (!("loginName" in command || "userId" in command)) {
|
||||
return { error: "No userId, nor loginname provided" };
|
||||
}
|
||||
|
||||
let sessionCookie;
|
||||
let loginSettings: LoginSettings | undefined;
|
||||
let session;
|
||||
let user: User;
|
||||
|
||||
if ("loginName" in command) {
|
||||
sessionCookie = await getSessionCookieByLoginName({
|
||||
loginName: command.loginName,
|
||||
organization: command.organization,
|
||||
}).catch((error) => {
|
||||
console.warn("Ignored error:", error);
|
||||
});
|
||||
} else if (command.userId) {
|
||||
const users = await listUsers({
|
||||
loginName: command.loginName,
|
||||
organizationId: command.organization,
|
||||
});
|
||||
|
||||
if (users.details?.totalResult == BigInt(1) && users.result[0].userId) {
|
||||
user = users.result[0];
|
||||
|
||||
const checks = create(ChecksSchema, {
|
||||
user: { search: { case: "userId", value: users.result[0].userId } },
|
||||
password: { password: command.checks.password?.password },
|
||||
});
|
||||
|
||||
loginSettings = await getLoginSettings(command.organization);
|
||||
|
||||
session = await createSessionAndUpdateCookie(
|
||||
checks,
|
||||
undefined,
|
||||
command.authRequestId,
|
||||
loginSettings?.passwordCheckLifetime,
|
||||
);
|
||||
}
|
||||
|
||||
// this is a fake error message to hide that the user does not even exist
|
||||
return { error: "Could not verify password" };
|
||||
} else {
|
||||
session = await setSessionAndUpdateCookie(
|
||||
sessionCookie,
|
||||
command.checks,
|
||||
undefined,
|
||||
command.authRequestId,
|
||||
loginSettings?.passwordCheckLifetime,
|
||||
);
|
||||
|
||||
if (!session?.factors?.user?.id) {
|
||||
return { error: "Could not create session for user" };
|
||||
}
|
||||
|
||||
const userResponse = await getUserByID(session?.factors?.user?.id);
|
||||
|
||||
if (!userResponse.user) {
|
||||
return { error: "Could not find user" };
|
||||
}
|
||||
|
||||
user = userResponse.user;
|
||||
}
|
||||
|
||||
if (!loginSettings) {
|
||||
loginSettings = await getLoginSettings(
|
||||
command.organization ?? session.factors?.user?.organizationId,
|
||||
);
|
||||
}
|
||||
|
||||
if (!session?.factors?.user?.id || !sessionCookie) {
|
||||
return { error: "Could not create session for user" };
|
||||
}
|
||||
// const userResponse = await getUserByID(command.userId);
|
||||
|
||||
// if (!userResponse || !userResponse.user) {
|
||||
// return { error: "Could not load user" };
|
||||
// }
|
||||
|
||||
// const checks = create(ChecksSchema, {
|
||||
// user: {
|
||||
// search: {
|
||||
// case: "loginName",
|
||||
// value: userResponse.user.preferredLoginName,
|
||||
// },
|
||||
// },
|
||||
// });
|
||||
|
||||
// const session = await createSessionAndUpdateCookie(
|
||||
// checks,
|
||||
// undefined,
|
||||
// command.authRequestId,
|
||||
// );
|
||||
|
||||
const authMethodResponse = await listAuthenticationMethodTypes(
|
||||
command.userId,
|
||||
);
|
||||
|
||||
if (!authMethodResponse || !authMethodResponse.authMethodTypes) {
|
||||
return { error: "Could not load possible authenticators" };
|
||||
}
|
||||
// if no authmethods are found on the user, redirect to set one up
|
||||
if (
|
||||
authMethodResponse &&
|
||||
authMethodResponse.authMethodTypes &&
|
||||
authMethodResponse.authMethodTypes.length == 0
|
||||
) {
|
||||
const params = new URLSearchParams({
|
||||
sessionId: session.id,
|
||||
});
|
||||
|
||||
if (session.factors?.user?.loginName) {
|
||||
params.set("loginName", session.factors?.user?.loginName);
|
||||
}
|
||||
return { redirect: `/authenticator/set?${params}` };
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user