context on verification pages

This commit is contained in:
Max Peintner
2024-12-19 08:59:39 +01:00
parent 4bb03574e6
commit 6cd0e7cb18
5 changed files with 164 additions and 104 deletions

View File

@@ -3,6 +3,7 @@ import { DynamicTheme } from "@/components/dynamic-theme";
import { UserAvatar } from "@/components/user-avatar"; import { UserAvatar } from "@/components/user-avatar";
import { VerifyForm } from "@/components/verify-form"; import { VerifyForm } from "@/components/verify-form";
import { VerifyRedirectButton } from "@/components/verify-redirect-button"; import { VerifyRedirectButton } from "@/components/verify-redirect-button";
import { loadMostRecentSession } from "@/lib/session";
import { import {
getBrandingSettings, getBrandingSettings,
getUserByID, getUserByID,
@@ -23,9 +24,17 @@ export default async function Page(props: { searchParams: Promise<any> }) {
const branding = await getBrandingSettings(organization); const branding = await getBrandingSettings(organization);
let sessionFactors;
let user: User | undefined; let user: User | undefined;
let human: HumanUser | undefined; let human: HumanUser | undefined;
if (userId) { let id: string | undefined;
if ("loginName" in searchParams) {
sessionFactors = await loadMostRecentSession({
loginName,
organization,
});
} else if ("userId" in searchParams && userId) {
const userResponse = await getUserByID(userId); const userResponse = await getUserByID(userId);
if (userResponse) { if (userResponse) {
user = userResponse.user; user = userResponse.user;
@@ -35,6 +44,8 @@ export default async function Page(props: { searchParams: Promise<any> }) {
} }
} }
id = userId ?? sessionFactors?.factors?.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(userId);
@@ -66,7 +77,7 @@ export default async function Page(props: { searchParams: Promise<any> }) {
<h1>{t("verify.title")}</h1> <h1>{t("verify.title")}</h1>
<p className="ztdl-p mb-6 block">{t("verify.description")}</p> <p className="ztdl-p mb-6 block">{t("verify.description")}</p>
{!userId && ( {!id && (
<> <>
<h1>{t("verify.title")}</h1> <h1>{t("verify.title")}</h1>
<p className="ztdl-p mb-6 block">{t("verify.description")}</p> <p className="ztdl-p mb-6 block">{t("verify.description")}</p>
@@ -85,21 +96,26 @@ export default async function Page(props: { searchParams: Promise<any> }) {
/> />
)} )}
{human?.email?.isVerified ? ( {id &&
<VerifyRedirectButton (human?.email?.isVerified ? (
userId={userId} // show page for already verified users
authRequestId={authRequestId} <VerifyRedirectButton
authMethods={authMethods} userId={id}
/> loginName={loginName}
) : ( organization={organization}
// check if auth methods are set authRequestId={authRequestId}
<VerifyForm authMethods={authMethods}
userId={userId} />
code={code} ) : (
isInvite={invite === "true"} // check if auth methods are set
params={params} <VerifyForm
/> loginName={loginName}
)} userId={id}
code={code}
isInvite={invite === "true"}
params={params}
/>
))}
</div> </div>
</DynamicTheme> </DynamicTheme>
); );

View File

@@ -17,12 +17,19 @@ type Inputs = {
type Props = { type Props = {
userId: string; userId: string;
loginName?: string;
code?: string; code?: string;
isInvite: boolean; isInvite: boolean;
params: URLSearchParams; params: URLSearchParams;
}; };
export function VerifyForm({ userId, code, isInvite, params }: Props) { export function VerifyForm({
userId,
loginName,
code,
isInvite,
params,
}: Props) {
const t = useTranslations("verify"); const t = useTranslations("verify");
const router = useRouter(); const router = useRouter();

View File

@@ -1,6 +1,9 @@
"use client"; "use client";
import { sendVerificationRedirectWithoutCheck } from "@/lib/server/verify"; import {
sendVerificationRedirectWithoutCheck,
SendVerificationRedirectWithoutCheckCommand,
} from "@/lib/server/verify";
import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { useState } from "react"; import { useState } from "react";
@@ -11,12 +14,16 @@ import { Spinner } from "./spinner";
export function VerifyRedirectButton({ export function VerifyRedirectButton({
userId, userId,
loginName,
authRequestId, authRequestId,
authMethods, authMethods,
organization,
}: { }: {
userId: string; userId?: string;
loginName?: string;
authRequestId: string; authRequestId: string;
authMethods: AuthenticationMethodType[] | null; authMethods: AuthenticationMethodType[] | null;
organization?: string;
}) { }) {
const t = useTranslations("verify"); const t = useTranslations("verify");
const [error, setError] = useState<string>(""); const [error, setError] = useState<string>("");
@@ -26,10 +33,24 @@ export function VerifyRedirectButton({
async function submitAndContinue(): Promise<boolean | void> { async function submitAndContinue(): Promise<boolean | void> {
setLoading(true); setLoading(true);
await sendVerificationRedirectWithoutCheck({ let command = {
userId, organization,
authRequestId, authRequestId,
}) } as SendVerificationRedirectWithoutCheckCommand;
if (userId) {
command = {
...command,
userId,
} as SendVerificationRedirectWithoutCheckCommand;
} else if (loginName) {
command = {
...command,
loginName,
} as SendVerificationRedirectWithoutCheckCommand;
}
await sendVerificationRedirectWithoutCheck(command)
.catch((error) => { .catch((error) => {
setError("Could not verify user"); setError("Could not verify user");
return; return;

View File

@@ -340,7 +340,7 @@ export async function checkSessionAndSetPassword({
} }
} }
function checkMFAFactors( export function checkMFAFactors(
session: Session, session: Session,
loginSettings: LoginSettings | undefined, loginSettings: LoginSettings | undefined,
authMethods: AuthenticationMethodType[], authMethods: AuthenticationMethodType[],

View File

@@ -2,23 +2,22 @@
import { import {
getLoginSettings, getLoginSettings,
getSession,
getUserByID, getUserByID,
listAuthenticationMethodTypes, listAuthenticationMethodTypes,
listUsers,
resendEmailCode, resendEmailCode,
resendInviteCode, resendInviteCode,
verifyEmail, verifyEmail,
verifyInviteCode, verifyInviteCode,
} from "@/lib/zitadel"; } from "@/lib/zitadel";
import { create } from "@zitadel/client"; 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 { 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 { User } from "@zitadel/proto/zitadel/user/v2/user_pb";
import { getNextUrl } from "../client";
import { getSessionCookieByLoginName } from "../cookies"; import { getSessionCookieByLoginName } from "../cookies";
import { import { createSessionAndUpdateCookie } from "./cookie";
createSessionAndUpdateCookie, import { checkMFAFactors } from "./password";
setSessionAndUpdateCookie,
} from "./cookie";
type VerifyUserByEmailCommand = { type VerifyUserByEmailCommand = {
userId: string; userId: string;
@@ -96,16 +95,13 @@ export async function resendVerification(command: resendVerifyEmailCommand) {
: resendEmailCode(command.userId); : resendEmailCode(command.userId);
} }
type SendVerificationRedirectWithoutCheckCommand = export type SendVerificationRedirectWithoutCheckCommand = {
| { organization?: string;
loginName: string; authRequestId?: string;
organization?: string; } & (
authRequestId?: string; | { userId: string; loginName?: never }
} | { userId?: never; loginName: string }
| { );
userId: string;
authRequestId?: string;
};
export async function sendVerificationRedirectWithoutCheck( export async function sendVerificationRedirectWithoutCheck(
command: SendVerificationRedirectWithoutCheckCommand, command: SendVerificationRedirectWithoutCheckCommand,
@@ -114,52 +110,31 @@ export async function sendVerificationRedirectWithoutCheck(
return { error: "No userId, nor loginname provided" }; return { error: "No userId, nor loginname provided" };
} }
let sessionCookie; let session: Session | undefined;
let loginSettings: LoginSettings | undefined; let user: User | undefined;
let session;
let user: User; const loginSettings = await getLoginSettings(command.organization);
if ("loginName" in command) { if ("loginName" in command) {
sessionCookie = await getSessionCookieByLoginName({ const sessionCookie = await getSessionCookieByLoginName({
loginName: command.loginName, loginName: command.loginName,
organization: command.organization, organization: command.organization,
}).catch((error) => { }).catch((error) => {
console.warn("Ignored error:", 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) { if (!sessionCookie) {
user = users.result[0]; return { error: "Could not load session cookie" };
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 session = await getSession({
return { error: "Could not verify password" }; sessionId: sessionCookie.id,
} else { sessionToken: sessionCookie.token,
session = await setSessionAndUpdateCookie( }).then((response) => {
sessionCookie, if (response?.session) {
command.checks, return response.session;
undefined, }
command.authRequestId, });
loginSettings?.passwordCheckLifetime,
);
if (!session?.factors?.user?.id) { if (!session?.factors?.user?.id) {
return { error: "Could not create session for user" }; return { error: "Could not create session for user" };
@@ -167,50 +142,57 @@ export async function sendVerificationRedirectWithoutCheck(
const userResponse = await getUserByID(session?.factors?.user?.id); const userResponse = await getUserByID(session?.factors?.user?.id);
if (!userResponse.user) { if (!userResponse?.user) {
return { error: "Could not find user" }; return { error: "Could not load user" };
} }
user = userResponse.user; user = userResponse.user;
} } else if ("userId" in command) {
const userResponse = await getUserByID(command.userId);
if (!loginSettings) { if (!userResponse?.user) {
loginSettings = await getLoginSettings( return { error: "Could not load user" };
command.organization ?? session.factors?.user?.organizationId, }
user = userResponse.user;
const checks = create(ChecksSchema, {
user: {
search: {
case: "loginName",
value: userResponse.user.preferredLoginName,
},
},
});
session = await createSessionAndUpdateCookie(
checks,
undefined,
command.authRequestId,
); );
// this is a fake error message to hide that the user does not even exist
return { error: "Could not verify password" };
} }
if (!session?.factors?.user?.id || !sessionCookie) { if (!session?.factors?.user?.id) {
return { error: "Could not create session for user" }; return { error: "Could not create session for user" };
} }
// const userResponse = await getUserByID(command.userId);
// if (!userResponse || !userResponse.user) { if (!session?.factors?.user?.id) {
// return { error: "Could not load user" }; return { error: "Could not create session for user" };
// } }
// const checks = create(ChecksSchema, { if (!user) {
// user: { return { error: "Could not load user" };
// search: { }
// case: "loginName",
// value: userResponse.user.preferredLoginName,
// },
// },
// });
// const session = await createSessionAndUpdateCookie( const authMethodResponse = await listAuthenticationMethodTypes(user.userId);
// checks,
// undefined,
// command.authRequestId,
// );
const authMethodResponse = await listAuthenticationMethodTypes(
command.userId,
);
if (!authMethodResponse || !authMethodResponse.authMethodTypes) { if (!authMethodResponse || !authMethodResponse.authMethodTypes) {
return { error: "Could not load possible authenticators" }; return { error: "Could not load possible authenticators" };
} }
// if no authmethods are found on the user, redirect to set one up // if no authmethods are found on the user, redirect to set one up
if ( if (
authMethodResponse && authMethodResponse &&
@@ -226,4 +208,38 @@ export async function sendVerificationRedirectWithoutCheck(
} }
return { redirect: `/authenticator/set?${params}` }; return { redirect: `/authenticator/set?${params}` };
} }
// redirect to mfa factor if user has one, or redirect to set one up
checkMFAFactors(
session,
loginSettings,
authMethodResponse.authMethodTypes,
command.organization,
command.authRequestId,
);
// login user if no additional steps are required
if (command.authRequestId && session.id) {
const nextUrl = await getNextUrl(
{
sessionId: session.id,
authRequestId: command.authRequestId,
organization:
command.organization ?? session.factors?.user?.organizationId,
},
loginSettings?.defaultRedirectUri,
);
return { redirect: nextUrl };
}
const url = await getNextUrl(
{
loginName: session.factors.user.loginName,
organization: session.factors?.user?.organizationId,
},
loginSettings?.defaultRedirectUri,
);
return { redirect: url };
} }