From 4be67c223564a6b5941af28df003291273e0622d Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Thu, 28 Nov 2024 09:59:37 +0100 Subject: [PATCH 01/17] accept local only force mfa, fix nexturl org context --- apps/login/src/components/password-form.tsx | 1 - apps/login/src/lib/client.ts | 31 ++++++++++++--------- apps/login/src/lib/server/password.ts | 6 ++-- 3 files changed, 22 insertions(+), 16 deletions(-) diff --git a/apps/login/src/components/password-form.tsx b/apps/login/src/components/password-form.tsx index 6f810ac30f..0d05bcd628 100644 --- a/apps/login/src/components/password-form.tsx +++ b/apps/login/src/components/password-form.tsx @@ -59,7 +59,6 @@ export function PasswordForm({ password: { password: values.password }, }), authRequestId, - forceMfa: loginSettings?.forceMfa, }) .catch(() => { setError("Could not verify password"); diff --git a/apps/login/src/lib/client.ts b/apps/login/src/lib/client.ts index 7dc2b83cbb..37d22dc83d 100644 --- a/apps/login/src/lib/client.ts +++ b/apps/login/src/lib/client.ts @@ -15,24 +15,29 @@ export async function getNextUrl( defaultRedirectUri?: string, ): Promise { if ("sessionId" in command && "authRequestId" in command) { - const url = - `/login?` + - new URLSearchParams({ - sessionId: command.sessionId, - authRequest: command.authRequestId, - }); - return url; + const params = new URLSearchParams({ + sessionId: command.sessionId, + authRequest: command.authRequestId, + }); + + if (command.organization) { + params.append("organization", command.organization); + } + + return `/login?` + params; } if (defaultRedirectUri) { return defaultRedirectUri; } - const signedInUrl = - `/signedin?` + - new URLSearchParams({ - loginName: command.loginName, - }); + const params = new URLSearchParams({ + loginName: command.loginName, + }); - return signedInUrl; + if (command.organization) { + params.append("organization", command.organization); + } + + return `/signedin?` + params; } diff --git a/apps/login/src/lib/server/password.ts b/apps/login/src/lib/server/password.ts index 59afca50a5..32e9102913 100644 --- a/apps/login/src/lib/server/password.ts +++ b/apps/login/src/lib/server/password.ts @@ -54,7 +54,6 @@ export type UpdateSessionCommand = { organization?: string; checks: Checks; authRequestId?: string; - forceMfa?: boolean; }; export async function sendPassword(command: UpdateSessionCommand) { @@ -209,7 +208,10 @@ export async function sendPassword(command: UpdateSessionCommand) { } return { redirect: `/password/change?` + params }; - } else if (command.forceMfa && !availableSecondFactors.length) { + } else if ( + (loginSettings?.forceMfa || loginSettings?.forceMfaLocalOnly) && + !availableSecondFactors.length + ) { const params = new URLSearchParams({ loginName: session.factors.user.loginName, force: "true", // this defines if the mfa is forced in the settings From ba3359ff51c503ae4587b60c96bebad05ae17063 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Thu, 28 Nov 2024 16:56:46 +0100 Subject: [PATCH 02/17] otp url template, reset with authrequest --- apps/login/src/app/(login)/otp/[method]/page.tsx | 5 +++++ apps/login/src/components/login-otp.tsx | 15 ++++++++++++++- apps/login/src/components/password-form.tsx | 1 + apps/login/src/components/session-item.tsx | 2 +- apps/login/src/lib/server/password.ts | 3 ++- apps/login/src/lib/zitadel.ts | 10 ++++++++-- 6 files changed, 31 insertions(+), 5 deletions(-) diff --git a/apps/login/src/app/(login)/otp/[method]/page.tsx b/apps/login/src/app/(login)/otp/[method]/page.tsx index 9c91b3edd2..1c0904cee2 100644 --- a/apps/login/src/app/(login)/otp/[method]/page.tsx +++ b/apps/login/src/app/(login)/otp/[method]/page.tsx @@ -5,6 +5,7 @@ import { UserAvatar } from "@/components/user-avatar"; import { loadMostRecentSession } from "@/lib/session"; import { getBrandingSettings, getLoginSettings } from "@/lib/zitadel"; import { getLocale, getTranslations } from "next-intl/server"; +import { headers } from "next/headers"; export default async function Page(props: { searchParams: Promise>; @@ -30,6 +31,8 @@ export default async function Page(props: { const loginSettings = await getLoginSettings(organization); + const host = (await headers()).get("host"); + return (
@@ -67,6 +70,8 @@ export default async function Page(props: { organization={organization} method={method} loginSettings={loginSettings} + host={host} + code={code} > )}
diff --git a/apps/login/src/components/login-otp.tsx b/apps/login/src/components/login-otp.tsx index 27fa187c7d..262541eb1b 100644 --- a/apps/login/src/components/login-otp.tsx +++ b/apps/login/src/components/login-otp.tsx @@ -25,6 +25,7 @@ type Props = { method: string; code?: string; loginSettings?: LoginSettings; + host: string | null; }; type Inputs = { @@ -39,6 +40,7 @@ export function LoginOTP({ method, code, loginSettings, + host, }: Props) { const t = useTranslations("otp"); @@ -76,7 +78,18 @@ export function LoginOTP({ if (method === "email") { challenges = create(RequestChallengesSchema, { - otpEmail: { deliveryType: { case: "sendCode", value: {} } }, + otpEmail: { + deliveryType: { + case: "sendCode", + value: host + ? { + urlTemplate: + `${host.includes("localhost") ? "http://" : "https://"}${host}/otp/method=${method}?code={{.Code}}&userId={{.UserID}}&sessionId={{.SessionID}}&organization={{.OrgID}}` + + (authRequestId ? `&authRequestId=${authRequestId}` : ""), + } + : {}, + }, + }, }); } diff --git a/apps/login/src/components/password-form.tsx b/apps/login/src/components/password-form.tsx index 0d05bcd628..a1bc6cd941 100644 --- a/apps/login/src/components/password-form.tsx +++ b/apps/login/src/components/password-form.tsx @@ -86,6 +86,7 @@ export function PasswordForm({ const response = await resetPassword({ loginName, organization, + authRequestId, }) .catch(() => { setError("Could not reset password"); diff --git a/apps/login/src/components/session-item.tsx b/apps/login/src/components/session-item.tsx index 6bdab2394a..4df720b056 100644 --- a/apps/login/src/components/session-item.tsx +++ b/apps/login/src/components/session-item.tsx @@ -102,7 +102,7 @@ export function SessionItem({ /> -
+
{session.factors?.user?.displayName} {session.factors?.user?.loginName} diff --git a/apps/login/src/lib/server/password.ts b/apps/login/src/lib/server/password.ts index 32e9102913..49853c9ce2 100644 --- a/apps/login/src/lib/server/password.ts +++ b/apps/login/src/lib/server/password.ts @@ -27,6 +27,7 @@ import { getSessionCookieByLoginName } from "../cookies"; type ResetPasswordCommand = { loginName: string; organization?: string; + authRequestId?: string; }; export async function resetPassword(command: ResetPasswordCommand) { @@ -46,7 +47,7 @@ export async function resetPassword(command: ResetPasswordCommand) { } const userId = users.result[0].userId; - return passwordReset(userId, host); + return passwordReset(userId, host, command.authRequestId); } export type UpdateSessionCommand = { diff --git a/apps/login/src/lib/zitadel.ts b/apps/login/src/lib/zitadel.ts index be410551fb..0afc4c4dc1 100644 --- a/apps/login/src/lib/zitadel.ts +++ b/apps/login/src/lib/zitadel.ts @@ -504,7 +504,11 @@ export function createUser( * @param userId the id of the user where the email should be set * @returns the newly set email */ -export async function passwordReset(userId: string, host: string | null) { +export async function passwordReset( + userId: string, + host: string | null, + authRequestId?: string, +) { let medium = create(SendPasswordResetLinkSchema, { notificationType: NotificationType.Email, }); @@ -512,7 +516,9 @@ export async function passwordReset(userId: string, host: string | null) { if (host) { medium = { ...medium, - urlTemplate: `${host.includes("localhost") ? "http://" : "https://"}${host}/password/set?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}`, + urlTemplate: + `${host.includes("localhost") ? "http://" : "https://"}${host}/password/set?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}` + + (authRequestId ? `&authRequestId=${authRequestId}` : ""), }; } From 837f45b36c003f1d3f95704e52fe1af73237ded6 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Fri, 29 Nov 2024 08:23:47 +0100 Subject: [PATCH 03/17] handle password change required on pwd page if no additional factor --- apps/login/src/lib/server/loginname.ts | 37 +++++++++-------- apps/login/src/lib/server/password.ts | 56 ++++++++++++++++++-------- 2 files changed, 60 insertions(+), 33 deletions(-) diff --git a/apps/login/src/lib/server/loginname.ts b/apps/login/src/lib/server/loginname.ts index 918202065b..6e2c230db3 100644 --- a/apps/login/src/lib/server/loginname.ts +++ b/apps/login/src/lib/server/loginname.ts @@ -2,7 +2,6 @@ import { create } from "@zitadel/client"; import { ChecksSchema } from "@zitadel/proto/zitadel/session/v2/session_service_pb"; -import { UserState } from "@zitadel/proto/zitadel/user/v2/user_pb"; import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; import { headers } from "next/headers"; import { idpTypeToIdentityProviderType, idpTypeToSlug } from "../idp"; @@ -136,25 +135,29 @@ export async function sendLoginname(command: SendLoginnameCommand) { return { error: "Could not create session for user" }; } - if (users.result[0].state === UserState.INITIAL) { - const params = new URLSearchParams({ - loginName: session.factors?.user?.loginName, - initial: "true", // this does not require a code to be set - }); + // TODO: check if handling of userstate INITIAL is needed - if (command.organization || session.factors?.user?.organizationId) { - params.append( - "organization", - command.organization ?? session.factors?.user?.organizationId, - ); - } + // if ( + // users.result[0].state === UserState.INITIAL + // ) { + // const params = new URLSearchParams({ + // loginName: session.factors?.user?.loginName, + // initial: "true", // this does not require a code to be set + // }); - 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: "/password/set?" + params }; - } + // if (command.authRequestId) { + // params.append("authRequestId", command.authRequestId); + // } + + // return { redirect: "/password/set?" + params }; + // } const methods = await listAuthenticationMethodTypes( session.factors?.user?.id, diff --git a/apps/login/src/lib/server/password.ts b/apps/login/src/lib/server/password.ts index 49853c9ce2..007308ed18 100644 --- a/apps/login/src/lib/server/password.ts +++ b/apps/login/src/lib/server/password.ts @@ -18,7 +18,7 @@ import { ChecksSchema, } 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 { User } from "@zitadel/proto/zitadel/user/v2/user_pb"; import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; import { headers } from "next/headers"; import { getNextUrl } from "../client"; @@ -148,6 +148,26 @@ export async function sendPassword(command: UpdateSessionCommand) { m !== AuthenticationMethodType.PASSKEY, ); + const humanUser = user.type.case === "human" ? user.type.value : undefined; + if ( + availableSecondFactors?.length == 0 && + humanUser?.passwordChangeRequired + ) { + const params = new URLSearchParams({ + loginName: session.factors?.user?.loginName, + }); + + if (command.organization || session.factors?.user?.organizationId) { + params.append("organization", session.factors?.user?.organizationId); + } + + if (command.authRequestId) { + params.append("authRequestId", command.authRequestId); + } + + return { redirect: "/password/change?" + params }; + } + if (availableSecondFactors?.length == 1) { const params = new URLSearchParams({ loginName: session.factors?.user.loginName, @@ -192,24 +212,28 @@ export async function sendPassword(command: UpdateSessionCommand) { } return { redirect: `/mfa?` + params }; - } else if (user.state === UserState.INITIAL) { - const params = new URLSearchParams({ - loginName: session.factors.user.loginName, - }); + } + // TODO: check if handling of userstate INITIAL is needed - if (command.authRequestId) { - params.append("authRequestId", command.authRequestId); - } + // else if (user.state === UserState.INITIAL) { + // const params = new URLSearchParams({ + // loginName: session.factors.user.loginName, + // }); - if (command.organization || session.factors?.user?.organizationId) { - params.append( - "organization", - command.organization ?? session.factors?.user?.organizationId, - ); - } + // if (command.authRequestId) { + // params.append("authRequestId", command.authRequestId); + // } - return { redirect: `/password/change?` + params }; - } else if ( + // if (command.organization || session.factors?.user?.organizationId) { + // params.append( + // "organization", + // command.organization ?? session.factors?.user?.organizationId, + // ); + // } + + // return { redirect: `/password/change?` + params }; + // } + else if ( (loginSettings?.forceMfa || loginSettings?.forceMfaLocalOnly) && !availableSecondFactors.length ) { From bac941890aa4412f401b6972d70e43ac823f7359 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Fri, 29 Nov 2024 13:38:56 +0100 Subject: [PATCH 04/17] idp for invites --- .../app/(login)/authenticator/set/page.tsx | 56 ++-- .../(login)/idp/[provider]/success/page.tsx | 5 +- apps/login/src/app/(login)/loginname/page.tsx | 19 +- .../login/src/components/sign-in-with-idp.tsx | 249 ++++++++++-------- apps/login/src/lib/server/password.ts | 1 + 5 files changed, 185 insertions(+), 145 deletions(-) diff --git a/apps/login/src/app/(login)/authenticator/set/page.tsx b/apps/login/src/app/(login)/authenticator/set/page.tsx index 1c4422327f..1c48ea7a19 100644 --- a/apps/login/src/app/(login)/authenticator/set/page.tsx +++ b/apps/login/src/app/(login)/authenticator/set/page.tsx @@ -2,10 +2,12 @@ import { Alert } from "@/components/alert"; import { BackButton } from "@/components/back-button"; import { ChooseAuthenticatorToSetup } from "@/components/choose-authenticator-to-setup"; import { DynamicTheme } from "@/components/dynamic-theme"; +import { SignInWithIdp } from "@/components/sign-in-with-idp"; import { UserAvatar } from "@/components/user-avatar"; import { getSessionCookieById } from "@/lib/cookies"; import { loadMostRecentSession } from "@/lib/session"; import { + getActiveIdentityProviders, getBrandingSettings, getLoginSettings, getSession, @@ -74,6 +76,10 @@ export default async function Page(props: { }); } + if (!sessionWithData) { + return {tError("unknownContext")}; + } + const branding = await getBrandingSettings( sessionWithData.factors?.user?.organizationId, ); @@ -82,22 +88,32 @@ export default async function Page(props: { sessionWithData.factors?.user?.organizationId, ); + const identityProviders = await getActiveIdentityProviders( + sessionWithData.factors?.user?.organizationId, + ).then((resp) => { + return resp.identityProviders; + }); + const params = new URLSearchParams({ initial: "true", // defines that a code is not required and is therefore not shown in the UI }); - if (loginName) { - params.set("loginName", loginName); + if (sessionWithData.factors?.user?.loginName) { + params.set("loginName", sessionWithData.factors?.user?.loginName); } - if (organization) { - params.set("organization", organization); + if (sessionWithData.factors?.user?.organizationId) { + params.set("organization", sessionWithData.factors?.user?.organizationId); } if (authRequestId) { params.set("authRequestId", authRequestId); } + const host = process.env.VERCEL_URL + ? `https://${process.env.VERCEL_URL}` + : "http://localhost:3000"; + return (
@@ -105,18 +121,14 @@ export default async function Page(props: {

{t("description")}

- {sessionWithData && ( - - )} + - {!(loginName || sessionId) && {tError("unknownContext")}} - - {loginSettings && sessionWithData && ( + {loginSettings && ( )} +

+ or sign in with an Identity Provider +

+ + {loginSettings?.allowExternalIdp && identityProviders && ( + + )} +
diff --git a/apps/login/src/app/(login)/idp/[provider]/success/page.tsx b/apps/login/src/app/(login)/idp/[provider]/success/page.tsx index 48a035f34e..b536456c23 100644 --- a/apps/login/src/app/(login)/idp/[provider]/success/page.tsx +++ b/apps/login/src/app/(login)/idp/[provider]/success/page.tsx @@ -37,7 +37,7 @@ export default async function Page(props: { const searchParams = await props.searchParams; const locale = getLocale(); const t = await getTranslations({ locale, namespace: "idp" }); - const { id, token, authRequestId, organization } = searchParams; + const { id, token, authRequestId, organization, link } = searchParams; const { provider } = params; const branding = await getBrandingSettings(organization); @@ -50,7 +50,8 @@ export default async function Page(props: { const { idpInformation, userId } = intent; - if (userId) { + // sign in user. If user should be linked continue + if (userId && !link) { // TODO: update user if idp.options.isAutoUpdate is true return ( diff --git a/apps/login/src/app/(login)/loginname/page.tsx b/apps/login/src/app/(login)/loginname/page.tsx index 55a6682ce1..b0a1ba4bb9 100644 --- a/apps/login/src/app/(login)/loginname/page.tsx +++ b/apps/login/src/app/(login)/loginname/page.tsx @@ -2,23 +2,14 @@ import { DynamicTheme } from "@/components/dynamic-theme"; import { SignInWithIdp } from "@/components/sign-in-with-idp"; import { UsernameForm } from "@/components/username-form"; import { + getActiveIdentityProviders, getBrandingSettings, getDefaultOrg, getLoginSettings, - settingsService, } from "@/lib/zitadel"; -import { makeReqCtx } from "@zitadel/client/v2"; import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb"; import { getLocale, getTranslations } from "next-intl/server"; -function getIdentityProviders(orgId?: string) { - return settingsService - .getActiveIdentityProviders({ ctx: makeReqCtx(orgId) }, {}) - .then((resp) => { - return resp.identityProviders; - }); -} - export default async function Page(props: { searchParams: Promise>; }) { @@ -47,9 +38,11 @@ export default async function Page(props: { organization ?? defaultOrganization, ); - const identityProviders = await getIdentityProviders( + const identityProviders = await getActiveIdentityProviders( organization ?? defaultOrganization, - ); + ).then((resp) => { + return resp.identityProviders; + }); const branding = await getBrandingSettings( organization ?? defaultOrganization, @@ -68,7 +61,7 @@ export default async function Page(props: { submit={submit} allowRegister={!!loginSettings?.allowRegister} > - {identityProviders && process.env.ZITADEL_API_URL && ( + {identityProviders && ( (false); const [error, setError] = useState(""); @@ -39,6 +41,10 @@ export function SignInWithIdp({ const params = new URLSearchParams(); + if (linkOnly) { + params.set("link", "true"); + } + if (authRequestId) { params.set("authRequestId", authRequestId); } @@ -70,121 +76,134 @@ export function SignInWithIdp({ return (
{identityProviders && - identityProviders.map((idp, i) => { - switch (idp.type) { - case IdentityProviderType.APPLE: - return ( - - startFlow(idp.id, idpTypeToSlug(IdentityProviderType.APPLE)) - } - > - ); - case IdentityProviderType.OAUTH: - return ( - - startFlow(idp.id, idpTypeToSlug(IdentityProviderType.OAUTH)) - } - > - ); - case IdentityProviderType.OIDC: - return ( - - startFlow(idp.id, idpTypeToSlug(IdentityProviderType.OIDC)) - } - > - ); - case IdentityProviderType.GITHUB: - return ( - - startFlow( - idp.id, - idpTypeToSlug(IdentityProviderType.GITHUB), - ) - } - > - ); - case IdentityProviderType.GITHUB_ES: - return ( - - startFlow( - idp.id, - idpTypeToSlug(IdentityProviderType.GITHUB_ES), - ) - } - > - ); - case IdentityProviderType.AZURE_AD: - return ( - - startFlow( - idp.id, - idpTypeToSlug(IdentityProviderType.AZURE_AD), - ) - } - > - ); - case IdentityProviderType.GOOGLE: - return ( - - startFlow( - idp.id, - idpTypeToSlug(IdentityProviderType.GOOGLE), - ) - } - > - ); - case IdentityProviderType.GITLAB: - return ( - - startFlow( - idp.id, - idpTypeToSlug(IdentityProviderType.GITLAB), - ) - } - > - ); - case IdentityProviderType.GITLAB_SELF_HOSTED: - return ( - - startFlow( - idp.id, - idpTypeToSlug(IdentityProviderType.GITLAB_SELF_HOSTED), - ) - } - > - ); - default: - return null; - } - })} + identityProviders + // .filter((idp) => + // linkOnly ? idp.config?.options.isLinkingAllowed : true, + // ) + .map((idp, i) => { + switch (idp.type) { + case IdentityProviderType.APPLE: + return ( + + startFlow( + idp.id, + idpTypeToSlug(IdentityProviderType.APPLE), + ) + } + > + ); + case IdentityProviderType.OAUTH: + return ( + + startFlow( + idp.id, + idpTypeToSlug(IdentityProviderType.OAUTH), + ) + } + > + ); + case IdentityProviderType.OIDC: + return ( + + startFlow( + idp.id, + idpTypeToSlug(IdentityProviderType.OIDC), + ) + } + > + ); + case IdentityProviderType.GITHUB: + return ( + + startFlow( + idp.id, + idpTypeToSlug(IdentityProviderType.GITHUB), + ) + } + > + ); + case IdentityProviderType.GITHUB_ES: + return ( + + startFlow( + idp.id, + idpTypeToSlug(IdentityProviderType.GITHUB_ES), + ) + } + > + ); + case IdentityProviderType.AZURE_AD: + return ( + + startFlow( + idp.id, + idpTypeToSlug(IdentityProviderType.AZURE_AD), + ) + } + > + ); + case IdentityProviderType.GOOGLE: + return ( + + startFlow( + idp.id, + idpTypeToSlug(IdentityProviderType.GOOGLE), + ) + } + > + ); + case IdentityProviderType.GITLAB: + return ( + + startFlow( + idp.id, + idpTypeToSlug(IdentityProviderType.GITLAB), + ) + } + > + ); + case IdentityProviderType.GITLAB_SELF_HOSTED: + return ( + + startFlow( + idp.id, + idpTypeToSlug(IdentityProviderType.GITLAB_SELF_HOSTED), + ) + } + > + ); + default: + return null; + } + })} {error && (
{error} diff --git a/apps/login/src/lib/server/password.ts b/apps/login/src/lib/server/password.ts index 007308ed18..8f448c4d7a 100644 --- a/apps/login/src/lib/server/password.ts +++ b/apps/login/src/lib/server/password.ts @@ -149,6 +149,7 @@ export async function sendPassword(command: UpdateSessionCommand) { ); const humanUser = user.type.case === "human" ? user.type.value : undefined; + console.log("humanUser", humanUser); if ( availableSecondFactors?.length == 0 && humanUser?.passwordChangeRequired From 3cde9d46a6f14e8cdec5101e1ecc5e911b656c74 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Fri, 29 Nov 2024 14:35:06 +0100 Subject: [PATCH 05/17] handle disallow passkey, handle disallow pwd --- apps/login/src/lib/server/loginname.ts | 60 ++++++++++++++------------ apps/login/src/lib/server/password.ts | 25 ++--------- 2 files changed, 37 insertions(+), 48 deletions(-) diff --git a/apps/login/src/lib/server/loginname.ts b/apps/login/src/lib/server/loginname.ts index 6e2c230db3..9b04d91fe8 100644 --- a/apps/login/src/lib/server/loginname.ts +++ b/apps/login/src/lib/server/loginname.ts @@ -6,6 +6,8 @@ import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_se import { headers } from "next/headers"; 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 { getActiveIdentityProviders, getIDPByID, @@ -34,6 +36,15 @@ export async function sendLoginname(command: SendLoginnameCommand) { const loginSettings = await getLoginSettings(command.organization); + const potentialUsers = users.result.filter((u) => { + const human = u.type.case === "human" ? u.type.value : undefined; + return loginSettings?.disableLoginWithEmail + ? human?.email?.isVerified && human?.email?.email !== command.loginName + : loginSettings?.disableLoginWithPhone + ? human?.phone?.isVerified && human?.phone?.phone !== command.loginName + : true; + }); + const redirectUserToSingleIDPIfAvailable = async () => { const identityProviders = await getActiveIdentityProviders( command.organization, @@ -118,8 +129,8 @@ export async function sendLoginname(command: SendLoginnameCommand) { } }; - if (users.details?.totalResult == BigInt(1) && users.result[0].userId) { - const userId = users.result[0].userId; + if (potentialUsers.length == 1 && potentialUsers[0].userId) { + const userId = potentialUsers[0].userId; const checks = create(ChecksSchema, { user: { search: { case: "userId", value: userId } }, @@ -136,28 +147,9 @@ export async function sendLoginname(command: SendLoginnameCommand) { } // TODO: check if handling of userstate INITIAL is needed - - // if ( - // users.result[0].state === UserState.INITIAL - // ) { - // const params = new URLSearchParams({ - // loginName: session.factors?.user?.loginName, - // initial: "true", // this does not require a code to be set - // }); - - // if (command.organization || session.factors?.user?.organizationId) { - // params.append( - // "organization", - // command.organization ?? session.factors?.user?.organizationId, - // ); - // } - - // if (command.authRequestId) { - // params.append("authRequestId", command.authRequestId); - // } - - // return { redirect: "/password/set?" + params }; - // } + if (potentialUsers[0].state === UserState.INITIAL) { + return { error: "Initial User not supported" }; + } const methods = await listAuthenticationMethodTypes( session.factors?.user?.id, @@ -165,9 +157,9 @@ export async function sendLoginname(command: SendLoginnameCommand) { if (!methods.authMethodTypes || !methods.authMethodTypes.length) { if ( - users.result[0].type.case === "human" && - users.result[0].type.value.email && - !users.result[0].type.value.email.isVerified + potentialUsers[0].type.case === "human" && + potentialUsers[0].type.value.email && + !potentialUsers[0].type.value.email.isVerified ) { const paramsVerify = new URLSearchParams({ loginName: session.factors?.user?.loginName, @@ -212,6 +204,13 @@ export async function sendLoginname(command: SendLoginnameCommand) { const method = methods.authMethodTypes[0]; switch (method) { case AuthenticationMethodType.PASSWORD: // user has only password as auth method + if (!loginSettings?.allowUsernamePassword) { + return { + error: + "Username Password not allowed! Contact your administrator for more information.", + }; + } + const paramsPassword: any = { loginName: session.factors?.user?.loginName, }; @@ -232,6 +231,13 @@ export async function sendLoginname(command: SendLoginnameCommand) { }; case AuthenticationMethodType.PASSKEY: // AuthenticationMethodType.AUTHENTICATION_METHOD_TYPE_PASSKEY + if (loginSettings?.passkeysType === PasskeysType.NOT_ALLOWED) { + return { + error: + "Passkeys not allowed! Contact your administrator for more information.", + }; + } + const paramsPasskey: any = { loginName: command.loginName }; if (command.authRequestId) { paramsPasskey.authRequestId = command.authRequestId; diff --git a/apps/login/src/lib/server/password.ts b/apps/login/src/lib/server/password.ts index 8f448c4d7a..c4794c635d 100644 --- a/apps/login/src/lib/server/password.ts +++ b/apps/login/src/lib/server/password.ts @@ -18,7 +18,7 @@ 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, UserState } from "@zitadel/proto/zitadel/user/v2/user_pb"; import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; import { headers } from "next/headers"; import { getNextUrl } from "../client"; @@ -215,26 +215,9 @@ export async function sendPassword(command: UpdateSessionCommand) { return { redirect: `/mfa?` + params }; } // TODO: check if handling of userstate INITIAL is needed - - // else if (user.state === UserState.INITIAL) { - // 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: `/password/change?` + params }; - // } - else if ( + else if (user.state === UserState.INITIAL) { + return { error: "Initial User not supported" }; + } else if ( (loginSettings?.forceMfa || loginSettings?.forceMfaLocalOnly) && !availableSecondFactors.length ) { From 6f69ba11b2fe163d4b6279acf1649452f3e74fa2 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Tue, 3 Dec 2024 09:11:15 +0100 Subject: [PATCH 06/17] allow pwd in tests --- apps/login/cypress/integration/login.cy.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/login/cypress/integration/login.cy.ts b/apps/login/cypress/integration/login.cy.ts index f293653af1..a03863a9ce 100644 --- a/apps/login/cypress/integration/login.cy.ts +++ b/apps/login/cypress/integration/login.cy.ts @@ -49,6 +49,7 @@ describe("login", () => { data: { settings: { passkeysType: 1, + allowUsernamePassword: true, }, }, }); From f409dc363393b2d6a9567430f65b82a0e2b2013b Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Tue, 3 Dec 2024 09:39:42 +0100 Subject: [PATCH 07/17] complete idp flow --- apps/login/src/components/username-form.tsx | 2 ++ apps/login/src/lib/server/loginname.ts | 9 ++++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/apps/login/src/components/username-form.tsx b/apps/login/src/components/username-form.tsx index 7d18623aba..ab56bc2926 100644 --- a/apps/login/src/components/username-form.tsx +++ b/apps/login/src/components/username-form.tsx @@ -61,6 +61,8 @@ export function UsernameForm({ setLoading(false); }); + console.log(res, "res"); + if (res?.redirect) { return router.push(res.redirect); } diff --git a/apps/login/src/lib/server/loginname.ts b/apps/login/src/lib/server/loginname.ts index 9b04d91fe8..99b398fa44 100644 --- a/apps/login/src/lib/server/loginname.ts +++ b/apps/login/src/lib/server/loginname.ts @@ -94,6 +94,7 @@ export async function sendLoginname(command: SendLoginnameCommand) { const identityProviderId = identityProviders[0].idpId; const idp = await getIDPByID(identityProviderId); + const idpType = idp?.type; if (!idp || !idpType) { @@ -271,7 +272,7 @@ export async function sendLoginname(command: SendLoginnameCommand) { } else if ( methods.authMethodTypes.includes(AuthenticationMethodType.IDP) ) { - await redirectUserToIDP(userId); + return redirectUserToIDP(userId); } else if ( methods.authMethodTypes.includes(AuthenticationMethodType.PASSWORD) ) { @@ -297,8 +298,10 @@ export async function sendLoginname(command: SendLoginnameCommand) { // user not found, check if register is enabled on organization if (loginSettings?.allowRegister && !loginSettings?.allowUsernamePassword) { // TODO: do we need to handle login hints for IDPs here? - await redirectUserToSingleIDPIfAvailable(); - + const resp = await redirectUserToSingleIDPIfAvailable(); + if (resp) { + return resp; + } return { error: "Could not find user" }; } else if ( loginSettings?.allowRegister && From bb8f33af429e089abc794287cbfe206510b693ef Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Tue, 3 Dec 2024 09:45:10 +0100 Subject: [PATCH 08/17] temp remove idp from invite flow --- .../src/app/(login)/authenticator/set/page.tsx | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/apps/login/src/app/(login)/authenticator/set/page.tsx b/apps/login/src/app/(login)/authenticator/set/page.tsx index 1c48ea7a19..f4ee7024fe 100644 --- a/apps/login/src/app/(login)/authenticator/set/page.tsx +++ b/apps/login/src/app/(login)/authenticator/set/page.tsx @@ -2,12 +2,10 @@ import { Alert } from "@/components/alert"; import { BackButton } from "@/components/back-button"; import { ChooseAuthenticatorToSetup } from "@/components/choose-authenticator-to-setup"; import { DynamicTheme } from "@/components/dynamic-theme"; -import { SignInWithIdp } from "@/components/sign-in-with-idp"; import { UserAvatar } from "@/components/user-avatar"; import { getSessionCookieById } from "@/lib/cookies"; import { loadMostRecentSession } from "@/lib/session"; import { - getActiveIdentityProviders, getBrandingSettings, getLoginSettings, getSession, @@ -88,11 +86,11 @@ export default async function Page(props: { sessionWithData.factors?.user?.organizationId, ); - const identityProviders = await getActiveIdentityProviders( - sessionWithData.factors?.user?.organizationId, - ).then((resp) => { - return resp.identityProviders; - }); + // const identityProviders = await getActiveIdentityProviders( + // sessionWithData.factors?.user?.organizationId, + // ).then((resp) => { + // return resp.identityProviders; + // }); const params = new URLSearchParams({ initial: "true", // defines that a code is not required and is therefore not shown in the UI @@ -136,7 +134,7 @@ export default async function Page(props: { > )} -

+ {/*

or sign in with an Identity Provider

@@ -148,7 +146,7 @@ export default async function Page(props: { organization={sessionWithData.factors?.user?.organizationId} linkOnly={true} // tell the callback function to just link the IDP and not login, to get an error when user is already available > - )} + )} */}
From b0f991ac2c2a3db864e608fc1b3c8bc5bb489b46 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Tue, 3 Dec 2024 10:07:14 +0100 Subject: [PATCH 09/17] show validity based on idp --- apps/login/src/components/session-item.tsx | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/apps/login/src/components/session-item.tsx b/apps/login/src/components/session-item.tsx index 4df720b056..1845ea63bc 100644 --- a/apps/login/src/components/session-item.tsx +++ b/apps/login/src/components/session-item.tsx @@ -16,12 +16,14 @@ export function isSessionValid(session: Partial): { } { const validPassword = session?.factors?.password?.verifiedAt; const validPasskey = session?.factors?.webAuthN?.verifiedAt; + const validIDP = session?.factors?.intent?.verifiedAt; + const stillValid = session.expirationDate ? timestampDate(session.expirationDate) > new Date() : true; - const verifiedAt = validPassword || validPasskey; - const valid = !!((validPassword || validPasskey) && stillValid); + const verifiedAt = validPassword || validPasskey || validIDP; + const valid = !!((validPassword || validPasskey || validIDP) && stillValid); return { valid, verifiedAt }; } @@ -107,10 +109,16 @@ export function SessionItem({ {session.factors?.user?.loginName} - {valid && ( + {valid ? ( {verifiedAt && moment(timestampDate(verifiedAt)).fromNow()} + ) : ( + + expired{" "} + {session.expirationDate && + moment(timestampDate(session.expirationDate)).fromNow()} + )}
From d50db37fa24a40f9002e5d23f238fe1b72ca0c11 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Tue, 3 Dec 2024 10:26:44 +0100 Subject: [PATCH 10/17] cleanup host on idp login --- apps/login/src/app/(login)/loginname/page.tsx | 5 ----- apps/login/src/components/sign-in-with-idp.tsx | 5 +++-- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/apps/login/src/app/(login)/loginname/page.tsx b/apps/login/src/app/(login)/loginname/page.tsx index b0a1ba4bb9..e886de7f87 100644 --- a/apps/login/src/app/(login)/loginname/page.tsx +++ b/apps/login/src/app/(login)/loginname/page.tsx @@ -30,10 +30,6 @@ export default async function Page(props: { } } - const host = process.env.VERCEL_URL - ? `https://${process.env.VERCEL_URL}` - : "http://localhost:3000"; - const loginSettings = await getLoginSettings( organization ?? defaultOrganization, ); @@ -63,7 +59,6 @@ export default async function Page(props: { > {identityProviders && ( Date: Tue, 3 Dec 2024 10:49:33 +0100 Subject: [PATCH 11/17] cleanup --- apps/login/src/app/(login)/idp/page.tsx | 5 ----- apps/login/src/components/sign-in-with-idp.tsx | 9 ++------- apps/login/src/lib/server/idp.ts | 7 +++++-- 3 files changed, 7 insertions(+), 14 deletions(-) diff --git a/apps/login/src/app/(login)/idp/page.tsx b/apps/login/src/app/(login)/idp/page.tsx index d11bd8c20a..30d1c9fab5 100644 --- a/apps/login/src/app/(login)/idp/page.tsx +++ b/apps/login/src/app/(login)/idp/page.tsx @@ -24,10 +24,6 @@ export default async function Page(props: { const identityProviders = await getIdentityProviders(organization); - const host = process.env.VERCEL_URL - ? `https://${process.env.VERCEL_URL}` - : "http://localhost:3000"; - const branding = await getBrandingSettings(organization); return ( @@ -38,7 +34,6 @@ export default async function Page(props: { {identityProviders && ( { setError("Could not start IDP flow"); diff --git a/apps/login/src/lib/server/idp.ts b/apps/login/src/lib/server/idp.ts index 0c5d26d45c..2077e1697f 100644 --- a/apps/login/src/lib/server/idp.ts +++ b/apps/login/src/lib/server/idp.ts @@ -1,6 +1,7 @@ "use server"; import { startIdentityProviderFlow } from "@/lib/zitadel"; +import { headers } from "next/headers"; export type StartIDPFlowCommand = { idpId: string; @@ -9,11 +10,13 @@ export type StartIDPFlowCommand = { }; export async function startIDPFlow(command: StartIDPFlowCommand) { + const host = (await headers()).get("host"); + return startIdentityProviderFlow({ idpId: command.idpId, urls: { - successUrl: command.successUrl, - failureUrl: command.failureUrl, + successUrl: `${host}${command.successUrl}`, + failureUrl: `${host}${command.failureUrl}`, }, }).then((response) => { if ( From c2295760dcb421c532a641e1bdb45bd30d566966 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Tue, 3 Dec 2024 10:54:07 +0100 Subject: [PATCH 12/17] protocol for host, error handling --- apps/login/src/components/sign-in-with-idp.tsx | 5 +++++ apps/login/src/lib/server/idp.ts | 8 ++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/apps/login/src/components/sign-in-with-idp.tsx b/apps/login/src/components/sign-in-with-idp.tsx index 555b90feee..e94ba8364b 100644 --- a/apps/login/src/components/sign-in-with-idp.tsx +++ b/apps/login/src/components/sign-in-with-idp.tsx @@ -64,6 +64,11 @@ export function SignInWithIdp({ setLoading(false); }); + if (response && "error" in response && response?.error) { + setError(response.error); + return; + } + if (response && "redirect" in response && response?.redirect) { return router.push(response.redirect); } diff --git a/apps/login/src/lib/server/idp.ts b/apps/login/src/lib/server/idp.ts index 2077e1697f..ebb755987e 100644 --- a/apps/login/src/lib/server/idp.ts +++ b/apps/login/src/lib/server/idp.ts @@ -12,11 +12,15 @@ export type StartIDPFlowCommand = { export async function startIDPFlow(command: StartIDPFlowCommand) { const host = (await headers()).get("host"); + if (!host) { + return { error: "Could not get host" }; + } + return startIdentityProviderFlow({ idpId: command.idpId, urls: { - successUrl: `${host}${command.successUrl}`, - failureUrl: `${host}${command.failureUrl}`, + successUrl: `${host.includes("localhost") ? "http://" : "https://"}${host}${command.successUrl}`, + failureUrl: `${host.includes("localhost") ? "http://" : "https://"}${host}${command.failureUrl}`, }, }).then((response) => { if ( From 798cd464a4904b950772f3724adb549ca6a0e19c Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Tue, 3 Dec 2024 10:57:04 +0100 Subject: [PATCH 13/17] fix host, error handling --- apps/login/src/app/login/route.ts | 6 +++--- apps/login/src/lib/server/loginname.ts | 22 ++++++++++++++++++---- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/apps/login/src/app/login/route.ts b/apps/login/src/app/login/route.ts index af8a74f54c..9ef4d00741 100644 --- a/apps/login/src/app/login/route.ts +++ b/apps/login/src/app/login/route.ts @@ -174,7 +174,7 @@ export async function GET(request: NextRequest) { const idp = identityProviders.find((idp) => idp.id === idpId); if (idp) { - const host = request.nextUrl.origin; + const origin = request.nextUrl.origin; const identityProviderType = identityProviders[0].type; let provider = idpTypeToSlug(identityProviderType); @@ -193,10 +193,10 @@ export async function GET(request: NextRequest) { idpId, urls: { successUrl: - `${host}/idp/${provider}/success?` + + `${origin}/idp/${provider}/success?` + new URLSearchParams(params), failureUrl: - `${host}/idp/${provider}/failure?` + + `${origin}/idp/${provider}/failure?` + new URLSearchParams(params), }, }).then((resp) => { diff --git a/apps/login/src/lib/server/loginname.ts b/apps/login/src/lib/server/loginname.ts index 99b398fa44..295f9b455f 100644 --- a/apps/login/src/lib/server/loginname.ts +++ b/apps/login/src/lib/server/loginname.ts @@ -54,6 +54,11 @@ export async function sendLoginname(command: SendLoginnameCommand) { if (identityProviders.length === 1) { const host = (await headers()).get("host"); + + if (!host) { + return { error: "Could not get host" }; + } + const identityProviderType = identityProviders[0].type; const provider = idpTypeToSlug(identityProviderType); @@ -72,9 +77,11 @@ export async function sendLoginname(command: SendLoginnameCommand) { idpId: identityProviders[0].id, urls: { successUrl: - `${host}/idp/${provider}/success?` + new URLSearchParams(params), + `${host.includes("localhost") ? "http://" : "https://"}${host}/idp/${provider}/success?` + + new URLSearchParams(params), failureUrl: - `${host}/idp/${provider}/failure?` + new URLSearchParams(params), + `${host.includes("localhost") ? "http://" : "https://"}${host}/idp/${provider}/failure?` + + new URLSearchParams(params), }, }); @@ -91,6 +98,11 @@ export async function sendLoginname(command: SendLoginnameCommand) { if (identityProviders.length === 1) { const host = (await headers()).get("host"); + + if (!host) { + return { error: "Could not get host" }; + } + const identityProviderId = identityProviders[0].idpId; const idp = await getIDPByID(identityProviderId); @@ -118,9 +130,11 @@ export async function sendLoginname(command: SendLoginnameCommand) { idpId: idp.id, urls: { successUrl: - `${host}/idp/${provider}/success?` + new URLSearchParams(params), + `${host.includes("localhost") ? "http://" : "https://"}${host}/idp/${provider}/success?` + + new URLSearchParams(params), failureUrl: - `${host}/idp/${provider}/failure?` + new URLSearchParams(params), + `${host.includes("localhost") ? "http://" : "https://"}${host}/idp/${provider}/failure?` + + new URLSearchParams(params), }, }); From b81afaee15d43f6ad1f59a851f3baa7d7ef990e5 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Tue, 3 Dec 2024 13:31:54 +0100 Subject: [PATCH 14/17] not supported readme --- apps/login/readme.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/apps/login/readme.md b/apps/login/readme.md index 2aa09da2e4..bc946cf0c4 100644 --- a/apps/login/readme.md +++ b/apps/login/readme.md @@ -389,7 +389,8 @@ In future, self service options to jump to are shown below, like: ## Currently NOT Supported -- loginSettings.disableLoginWithEmail -- loginSettings.disableLoginWithPhone -- loginSettings.allowExternalIdp - this will be deprecated with the new login as it can be determined by the available IDPs -- loginSettings.forceMfaLocalOnly +Timebased features like the multifactor init prompt or password expiry, are not supported due to a current limitation in the API. Lockout settings which keeps track of the password retries, will also be implemented in a later stage. + +- Lockout Settings +- Password Expiry Settings +- Login Settings: multifactor init prompt From 30091ddb17808b9f729c39bea7b20c1a70cb8655 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Tue, 3 Dec 2024 13:48:07 +0100 Subject: [PATCH 15/17] cleanup --- apps/login/src/app/(login)/authenticator/set/page.tsx | 4 ++++ apps/login/src/components/sign-in-with-idp.tsx | 2 ++ apps/login/src/components/username-form.tsx | 2 -- apps/login/src/lib/server/password.ts | 2 +- 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/apps/login/src/app/(login)/authenticator/set/page.tsx b/apps/login/src/app/(login)/authenticator/set/page.tsx index f4ee7024fe..36294c8c1c 100644 --- a/apps/login/src/app/(login)/authenticator/set/page.tsx +++ b/apps/login/src/app/(login)/authenticator/set/page.tsx @@ -86,6 +86,8 @@ export default async function Page(props: { sessionWithData.factors?.user?.organizationId, ); + /* - TODO: Implement after https://github.com/zitadel/zitadel/issues/8981 */ + // const identityProviders = await getActiveIdentityProviders( // sessionWithData.factors?.user?.organizationId, // ).then((resp) => { @@ -134,6 +136,8 @@ export default async function Page(props: { > )} + {/* - TODO: Implement after https://github.com/zitadel/zitadel/issues/8981 */} + {/*

or sign in with an Identity Provider

diff --git a/apps/login/src/components/sign-in-with-idp.tsx b/apps/login/src/components/sign-in-with-idp.tsx index e94ba8364b..a10680eae5 100644 --- a/apps/login/src/components/sign-in-with-idp.tsx +++ b/apps/login/src/components/sign-in-with-idp.tsx @@ -78,6 +78,8 @@ export function SignInWithIdp({
{identityProviders && identityProviders + /* - TODO: Implement after https://github.com/zitadel/zitadel/issues/8981 */ + // .filter((idp) => // linkOnly ? idp.config?.options.isLinkingAllowed : true, // ) diff --git a/apps/login/src/components/username-form.tsx b/apps/login/src/components/username-form.tsx index ab56bc2926..7d18623aba 100644 --- a/apps/login/src/components/username-form.tsx +++ b/apps/login/src/components/username-form.tsx @@ -61,8 +61,6 @@ export function UsernameForm({ setLoading(false); }); - console.log(res, "res"); - if (res?.redirect) { return router.push(res.redirect); } diff --git a/apps/login/src/lib/server/password.ts b/apps/login/src/lib/server/password.ts index c4794c635d..5e202aabd5 100644 --- a/apps/login/src/lib/server/password.ts +++ b/apps/login/src/lib/server/password.ts @@ -149,7 +149,7 @@ export async function sendPassword(command: UpdateSessionCommand) { ); const humanUser = user.type.case === "human" ? user.type.value : undefined; - console.log("humanUser", humanUser); + if ( availableSecondFactors?.length == 0 && humanUser?.passwordChangeRequired From 017c3215eba4f68ef1f4e190397e9e4c95ab2cef Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Tue, 3 Dec 2024 14:31:50 +0100 Subject: [PATCH 16/17] use origin header instead of host --- .../app/(login)/authenticator/set/page.tsx | 4 ---- .../src/app/(login)/otp/[method]/page.tsx | 4 ++-- apps/login/src/components/login-otp.tsx | 8 +++---- apps/login/src/lib/server/idp.ts | 10 ++++---- apps/login/src/lib/server/invite.ts | 4 ++-- apps/login/src/lib/server/loginname.ts | 24 ++++++++----------- apps/login/src/lib/server/passkeys.ts | 2 +- apps/login/src/lib/server/password.ts | 4 ++-- apps/login/src/lib/zitadel.ts | 12 +++++----- 9 files changed, 32 insertions(+), 40 deletions(-) diff --git a/apps/login/src/app/(login)/authenticator/set/page.tsx b/apps/login/src/app/(login)/authenticator/set/page.tsx index 36294c8c1c..a34a9e8c7c 100644 --- a/apps/login/src/app/(login)/authenticator/set/page.tsx +++ b/apps/login/src/app/(login)/authenticator/set/page.tsx @@ -110,10 +110,6 @@ export default async function Page(props: { params.set("authRequestId", authRequestId); } - const host = process.env.VERCEL_URL - ? `https://${process.env.VERCEL_URL}` - : "http://localhost:3000"; - return (
diff --git a/apps/login/src/app/(login)/otp/[method]/page.tsx b/apps/login/src/app/(login)/otp/[method]/page.tsx index 1c0904cee2..c509cc6ee1 100644 --- a/apps/login/src/app/(login)/otp/[method]/page.tsx +++ b/apps/login/src/app/(login)/otp/[method]/page.tsx @@ -31,7 +31,7 @@ export default async function Page(props: { const loginSettings = await getLoginSettings(organization); - const host = (await headers()).get("host"); + const origin = (await headers()).get("origin"); return ( @@ -70,7 +70,7 @@ export default async function Page(props: { organization={organization} method={method} loginSettings={loginSettings} - host={host} + origin={origin} code={code} > )} diff --git a/apps/login/src/components/login-otp.tsx b/apps/login/src/components/login-otp.tsx index 262541eb1b..6dbacdb66e 100644 --- a/apps/login/src/components/login-otp.tsx +++ b/apps/login/src/components/login-otp.tsx @@ -25,7 +25,7 @@ type Props = { method: string; code?: string; loginSettings?: LoginSettings; - host: string | null; + origin: string | null; }; type Inputs = { @@ -40,7 +40,7 @@ export function LoginOTP({ method, code, loginSettings, - host, + origin, }: Props) { const t = useTranslations("otp"); @@ -81,10 +81,10 @@ export function LoginOTP({ otpEmail: { deliveryType: { case: "sendCode", - value: host + value: origin ? { urlTemplate: - `${host.includes("localhost") ? "http://" : "https://"}${host}/otp/method=${method}?code={{.Code}}&userId={{.UserID}}&sessionId={{.SessionID}}&organization={{.OrgID}}` + + `${origin}/otp/method=${method}?code={{.Code}}&userId={{.UserID}}&sessionId={{.SessionID}}&organization={{.OrgID}}` + (authRequestId ? `&authRequestId=${authRequestId}` : ""), } : {}, diff --git a/apps/login/src/lib/server/idp.ts b/apps/login/src/lib/server/idp.ts index ebb755987e..0b376ad4bc 100644 --- a/apps/login/src/lib/server/idp.ts +++ b/apps/login/src/lib/server/idp.ts @@ -10,17 +10,17 @@ export type StartIDPFlowCommand = { }; export async function startIDPFlow(command: StartIDPFlowCommand) { - const host = (await headers()).get("host"); + const origin = (await headers()).get("origin"); - if (!host) { - return { error: "Could not get host" }; + if (!origin) { + return { error: "Could not get origin" }; } return startIdentityProviderFlow({ idpId: command.idpId, urls: { - successUrl: `${host.includes("localhost") ? "http://" : "https://"}${host}${command.successUrl}`, - failureUrl: `${host.includes("localhost") ? "http://" : "https://"}${host}${command.failureUrl}`, + successUrl: `${origin}${command.successUrl}`, + failureUrl: `${origin}${command.failureUrl}`, }, }).then((response) => { if ( diff --git a/apps/login/src/lib/server/invite.ts b/apps/login/src/lib/server/invite.ts index 3c68587898..b9db345b21 100644 --- a/apps/login/src/lib/server/invite.ts +++ b/apps/login/src/lib/server/invite.ts @@ -20,7 +20,7 @@ export type RegisterUserResponse = { }; export async function inviteUser(command: InviteUserCommand) { - const host = (await headers()).get("host"); + const origin = (await headers()).get("origin"); const human = await addHumanUser({ email: command.email, @@ -34,7 +34,7 @@ export async function inviteUser(command: InviteUserCommand) { return { error: "Could not create user" }; } - const codeResponse = await createInviteCode(human.userId, host); + const codeResponse = await createInviteCode(human.userId, origin); if (!codeResponse || !human) { return { error: "Could not create invite code" }; diff --git a/apps/login/src/lib/server/loginname.ts b/apps/login/src/lib/server/loginname.ts index 295f9b455f..ca92ad1556 100644 --- a/apps/login/src/lib/server/loginname.ts +++ b/apps/login/src/lib/server/loginname.ts @@ -53,10 +53,10 @@ export async function sendLoginname(command: SendLoginnameCommand) { }); if (identityProviders.length === 1) { - const host = (await headers()).get("host"); + const origin = (await headers()).get("origin"); - if (!host) { - return { error: "Could not get host" }; + if (!origin) { + return { error: "Could not get origin" }; } const identityProviderType = identityProviders[0].type; @@ -77,11 +77,9 @@ export async function sendLoginname(command: SendLoginnameCommand) { idpId: identityProviders[0].id, urls: { successUrl: - `${host.includes("localhost") ? "http://" : "https://"}${host}/idp/${provider}/success?` + - new URLSearchParams(params), + `${origin}/idp/${provider}/success?` + new URLSearchParams(params), failureUrl: - `${host.includes("localhost") ? "http://" : "https://"}${host}/idp/${provider}/failure?` + - new URLSearchParams(params), + `${origin}/idp/${provider}/failure?` + new URLSearchParams(params), }, }); @@ -97,10 +95,10 @@ export async function sendLoginname(command: SendLoginnameCommand) { }); if (identityProviders.length === 1) { - const host = (await headers()).get("host"); + const origin = (await headers()).get("origin"); - if (!host) { - return { error: "Could not get host" }; + if (!origin) { + return { error: "Could not get origin" }; } const identityProviderId = identityProviders[0].idpId; @@ -130,11 +128,9 @@ export async function sendLoginname(command: SendLoginnameCommand) { idpId: idp.id, urls: { successUrl: - `${host.includes("localhost") ? "http://" : "https://"}${host}/idp/${provider}/success?` + - new URLSearchParams(params), + `${origin}/idp/${provider}/success?` + new URLSearchParams(params), failureUrl: - `${host.includes("localhost") ? "http://" : "https://"}${host}/idp/${provider}/failure?` + - new URLSearchParams(params), + `${origin}/idp/${provider}/failure?` + new URLSearchParams(params), }, }); diff --git a/apps/login/src/lib/server/passkeys.ts b/apps/login/src/lib/server/passkeys.ts index 181962cae1..518b070993 100644 --- a/apps/login/src/lib/server/passkeys.ts +++ b/apps/login/src/lib/server/passkeys.ts @@ -40,7 +40,7 @@ export async function registerPasskeyLink( const host = (await headers()).get("host"); if (!host) { - throw new Error("Could not get domain"); + throw new Error("Could not get host"); } const [hostname, port] = host.split(":"); diff --git a/apps/login/src/lib/server/password.ts b/apps/login/src/lib/server/password.ts index 5e202aabd5..5b284a3e5e 100644 --- a/apps/login/src/lib/server/password.ts +++ b/apps/login/src/lib/server/password.ts @@ -31,7 +31,7 @@ type ResetPasswordCommand = { }; export async function resetPassword(command: ResetPasswordCommand) { - const host = (await headers()).get("host"); + const origin = (await headers()).get("origin"); const users = await listUsers({ loginName: command.loginName, @@ -47,7 +47,7 @@ export async function resetPassword(command: ResetPasswordCommand) { } const userId = users.result[0].userId; - return passwordReset(userId, host, command.authRequestId); + return passwordReset(userId, origin, command.authRequestId); } export type UpdateSessionCommand = { diff --git a/apps/login/src/lib/zitadel.ts b/apps/login/src/lib/zitadel.ts index 0afc4c4dc1..7210442fef 100644 --- a/apps/login/src/lib/zitadel.ts +++ b/apps/login/src/lib/zitadel.ts @@ -267,15 +267,15 @@ export async function resendInviteCode(userId: string) { return userService.resendInviteCode({ userId }, {}); } -export async function createInviteCode(userId: string, host: string | null) { +export async function createInviteCode(userId: string, origin: string | null) { let medium = create(SendInviteCodeSchema, { applicationName: "Typescript Login", }); - if (host) { + if (origin) { medium = { ...medium, - urlTemplate: `${host.includes("localhost") ? "http://" : "https://"}${host}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}&invite=true`, + urlTemplate: `${origin}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}&invite=true`, }; } @@ -506,18 +506,18 @@ export function createUser( */ export async function passwordReset( userId: string, - host: string | null, + origin: string | null, authRequestId?: string, ) { let medium = create(SendPasswordResetLinkSchema, { notificationType: NotificationType.Email, }); - if (host) { + if (origin) { medium = { ...medium, urlTemplate: - `${host.includes("localhost") ? "http://" : "https://"}${host}/password/set?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}` + + `${origin}/password/set?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}` + (authRequestId ? `&authRequestId=${authRequestId}` : ""), }; } From 36db734343525d0827c4e1c61d22e0bc45a4930e Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Tue, 3 Dec 2024 14:56:21 +0100 Subject: [PATCH 17/17] Revert "use origin header instead of host" This reverts commit 017c3215eba4f68ef1f4e190397e9e4c95ab2cef. --- .../app/(login)/authenticator/set/page.tsx | 4 ++++ .../src/app/(login)/otp/[method]/page.tsx | 4 ++-- apps/login/src/components/login-otp.tsx | 8 +++---- apps/login/src/lib/server/idp.ts | 10 ++++---- apps/login/src/lib/server/invite.ts | 4 ++-- apps/login/src/lib/server/loginname.ts | 24 +++++++++++-------- apps/login/src/lib/server/passkeys.ts | 2 +- apps/login/src/lib/server/password.ts | 4 ++-- apps/login/src/lib/zitadel.ts | 12 +++++----- 9 files changed, 40 insertions(+), 32 deletions(-) diff --git a/apps/login/src/app/(login)/authenticator/set/page.tsx b/apps/login/src/app/(login)/authenticator/set/page.tsx index a34a9e8c7c..36294c8c1c 100644 --- a/apps/login/src/app/(login)/authenticator/set/page.tsx +++ b/apps/login/src/app/(login)/authenticator/set/page.tsx @@ -110,6 +110,10 @@ export default async function Page(props: { params.set("authRequestId", authRequestId); } + const host = process.env.VERCEL_URL + ? `https://${process.env.VERCEL_URL}` + : "http://localhost:3000"; + return (
diff --git a/apps/login/src/app/(login)/otp/[method]/page.tsx b/apps/login/src/app/(login)/otp/[method]/page.tsx index c509cc6ee1..1c0904cee2 100644 --- a/apps/login/src/app/(login)/otp/[method]/page.tsx +++ b/apps/login/src/app/(login)/otp/[method]/page.tsx @@ -31,7 +31,7 @@ export default async function Page(props: { const loginSettings = await getLoginSettings(organization); - const origin = (await headers()).get("origin"); + const host = (await headers()).get("host"); return ( @@ -70,7 +70,7 @@ export default async function Page(props: { organization={organization} method={method} loginSettings={loginSettings} - origin={origin} + host={host} code={code} > )} diff --git a/apps/login/src/components/login-otp.tsx b/apps/login/src/components/login-otp.tsx index 6dbacdb66e..262541eb1b 100644 --- a/apps/login/src/components/login-otp.tsx +++ b/apps/login/src/components/login-otp.tsx @@ -25,7 +25,7 @@ type Props = { method: string; code?: string; loginSettings?: LoginSettings; - origin: string | null; + host: string | null; }; type Inputs = { @@ -40,7 +40,7 @@ export function LoginOTP({ method, code, loginSettings, - origin, + host, }: Props) { const t = useTranslations("otp"); @@ -81,10 +81,10 @@ export function LoginOTP({ otpEmail: { deliveryType: { case: "sendCode", - value: origin + value: host ? { urlTemplate: - `${origin}/otp/method=${method}?code={{.Code}}&userId={{.UserID}}&sessionId={{.SessionID}}&organization={{.OrgID}}` + + `${host.includes("localhost") ? "http://" : "https://"}${host}/otp/method=${method}?code={{.Code}}&userId={{.UserID}}&sessionId={{.SessionID}}&organization={{.OrgID}}` + (authRequestId ? `&authRequestId=${authRequestId}` : ""), } : {}, diff --git a/apps/login/src/lib/server/idp.ts b/apps/login/src/lib/server/idp.ts index 0b376ad4bc..ebb755987e 100644 --- a/apps/login/src/lib/server/idp.ts +++ b/apps/login/src/lib/server/idp.ts @@ -10,17 +10,17 @@ export type StartIDPFlowCommand = { }; export async function startIDPFlow(command: StartIDPFlowCommand) { - const origin = (await headers()).get("origin"); + const host = (await headers()).get("host"); - if (!origin) { - return { error: "Could not get origin" }; + if (!host) { + return { error: "Could not get host" }; } return startIdentityProviderFlow({ idpId: command.idpId, urls: { - successUrl: `${origin}${command.successUrl}`, - failureUrl: `${origin}${command.failureUrl}`, + successUrl: `${host.includes("localhost") ? "http://" : "https://"}${host}${command.successUrl}`, + failureUrl: `${host.includes("localhost") ? "http://" : "https://"}${host}${command.failureUrl}`, }, }).then((response) => { if ( diff --git a/apps/login/src/lib/server/invite.ts b/apps/login/src/lib/server/invite.ts index b9db345b21..3c68587898 100644 --- a/apps/login/src/lib/server/invite.ts +++ b/apps/login/src/lib/server/invite.ts @@ -20,7 +20,7 @@ export type RegisterUserResponse = { }; export async function inviteUser(command: InviteUserCommand) { - const origin = (await headers()).get("origin"); + const host = (await headers()).get("host"); const human = await addHumanUser({ email: command.email, @@ -34,7 +34,7 @@ export async function inviteUser(command: InviteUserCommand) { return { error: "Could not create user" }; } - const codeResponse = await createInviteCode(human.userId, origin); + const codeResponse = await createInviteCode(human.userId, host); if (!codeResponse || !human) { return { error: "Could not create invite code" }; diff --git a/apps/login/src/lib/server/loginname.ts b/apps/login/src/lib/server/loginname.ts index ca92ad1556..295f9b455f 100644 --- a/apps/login/src/lib/server/loginname.ts +++ b/apps/login/src/lib/server/loginname.ts @@ -53,10 +53,10 @@ export async function sendLoginname(command: SendLoginnameCommand) { }); if (identityProviders.length === 1) { - const origin = (await headers()).get("origin"); + const host = (await headers()).get("host"); - if (!origin) { - return { error: "Could not get origin" }; + if (!host) { + return { error: "Could not get host" }; } const identityProviderType = identityProviders[0].type; @@ -77,9 +77,11 @@ export async function sendLoginname(command: SendLoginnameCommand) { idpId: identityProviders[0].id, urls: { successUrl: - `${origin}/idp/${provider}/success?` + new URLSearchParams(params), + `${host.includes("localhost") ? "http://" : "https://"}${host}/idp/${provider}/success?` + + new URLSearchParams(params), failureUrl: - `${origin}/idp/${provider}/failure?` + new URLSearchParams(params), + `${host.includes("localhost") ? "http://" : "https://"}${host}/idp/${provider}/failure?` + + new URLSearchParams(params), }, }); @@ -95,10 +97,10 @@ export async function sendLoginname(command: SendLoginnameCommand) { }); if (identityProviders.length === 1) { - const origin = (await headers()).get("origin"); + const host = (await headers()).get("host"); - if (!origin) { - return { error: "Could not get origin" }; + if (!host) { + return { error: "Could not get host" }; } const identityProviderId = identityProviders[0].idpId; @@ -128,9 +130,11 @@ export async function sendLoginname(command: SendLoginnameCommand) { idpId: idp.id, urls: { successUrl: - `${origin}/idp/${provider}/success?` + new URLSearchParams(params), + `${host.includes("localhost") ? "http://" : "https://"}${host}/idp/${provider}/success?` + + new URLSearchParams(params), failureUrl: - `${origin}/idp/${provider}/failure?` + new URLSearchParams(params), + `${host.includes("localhost") ? "http://" : "https://"}${host}/idp/${provider}/failure?` + + new URLSearchParams(params), }, }); diff --git a/apps/login/src/lib/server/passkeys.ts b/apps/login/src/lib/server/passkeys.ts index 518b070993..181962cae1 100644 --- a/apps/login/src/lib/server/passkeys.ts +++ b/apps/login/src/lib/server/passkeys.ts @@ -40,7 +40,7 @@ export async function registerPasskeyLink( const host = (await headers()).get("host"); if (!host) { - throw new Error("Could not get host"); + throw new Error("Could not get domain"); } const [hostname, port] = host.split(":"); diff --git a/apps/login/src/lib/server/password.ts b/apps/login/src/lib/server/password.ts index 5b284a3e5e..5e202aabd5 100644 --- a/apps/login/src/lib/server/password.ts +++ b/apps/login/src/lib/server/password.ts @@ -31,7 +31,7 @@ type ResetPasswordCommand = { }; export async function resetPassword(command: ResetPasswordCommand) { - const origin = (await headers()).get("origin"); + const host = (await headers()).get("host"); const users = await listUsers({ loginName: command.loginName, @@ -47,7 +47,7 @@ export async function resetPassword(command: ResetPasswordCommand) { } const userId = users.result[0].userId; - return passwordReset(userId, origin, command.authRequestId); + return passwordReset(userId, host, command.authRequestId); } export type UpdateSessionCommand = { diff --git a/apps/login/src/lib/zitadel.ts b/apps/login/src/lib/zitadel.ts index 7210442fef..0afc4c4dc1 100644 --- a/apps/login/src/lib/zitadel.ts +++ b/apps/login/src/lib/zitadel.ts @@ -267,15 +267,15 @@ export async function resendInviteCode(userId: string) { return userService.resendInviteCode({ userId }, {}); } -export async function createInviteCode(userId: string, origin: string | null) { +export async function createInviteCode(userId: string, host: string | null) { let medium = create(SendInviteCodeSchema, { applicationName: "Typescript Login", }); - if (origin) { + if (host) { medium = { ...medium, - urlTemplate: `${origin}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}&invite=true`, + urlTemplate: `${host.includes("localhost") ? "http://" : "https://"}${host}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}&invite=true`, }; } @@ -506,18 +506,18 @@ export function createUser( */ export async function passwordReset( userId: string, - origin: string | null, + host: string | null, authRequestId?: string, ) { let medium = create(SendPasswordResetLinkSchema, { notificationType: NotificationType.Email, }); - if (origin) { + if (host) { medium = { ...medium, urlTemplate: - `${origin}/password/set?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}` + + `${host.includes("localhost") ? "http://" : "https://"}${host}/password/set?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}` + (authRequestId ? `&authRequestId=${authRequestId}` : ""), }; }