fix: verify email

This commit is contained in:
Max Peintner
2024-12-17 15:57:42 +01:00
parent 0f4d31eec7
commit 4bb03574e6
5 changed files with 374 additions and 240 deletions

View File

@@ -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";

View File

@@ -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";

View File

@@ -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}` };
}
}

View File

@@ -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);
// }
}

View 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}` };
}
}