diff --git a/apps/login/src/components/verify-form.tsx b/apps/login/src/components/verify-form.tsx index 003b261b02..b575b4e9b5 100644 --- a/apps/login/src/components/verify-form.tsx +++ b/apps/login/src/components/verify-form.tsx @@ -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"; diff --git a/apps/login/src/components/verify-redirect-button.tsx b/apps/login/src/components/verify-redirect-button.tsx index 4fe313cd1d..2a2f672326 100644 --- a/apps/login/src/components/verify-redirect-button.tsx +++ b/apps/login/src/components/verify-redirect-button.tsx @@ -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"; diff --git a/apps/login/src/lib/server/email.ts b/apps/login/src/lib/server/email.ts deleted file mode 100644 index 51eb1a019b..0000000000 --- a/apps/login/src/lib/server/email.ts +++ /dev/null @@ -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}` }; - } -} diff --git a/apps/login/src/lib/server/password.ts b/apps/login/src/lib/server/password.ts index 47bf260eaa..3dc7df720a 100644 --- a/apps/login/src/lib/server/password.ts +++ b/apps/login/src/lib/server/password.ts @@ -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); + // } +} diff --git a/apps/login/src/lib/server/verify.ts b/apps/login/src/lib/server/verify.ts new file mode 100644 index 0000000000..ff897ba844 --- /dev/null +++ b/apps/login/src/lib/server/verify.ts @@ -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}` }; + } +}