VYPR
High severityNVD Advisory· Published Mar 7, 2026· Updated Mar 9, 2026

ZITADEL: Account Takeover Due to Improper Instance Validation in V2 Login

CVE-2026-29067

Description

ZITADEL is an open source identity management platform. From version 4.0.0-rc.1 to 4.7.0, a potential vulnerability exists in ZITADEL's password reset mechanism in login V2. ZITADEL utilizes the Forwarded or X-Forwarded-Host header from incoming requests to construct the URL for the password reset confirmation link. This link, containing a secret code, is then emailed to the user. This issue has been patched in version 4.7.1.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
github.com/zitadel/zitadelGo
< 1.80.0-v2.20.0.20251208091519-4c879b47334e1.80.0-v2.20.0.20251208091519-4c879b47334e
github.com/zitadel/zitadelGo
>= 1.83.4, <= 1.87.5
github.com/zitadel/zitadelGo
>= 4.0.0-rc.1, < 4.7.14.7.1
github.com/zitadel/zitadel/v2Go
< 1.80.0-v2.20.0.20251208091519-4c879b47334e1.80.0-v2.20.0.20251208091519-4c879b47334e

Affected products

1

Patches

1
4c879b47334e

fix(login): Centralize host header resolution and forward headers to APIs

https://github.com/zitadel/zitadelMax PeintnerDec 8, 2025via ghsa
75 files changed · +1631 1639
  • apps/login/next.config.mjs+1 4 modified
    @@ -37,12 +37,9 @@ const nextConfig = {
       output: process.env.NEXT_OUTPUT_MODE || undefined,
       reactStrictMode: true,
       experimental: {
    -    dynamicIO: true,
         // Add React 19 compatibility optimizations
         optimizePackageImports: ['@radix-ui/react-tooltip', '@heroicons/react'],
    -  },
    -  eslint: {
    -    ignoreDuringBuilds: true,
    +    useCache: true,
       },
       // Improve SSR stability - not actually needed for React 19 SSR issues
       // onDemandEntries: {
    
  • apps/login/next-env.d.ts+1 0 modified
    @@ -1,5 +1,6 @@
     /// <reference types="next" />
     /// <reference types="next/image-types/global" />
    +/// <reference path="./.next/types/routes.d.ts" />
     
     // NOTE: This file should not be edited
     // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
    
  • apps/login/package.json+2 2 modified
    @@ -36,7 +36,7 @@
         "deepmerge": "^4.3.1",
         "lucide-react": "0.469.0",
         "moment": "^2.29.4",
    -    "next": "15.4.0-canary.86",
    +    "next": "15.5.7",
         "next-intl": "^3.25.1",
         "next-themes": "^0.2.1",
         "nice-grpc": "2.0.1",
    @@ -75,7 +75,7 @@
         "dotenv-cli": "^8.0.0",
         "env-cmd": "^10.1.0",
         "eslint": "^8.57.0",
    -    "eslint-config-next": "15.4.0-canary.86",
    +    "eslint-config-next": "15.5.7",
         "eslint-config-prettier": "^9.1.0",
         "gaxios": "^7.1.0",
         "grpc-tools": "1.13.0",
    
  • apps/login/src/app/(login)/accounts/page.tsx+8 14 modified
    @@ -2,8 +2,8 @@ import { DynamicTheme } from "@/components/dynamic-theme";
     import { SessionsList } from "@/components/sessions-list";
     import { Translated } from "@/components/translated";
     import { getAllSessionCookieIds } from "@/lib/cookies";
    -import { getServiceUrlFromHeaders } from "@/lib/service-url";
    -import { getBrandingSettings, getDefaultOrg, listSessions } from "@/lib/zitadel";
    +import { getServiceConfig } from "@/lib/service-url";
    +import { getBrandingSettings, getDefaultOrg, listSessions, ServiceConfig } from "@/lib/zitadel";
     import { UserPlusIcon } from "@heroicons/react/24/outline";
     import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb";
     import { Metadata } from "next";
    @@ -17,13 +17,11 @@ export async function generateMetadata(): Promise<Metadata> {
       return { title: t("title") };
     }
     
    -async function loadSessions({ serviceUrl }: { serviceUrl: string }) {
    +async function loadSessions({ serviceConfig }: { serviceConfig: ServiceConfig }) {
       const cookieIds = await getAllSessionCookieIds();
     
       if (cookieIds && cookieIds.length) {
    -    const response = await listSessions({
    -      serviceUrl,
    -      ids: cookieIds.filter((id) => !!id) as string[],
    +    const response = await listSessions({ serviceConfig, ids: cookieIds.filter((id) => !!id) as string[],
         });
         return response?.sessions ?? [];
       } else {
    @@ -39,23 +37,19 @@ export default async function Page(props: { searchParams: Promise<Record<string
       const organization = searchParams?.organization;
     
       const _headers = await headers();
    -  const { serviceUrl } = getServiceUrlFromHeaders(_headers);
    +  const { serviceConfig } = getServiceConfig(_headers);
     
       let defaultOrganization;
       if (!organization) {
    -    const org: Organization | null = await getDefaultOrg({
    -      serviceUrl,
    -    });
    +    const org: Organization | null = await getDefaultOrg({ serviceConfig, });
         if (org) {
           defaultOrganization = org.id;
         }
       }
     
    -  let sessions = await loadSessions({ serviceUrl });
    +  let sessions = await loadSessions({ serviceConfig });
     
    -  const branding = await getBrandingSettings({
    -    serviceUrl,
    -    organization: organization ?? defaultOrganization,
    +  const branding = await getBrandingSettings({ serviceConfig, organization: organization ?? defaultOrganization,
       });
     
       const params = new URLSearchParams();
    
  • apps/login/src/app/(login)/authenticator/set/page.tsx+11 23 modified
    @@ -6,7 +6,7 @@ import { SignInWithIdp } from "@/components/sign-in-with-idp";
     import { Translated } from "@/components/translated";
     import { UserAvatar } from "@/components/user-avatar";
     import { getSessionCookieById } from "@/lib/cookies";
    -import { getServiceUrlFromHeaders } from "@/lib/service-url";
    +import { getServiceConfig } from "@/lib/service-url";
     import { loadMostRecentSession } from "@/lib/session";
     import { checkUserVerification } from "@/lib/verify-helper";
     import {
    @@ -35,7 +35,7 @@ export default async function Page(props: { searchParams: Promise<Record<string
       const { loginName, requestId, organization, sessionId } = searchParams;
     
       const _headers = await headers();
    -  const { serviceUrl } = getServiceUrlFromHeaders(_headers);
    +  const { serviceConfig } = getServiceConfig(_headers);
     
       const sessionWithData = sessionId
         ? await loadSessionById(sessionId, organization)
    @@ -52,11 +52,9 @@ export default async function Page(props: { searchParams: Promise<Record<string
           throw Error("Could not get user id from session");
         }
     
    -    return listAuthenticationMethodTypes({
    -      serviceUrl,
    -      userId,
    +    return listAuthenticationMethodTypes({ serviceConfig, userId,
         }).then((methods) => {
    -      return getUserByID({ serviceUrl, userId }).then((user) => {
    +      return getUserByID({ serviceConfig, userId }).then((user) => {
             const humanUser = user.user?.type.case === "human" ? user.user?.type.value : undefined;
     
             return {
    @@ -71,25 +69,21 @@ export default async function Page(props: { searchParams: Promise<Record<string
       }
     
       async function loadSessionByLoginname(loginName?: string, organization?: string) {
    -    return loadMostRecentSession({
    -      serviceUrl,
    -      sessionParams: {
    +    return loadMostRecentSession({ serviceConfig, sessionParams: {
             loginName,
             organization,
           },
         }).then((session) => {
    -      return getAuthMethodsAndUser(serviceUrl, session);
    +      return getAuthMethodsAndUser(serviceConfig.baseUrl, session);
         });
       }
     
       async function loadSessionById(sessionId: string, organization?: string) {
         const recent = await getSessionCookieById({ sessionId, organization });
    -    return getSession({
    -      serviceUrl,
    -      sessionId: recent.id,
    +    return getSession({ serviceConfig, sessionId: recent.id,
           sessionToken: recent.token,
         }).then((sessionResponse) => {
    -      return getAuthMethodsAndUser(serviceUrl, sessionResponse.session);
    +      return getAuthMethodsAndUser(serviceConfig.baseUrl, sessionResponse.session);
         });
       }
     
    @@ -101,14 +95,10 @@ export default async function Page(props: { searchParams: Promise<Record<string
         );
       }
     
    -  const branding = await getBrandingSettings({
    -    serviceUrl,
    -    organization: sessionWithData.factors.user?.organizationId,
    +  const branding = await getBrandingSettings({ serviceConfig, organization: sessionWithData.factors.user?.organizationId,
       });
     
    -  const loginSettings = await getLoginSettings({
    -    serviceUrl,
    -    organization: sessionWithData.factors.user?.organizationId,
    +  const loginSettings = await getLoginSettings({ serviceConfig, organization: sessionWithData.factors.user?.organizationId,
       });
     
       // check if user was verified recently
    @@ -132,9 +122,7 @@ export default async function Page(props: { searchParams: Promise<Record<string
         redirect(`/verify?` + params);
       }
     
    -  const identityProviders = await getActiveIdentityProviders({
    -    serviceUrl,
    -    orgId: sessionWithData.factors?.user?.organizationId,
    +  const identityProviders = await getActiveIdentityProviders({ serviceConfig, orgId: sessionWithData.factors?.user?.organizationId,
         linking_allowed: true,
       }).then((resp) => {
         return resp.identityProviders;
    
  • apps/login/src/app/(login)/device/consent/page.tsx+5 11 modified
    @@ -1,7 +1,7 @@
     import { ConsentScreen } from "@/components/consent";
     import { DynamicTheme } from "@/components/dynamic-theme";
     import { Translated } from "@/components/translated";
    -import { getServiceUrlFromHeaders } from "@/lib/service-url";
    +import { getServiceConfig } from "@/lib/service-url";
     import { getBrandingSettings, getDefaultOrg, getDeviceAuthorizationRequest } from "@/lib/zitadel";
     import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb";
     import { headers } from "next/headers";
    @@ -22,11 +22,9 @@ export default async function Page(props: { searchParams: Promise<Record<string
       }
     
       const _headers = await headers();
    -  const { serviceUrl } = getServiceUrlFromHeaders(_headers);
    +  const { serviceConfig } = getServiceConfig(_headers);
     
    -  const { deviceAuthorizationRequest } = await getDeviceAuthorizationRequest({
    -    serviceUrl,
    -    userCode,
    +  const { deviceAuthorizationRequest } = await getDeviceAuthorizationRequest({ serviceConfig, userCode,
       });
     
       if (!deviceAuthorizationRequest) {
    @@ -39,17 +37,13 @@ export default async function Page(props: { searchParams: Promise<Record<string
     
       let defaultOrganization;
       if (!organization) {
    -    const org: Organization | null = await getDefaultOrg({
    -      serviceUrl,
    -    });
    +    const org: Organization | null = await getDefaultOrg({ serviceConfig, });
         if (org) {
           defaultOrganization = org.id;
         }
       }
     
    -  const branding = await getBrandingSettings({
    -    serviceUrl,
    -    organization: organization ?? defaultOrganization,
    +  const branding = await getBrandingSettings({ serviceConfig, organization: organization ?? defaultOrganization,
       });
     
       const params = new URLSearchParams();
    
  • apps/login/src/app/(login)/device/page.tsx+4 8 modified
    @@ -1,7 +1,7 @@
     import { DeviceCodeForm } from "@/components/device-code-form";
     import { DynamicTheme } from "@/components/dynamic-theme";
     import { Translated } from "@/components/translated";
    -import { getServiceUrlFromHeaders } from "@/lib/service-url";
    +import { getServiceConfig } from "@/lib/service-url";
     import { getBrandingSettings, getDefaultOrg } from "@/lib/zitadel";
     import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb";
     import { Metadata } from "next";
    @@ -20,21 +20,17 @@ export default async function Page(props: { searchParams: Promise<Record<string
       const organization = searchParams?.organization;
     
       const _headers = await headers();
    -  const { serviceUrl } = getServiceUrlFromHeaders(_headers);
    +  const { serviceConfig } = getServiceConfig(_headers);
     
       let defaultOrganization;
       if (!organization) {
    -    const org: Organization | null = await getDefaultOrg({
    -      serviceUrl,
    -    });
    +    const org: Organization | null = await getDefaultOrg({ serviceConfig, });
         if (org) {
           defaultOrganization = org.id;
         }
       }
     
    -  const branding = await getBrandingSettings({
    -    serviceUrl,
    -    organization: organization ?? defaultOrganization,
    +  const branding = await getBrandingSettings({ serviceConfig, organization: organization ?? defaultOrganization,
       });
     
       return (
    
  • apps/login/src/app/(login)/idp/ldap/page.tsx+4 8 modified
    @@ -1,7 +1,7 @@
     import { DynamicTheme } from "@/components/dynamic-theme";
     import { LDAPUsernamePasswordForm } from "@/components/ldap-username-password-form";
     import { Translated } from "@/components/translated";
    -import { getServiceUrlFromHeaders } from "@/lib/service-url";
    +import { getServiceConfig } from "@/lib/service-url";
     import { getBrandingSettings, getDefaultOrg } from "@/lib/zitadel";
     import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb";
     import { headers } from "next/headers";
    @@ -18,21 +18,17 @@ export default async function Page(props: {
       }
     
       const _headers = await headers();
    -  const { serviceUrl } = getServiceUrlFromHeaders(_headers);
    +  const { serviceConfig } = getServiceConfig(_headers);
     
       let defaultOrganization;
       if (!organization) {
    -    const org: Organization | null = await getDefaultOrg({
    -      serviceUrl,
    -    });
    +    const org: Organization | null = await getDefaultOrg({ serviceConfig, });
         if (org) {
           defaultOrganization = org.id;
         }
       }
     
    -  const branding = await getBrandingSettings({
    -    serviceUrl,
    -    organization: organization ?? defaultOrganization,
    +  const branding = await getBrandingSettings({ serviceConfig, organization: organization ?? defaultOrganization,
       });
     
       // return login failed if no linking or creation is allowed and no user was found
    
  • apps/login/src/app/(login)/idp/page.tsx+4 8 modified
    @@ -1,7 +1,7 @@
     import { DynamicTheme } from "@/components/dynamic-theme";
     import { SignInWithIdp } from "@/components/sign-in-with-idp";
     import { Translated } from "@/components/translated";
    -import { getServiceUrlFromHeaders } from "@/lib/service-url";
    +import { getServiceConfig } from "@/lib/service-url";
     import { getActiveIdentityProviders, getBrandingSettings } from "@/lib/zitadel";
     import { Metadata } from "next";
     import { getTranslations } from "next-intl/server";
    @@ -19,18 +19,14 @@ export default async function Page(props: { searchParams: Promise<Record<string
       const organization = searchParams?.organization;
     
       const _headers = await headers();
    -  const { serviceUrl } = getServiceUrlFromHeaders(_headers);
    +  const { serviceConfig } = getServiceConfig(_headers);
     
    -  const identityProviders = await getActiveIdentityProviders({
    -    serviceUrl,
    -    orgId: organization,
    +  const identityProviders = await getActiveIdentityProviders({ serviceConfig, orgId: organization,
       }).then((resp) => {
         return resp.identityProviders;
       });
     
    -  const branding = await getBrandingSettings({
    -    serviceUrl,
    -    organization,
    +  const branding = await getBrandingSettings({ serviceConfig, organization,
       });
     
       return (
    
  • apps/login/src/app/(login)/idp/[provider]/account-not-found/page.tsx+4 8 modified
    @@ -2,7 +2,7 @@ import { Alert, AlertType } from "@/components/alert";
     import { Button } from "@/components/button";
     import { DynamicTheme } from "@/components/dynamic-theme";
     import { Translated } from "@/components/translated";
    -import { getServiceUrlFromHeaders } from "@/lib/service-url";
    +import { getServiceConfig } from "@/lib/service-url";
     import { getBrandingSettings, getDefaultOrg } from "@/lib/zitadel";
     import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb";
     import { Metadata } from "next";
    @@ -20,21 +20,17 @@ export default async function Page(props: { searchParams: Promise<Record<string
       const { organization, postErrorRedirectUrl } = searchParams;
     
       const _headers = await headers();
    -  const { serviceUrl } = getServiceUrlFromHeaders(_headers);
    +  const { serviceConfig } = getServiceConfig(_headers);
     
       let defaultOrganization;
       if (!organization) {
    -    const org: Organization | null = await getDefaultOrg({
    -      serviceUrl,
    -    });
    +    const org: Organization | null = await getDefaultOrg({ serviceConfig, });
         if (org) {
           defaultOrganization = org.id;
         }
       }
     
    -  const branding = await getBrandingSettings({
    -    serviceUrl,
    -    organization: organization ?? defaultOrganization,
    +  const branding = await getBrandingSettings({ serviceConfig, organization: organization ?? defaultOrganization,
       });
     
       return (
    
  • apps/login/src/app/(login)/idp/[provider]/complete-registration/page.tsx+3 5 modified
    @@ -1,7 +1,7 @@
     import { DynamicTheme } from "@/components/dynamic-theme";
     import { RegisterFormIDPIncomplete } from "@/components/register-form-idp-incomplete";
     import { Translated } from "@/components/translated";
    -import { getServiceUrlFromHeaders } from "@/lib/service-url";
    +import { getServiceConfig } from "@/lib/service-url";
     import { getBrandingSettings } from "@/lib/zitadel";
     import { headers } from "next/headers";
     
    @@ -16,11 +16,9 @@ export default async function CompleteRegistrationPage(props: {
       const { id, token, requestId, organization, idpId, idpUserId, idpUserName, givenName, familyName, email } = searchParams;
     
       const _headers = await headers();
    -  const { serviceUrl } = getServiceUrlFromHeaders(_headers);
    +  const { serviceConfig } = getServiceConfig(_headers);
     
    -  const branding = await getBrandingSettings({
    -    serviceUrl,
    -    organization,
    +  const branding = await getBrandingSettings({ serviceConfig, organization,
       });
     
       if (!id || !token || !idpId || !organization || !idpUserId || !idpUserName) {
    
  • apps/login/src/app/(login)/idp/[provider]/failure/page.tsx+6 14 modified
    @@ -3,7 +3,7 @@ import { ChooseAuthenticatorToLogin } from "@/components/choose-authenticator-to
     import { DynamicTheme } from "@/components/dynamic-theme";
     import { Translated } from "@/components/translated";
     import { UserAvatar } from "@/components/user-avatar";
    -import { getServiceUrlFromHeaders } from "@/lib/service-url";
    +import { getServiceConfig } from "@/lib/service-url";
     import { getBrandingSettings, getLoginSettings, getUserByID, listAuthenticationMethodTypes } from "@/lib/zitadel";
     import { HumanUser, User } from "@zitadel/proto/zitadel/user/v2/user_pb";
     import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
    @@ -18,16 +18,12 @@ export default async function Page(props: {
       const { organization, userId } = searchParams;
     
       const _headers = await headers();
    -  const { serviceUrl } = getServiceUrlFromHeaders(_headers);
    +  const { serviceConfig } = getServiceConfig(_headers);
     
    -  const branding = await getBrandingSettings({
    -    serviceUrl,
    -    organization,
    +  const branding = await getBrandingSettings({ serviceConfig, organization,
       });
     
    -  const loginSettings = await getLoginSettings({
    -    serviceUrl,
    -    organization,
    +  const loginSettings = await getLoginSettings({ serviceConfig, organization,
       });
     
       let authMethods: AuthenticationMethodType[] = [];
    @@ -43,9 +39,7 @@ export default async function Page(props: {
       }
     
       if (userId) {
    -    const userResponse = await getUserByID({
    -      serviceUrl,
    -      userId,
    +    const userResponse = await getUserByID({ serviceConfig, userId,
         });
         if (userResponse) {
           user = userResponse.user;
    @@ -58,9 +52,7 @@ export default async function Page(props: {
           }
         }
     
    -    const authMethodsResponse = await listAuthenticationMethodTypes({
    -      serviceUrl,
    -      userId,
    +    const authMethodsResponse = await listAuthenticationMethodTypes({ serviceConfig, userId,
         });
         if (authMethodsResponse.authMethodTypes) {
           authMethods = authMethodsResponse.authMethodTypes;
    
  • apps/login/src/app/(login)/idp/[provider]/linking-failed/page.tsx+3 5 modified
    @@ -1,6 +1,6 @@
     import { DynamicTheme } from "@/components/dynamic-theme";
     import { Translated } from "@/components/translated";
    -import { getServiceUrlFromHeaders } from "@/lib/service-url";
    +import { getServiceConfig } from "@/lib/service-url";
     import { getBrandingSettings } from "@/lib/zitadel";
     import { headers } from "next/headers";
     
    @@ -15,11 +15,9 @@ export default async function LinkingFailedPage(props: {
       const { organization, error } = searchParams;
     
       const _headers = await headers();
    -  const { serviceUrl } = getServiceUrlFromHeaders(_headers);
    +  const { serviceConfig } = getServiceConfig(_headers);
     
    -  const branding = await getBrandingSettings({
    -    serviceUrl,
    -    organization,
    +  const branding = await getBrandingSettings({ serviceConfig, organization,
       });
     
       return (
    
  • apps/login/src/app/(login)/idp/[provider]/registration-failed/page.tsx+4 8 modified
    @@ -2,7 +2,7 @@ import { Alert, AlertType } from "@/components/alert";
     import { Button } from "@/components/button";
     import { DynamicTheme } from "@/components/dynamic-theme";
     import { Translated } from "@/components/translated";
    -import { getServiceUrlFromHeaders } from "@/lib/service-url";
    +import { getServiceConfig } from "@/lib/service-url";
     import { getBrandingSettings, getDefaultOrg } from "@/lib/zitadel";
     import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb";
     import { Metadata } from "next";
    @@ -20,21 +20,17 @@ export default async function Page(props: { searchParams: Promise<Record<string
       const { organization, postErrorRedirectUrl } = searchParams;
     
       const _headers = await headers();
    -  const { serviceUrl } = getServiceUrlFromHeaders(_headers);
    +  const { serviceConfig } = getServiceConfig(_headers);
     
       let defaultOrganization;
       if (!organization) {
    -    const org: Organization | null = await getDefaultOrg({
    -      serviceUrl,
    -    });
    +    const org: Organization | null = await getDefaultOrg({ serviceConfig, });
         if (org) {
           defaultOrganization = org.id;
         }
       }
     
    -  const branding = await getBrandingSettings({
    -    serviceUrl,
    -    organization: organization ?? defaultOrganization,
    +  const branding = await getBrandingSettings({ serviceConfig, organization: organization ?? defaultOrganization,
       });
     
       return (
    
  • apps/login/src/app/(login)/loginname/page.tsx+7 17 modified
    @@ -2,7 +2,7 @@ import { DynamicTheme } from "@/components/dynamic-theme";
     import { SignInWithIdp } from "@/components/sign-in-with-idp";
     import { Translated } from "@/components/translated";
     import { UsernameForm } from "@/components/username-form";
    -import { getServiceUrlFromHeaders } from "@/lib/service-url";
    +import { getServiceConfig } from "@/lib/service-url";
     import { getActiveIdentityProviders, getBrandingSettings, getDefaultOrg, getLoginSettings } from "@/lib/zitadel";
     import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb";
     import { Metadata } from "next";
    @@ -24,38 +24,28 @@ export default async function Page(props: { searchParams: Promise<Record<string
       const submit: boolean = searchParams?.submit === "true";
     
       const _headers = await headers();
    -  const { serviceUrl } = getServiceUrlFromHeaders(_headers);
    +  const { serviceConfig } = getServiceConfig(_headers);
     
       let defaultOrganization;
       if (!organization) {
    -    const org: Organization | null = await getDefaultOrg({
    -      serviceUrl,
    -    });
    +    const org: Organization | null = await getDefaultOrg({ serviceConfig, });
         if (org) {
           defaultOrganization = org.id;
         }
       }
     
    -  const loginSettings = await getLoginSettings({
    -    serviceUrl,
    -    organization: organization ?? defaultOrganization,
    +  const loginSettings = await getLoginSettings({ serviceConfig, organization: organization ?? defaultOrganization,
       });
     
    -  const contextLoginSettings = await getLoginSettings({
    -    serviceUrl,
    -    organization,
    +  const contextLoginSettings = await getLoginSettings({ serviceConfig, organization,
       });
     
    -  const identityProviders = await getActiveIdentityProviders({
    -    serviceUrl,
    -    orgId: organization ?? defaultOrganization,
    +  const identityProviders = await getActiveIdentityProviders({ serviceConfig, orgId: organization ?? defaultOrganization,
       }).then((resp) => {
         return resp.identityProviders;
       });
     
    -  const branding = await getBrandingSettings({
    -    serviceUrl,
    -    organization: organization ?? defaultOrganization,
    +  const branding = await getBrandingSettings({ serviceConfig, organization: organization ?? defaultOrganization,
       });
     
       return (
    
  • apps/login/src/app/(login)/logout/done/page.tsx+3 5 modified
    @@ -1,20 +1,18 @@
     import { DynamicTheme } from "@/components/dynamic-theme";
     import { Translated } from "@/components/translated";
    -import { getServiceUrlFromHeaders } from "@/lib/service-url";
    +import { getServiceConfig } from "@/lib/service-url";
     import { getBrandingSettings } from "@/lib/zitadel";
     import { headers } from "next/headers";
     
     export default async function Page(props: { searchParams: Promise<any> }) {
       const searchParams = await props.searchParams;
     
       const _headers = await headers();
    -  const { serviceUrl } = getServiceUrlFromHeaders(_headers);
    +  const { serviceConfig } = getServiceConfig(_headers);
     
       const { organization } = searchParams;
     
    -  const branding = await getBrandingSettings({
    -    serviceUrl,
    -    organization,
    +  const branding = await getBrandingSettings({ serviceConfig, organization,
       });
     
       return (
    
  • apps/login/src/app/(login)/logout/page.tsx+35 21 modified
    @@ -2,26 +2,24 @@ import { DynamicTheme } from "@/components/dynamic-theme";
     import { SessionsClearList } from "@/components/sessions-clear-list";
     import { Translated } from "@/components/translated";
     import { getAllSessionCookieIds } from "@/lib/cookies";
    -import { getServiceUrlFromHeaders } from "@/lib/service-url";
    -import { getBrandingSettings, getDefaultOrg, listSessions } from "@/lib/zitadel";
    +import { getServiceConfig } from "@/lib/service-url";
    +import { getBrandingSettings, getDefaultOrg, listSessions, ServiceConfig } from "@/lib/zitadel";
    +import { verifyJwt } from "@zitadel/client/node";
     import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb";
     import { Metadata } from "next";
     import { getTranslations } from "next-intl/server";
     import { headers } from "next/headers";
     
     export async function generateMetadata(): Promise<Metadata> {
       const t = await getTranslations("logout");
    -  return { title: t('title')};
    +  return { title: t("title") };
     }
     
    -async function loadSessions({ serviceUrl }: { serviceUrl: string }) {
    +async function loadSessions({ serviceConfig }: { serviceConfig: ServiceConfig }) {
       const cookieIds = await getAllSessionCookieIds();
     
       if (cookieIds && cookieIds.length) {
    -    const response = await listSessions({
    -      serviceUrl,
    -      ids: cookieIds.filter((id) => !!id) as string[],
    -    });
    +    const response = await listSessions({ serviceConfig, ids: cookieIds.filter((id) => !!id) as string[] });
         return response?.sessions ?? [];
       } else {
         console.info("No session cookie found.");
    @@ -33,30 +31,46 @@ export default async function Page(props: { searchParams: Promise<Record<string
       const searchParams = await props.searchParams;
     
       const organization = searchParams?.organization;
    -  const postLogoutRedirectUri = searchParams?.post_logout_redirect || searchParams?.post_logout_redirect_uri;
    -  const logoutHint = searchParams?.logout_hint;
    -  // TODO implement with new translation service
    -  // const UILocales = searchParams?.ui_locales;
    +  const logoutToken = searchParams?.logout_token;
     
       const _headers = await headers();
    -  const { serviceUrl } = getServiceUrlFromHeaders(_headers);
    +  const { serviceConfig } = getServiceConfig(_headers);
    +
    +  let postLogoutRedirectUri, logoutHint;
    +  if (logoutToken) {
    +    try {
    +      const payload = await verifyJwt<{ post_logout_redirect_uri?: string; logoutHint?: string }>(
    +        logoutToken,
    +        `${serviceConfig.baseUrl}/oauth/v2/keys`,
    +        {
    +          instanceHost: serviceConfig.instanceHost,
    +          publicHost: serviceConfig.publicHost,
    +        },
    +      );
    +      console.log("logout token payload", payload);
    +
    +      if (payload.post_logout_redirect_uri && typeof payload.post_logout_redirect_uri === "string") {
    +        postLogoutRedirectUri = payload.post_logout_redirect_uri;
    +      }
    +      if (payload.logout_hint && typeof payload.logout_hint === "string") {
    +        logoutHint = payload.logout_hint;
    +      }
    +    } catch (error) {
    +      console.error("Failed to verify logout token", error);
    +    }
    +  }
     
       let defaultOrganization;
       if (!organization) {
    -    const org: Organization | null = await getDefaultOrg({
    -      serviceUrl,
    -    });
    +    const org: Organization | null = await getDefaultOrg({ serviceConfig });
         if (org) {
           defaultOrganization = org.id;
         }
       }
     
    -  let sessions = await loadSessions({ serviceUrl });
    +  let sessions = await loadSessions({ serviceConfig });
     
    -  const branding = await getBrandingSettings({
    -    serviceUrl,
    -    organization: organization ?? defaultOrganization,
    -  });
    +  const branding = await getBrandingSettings({ serviceConfig, organization: organization ?? defaultOrganization });
     
       const params = new URLSearchParams();
     
    
  • apps/login/src/app/(login)/mfa/page.tsx+9 19 modified
    @@ -5,7 +5,7 @@ import { DynamicTheme } from "@/components/dynamic-theme";
     import { Translated } from "@/components/translated";
     import { UserAvatar } from "@/components/user-avatar";
     import { getSessionCookieById } from "@/lib/cookies";
    -import { getServiceUrlFromHeaders } from "@/lib/service-url";
    +import { getServiceConfig } from "@/lib/service-url";
     import { loadMostRecentSession } from "@/lib/session";
     import { getBrandingSettings, getSession, listAuthenticationMethodTypes } from "@/lib/zitadel";
     import { Metadata } from "next";
    @@ -23,24 +23,20 @@ export default async function Page(props: { searchParams: Promise<Record<string
       const { loginName, requestId, organization, sessionId } = searchParams;
     
       const _headers = await headers();
    -  const { serviceUrl } = getServiceUrlFromHeaders(_headers);
    +  const { serviceConfig } = getServiceConfig(_headers);
     
       const sessionFactors = sessionId
    -    ? await loadSessionById(serviceUrl, sessionId, organization)
    -    : await loadSessionByLoginname(serviceUrl, loginName, organization);
    +    ? await loadSessionById(serviceConfig.baseUrl, sessionId, organization)
    +    : await loadSessionByLoginname(serviceConfig.baseUrl, loginName, organization);
     
       async function loadSessionByLoginname(serviceUrl: string, loginName?: string, organization?: string) {
    -    return loadMostRecentSession({
    -      serviceUrl,
    -      sessionParams: {
    +    return loadMostRecentSession({ serviceConfig, sessionParams: {
             loginName,
             organization,
           },
         }).then((session) => {
           if (session && session.factors?.user?.id) {
    -        return listAuthenticationMethodTypes({
    -          serviceUrl,
    -          userId: session.factors.user.id,
    +        return listAuthenticationMethodTypes({ serviceConfig, userId: session.factors.user.id,
             }).then((methods) => {
               return {
                 factors: session?.factors,
    @@ -53,15 +49,11 @@ export default async function Page(props: { searchParams: Promise<Record<string
     
       async function loadSessionById(host: string, sessionId: string, organization?: string) {
         const recent = await getSessionCookieById({ sessionId, organization });
    -    return getSession({
    -      serviceUrl,
    -      sessionId: recent.id,
    +    return getSession({ serviceConfig, sessionId: recent.id,
           sessionToken: recent.token,
         }).then((response) => {
           if (response?.session && response.session.factors?.user?.id) {
    -        return listAuthenticationMethodTypes({
    -          serviceUrl,
    -          userId: response.session.factors.user.id,
    +        return listAuthenticationMethodTypes({ serviceConfig, userId: response.session.factors.user.id,
             }).then((methods) => {
               return {
                 factors: response.session?.factors,
    @@ -72,9 +64,7 @@ export default async function Page(props: { searchParams: Promise<Record<string
         });
       }
     
    -  const branding = await getBrandingSettings({
    -    serviceUrl,
    -    organization,
    +  const branding = await getBrandingSettings({ serviceConfig, organization,
       });
     
       return (
    
  • apps/login/src/app/(login)/mfa/set/page.tsx+8 18 modified
    @@ -5,7 +5,7 @@ import { DynamicTheme } from "@/components/dynamic-theme";
     import { Translated } from "@/components/translated";
     import { UserAvatar } from "@/components/user-avatar";
     import { getSessionCookieById } from "@/lib/cookies";
    -import { getServiceUrlFromHeaders } from "@/lib/service-url";
    +import { getServiceConfig } from "@/lib/service-url";
     import { loadMostRecentSession } from "@/lib/session";
     import {
       getBrandingSettings,
    @@ -46,7 +46,7 @@ export default async function Page(props: { searchParams: Promise<Record<string
       const { loginName, checkAfter, force, requestId, organization, sessionId } = searchParams;
     
       const _headers = await headers();
    -  const { serviceUrl } = getServiceUrlFromHeaders(_headers);
    +  const { serviceConfig } = getServiceConfig(_headers);
     
       const sessionWithData = sessionId
         ? await loadSessionById(sessionId, organization)
    @@ -59,11 +59,9 @@ export default async function Page(props: { searchParams: Promise<Record<string
           throw Error("Could not get user id from session");
         }
     
    -    return listAuthenticationMethodTypes({
    -      serviceUrl,
    -      userId,
    +    return listAuthenticationMethodTypes({ serviceConfig, userId,
         }).then((methods) => {
    -      return getUserByID({ serviceUrl, userId }).then((user) => {
    +      return getUserByID({ serviceConfig, userId }).then((user) => {
             const humanUser = user.user?.type.case === "human" ? user.user?.type.value : undefined;
     
             return {
    @@ -79,9 +77,7 @@ export default async function Page(props: { searchParams: Promise<Record<string
       }
     
       async function loadSessionByLoginname(loginName?: string, organization?: string) {
    -    return loadMostRecentSession({
    -      serviceUrl,
    -      sessionParams: {
    +    return loadMostRecentSession({ serviceConfig, sessionParams: {
             loginName,
             organization,
           },
    @@ -92,22 +88,16 @@ export default async function Page(props: { searchParams: Promise<Record<string
     
       async function loadSessionById(sessionId: string, organization?: string) {
         const recent = await getSessionCookieById({ sessionId, organization });
    -    return getSession({
    -      serviceUrl,
    -      sessionId: recent.id,
    +    return getSession({ serviceConfig, sessionId: recent.id,
           sessionToken: recent.token,
         }).then((sessionResponse) => {
           return getAuthMethodsAndUser(sessionResponse.session);
         });
       }
     
    -  const branding = await getBrandingSettings({
    -    serviceUrl,
    -    organization,
    +  const branding = await getBrandingSettings({ serviceConfig, organization,
       });
    -  const loginSettings = await getLoginSettings({
    -    serviceUrl,
    -    organization: sessionWithData.factors?.user?.organizationId,
    +  const loginSettings = await getLoginSettings({ serviceConfig, organization: sessionWithData.factors?.user?.organizationId,
       });
     
       const { valid } = isSessionValid(sessionWithData);
    
  • apps/login/src/app/(login)/otp/[method]/page.tsx+8 15 modified
    @@ -4,8 +4,8 @@ import { LoginOTP } from "@/components/login-otp";
     import { Translated } from "@/components/translated";
     import { UserAvatar } from "@/components/user-avatar";
     import { getSessionCookieById } from "@/lib/cookies";
    -import { getOriginalHost } from "@/lib/server/host";
    -import { getServiceUrlFromHeaders } from "@/lib/service-url";
    +import { getPublicHost } from "@/lib/server/host";
    +import { getServiceConfig } from "@/lib/service-url";
     import { loadMostRecentSession } from "@/lib/session";
     import { getBrandingSettings, getLoginSettings, getSession } from "@/lib/zitadel";
     import { Metadata } from "next";
    @@ -25,8 +25,8 @@ export default async function Page(props: {
       const searchParams = await props.searchParams;
     
       const _headers = await headers();
    -  const { serviceUrl } = getServiceUrlFromHeaders(_headers);
    -  const host = await getOriginalHost();
    +  const { serviceConfig } = getServiceConfig(_headers);
    +  const host = getPublicHost(_headers);
     
       const {
         loginName, // send from password page
    @@ -40,18 +40,11 @@ export default async function Page(props: {
     
       const session = sessionId
         ? await loadSessionById(sessionId, organization)
    -    : await loadMostRecentSession({
    -        serviceUrl,
    -        sessionParams: { loginName, organization },
    -      });
    +    : await loadMostRecentSession({ serviceConfig, sessionParams: { loginName, organization } });
     
       async function loadSessionById(sessionId: string, organization?: string) {
         const recent = await getSessionCookieById({ sessionId, organization });
    -    return getSession({
    -      serviceUrl,
    -      sessionId: recent.id,
    -      sessionToken: recent.token,
    -    }).then((response) => {
    +    return getSession({ serviceConfig, sessionId: recent.id, sessionToken: recent.token }).then((response) => {
           if (response?.session) {
             return response.session;
           }
    @@ -60,12 +53,12 @@ export default async function Page(props: {
     
       // email links do not come with organization, thus we need to use the session's organization
       const branding = await getBrandingSettings({
    -    serviceUrl,
    +    serviceConfig,
         organization: organization ?? session?.factors?.user?.organizationId,
       });
     
       const loginSettings = await getLoginSettings({
    -    serviceUrl,
    +    serviceConfig,
         organization: organization ?? session?.factors?.user?.organizationId,
       });
     
    
  • apps/login/src/app/(login)/otp/[method]/set/page.tsx+8 20 modified
    @@ -5,7 +5,7 @@ import { DynamicTheme } from "@/components/dynamic-theme";
     import { TotpRegister } from "@/components/totp-register";
     import { Translated } from "@/components/translated";
     import { UserAvatar } from "@/components/user-avatar";
    -import { getServiceUrlFromHeaders } from "@/lib/service-url";
    +import { getServiceConfig } from "@/lib/service-url";
     import { loadMostRecentSession } from "@/lib/session";
     import { addOTPEmail, addOTPSMS, getBrandingSettings, getLoginSettings, registerTOTP } from "@/lib/zitadel";
     import { RegisterTOTPResponse } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
    @@ -24,20 +24,14 @@ export default async function Page(props: {
       const { method } = params;
     
       const _headers = await headers();
    -  const { serviceUrl } = getServiceUrlFromHeaders(_headers);
    +  const { serviceConfig } = getServiceConfig(_headers);
     
    -  const branding = await getBrandingSettings({
    -    serviceUrl,
    -    organization,
    +  const branding = await getBrandingSettings({ serviceConfig, organization,
       });
    -  const loginSettings = await getLoginSettings({
    -    serviceUrl,
    -    organization,
    +  const loginSettings = await getLoginSettings({ serviceConfig, organization,
       });
     
    -  const session = await loadMostRecentSession({
    -    serviceUrl,
    -    sessionParams: {
    +  const session = await loadMostRecentSession({ serviceConfig, sessionParams: {
           loginName,
           organization,
         },
    @@ -46,9 +40,7 @@ export default async function Page(props: {
       let totpResponse: RegisterTOTPResponse | undefined, error: Error | undefined;
       if (session && session.factors?.user?.id) {
         if (method === "time-based") {
    -      await registerTOTP({
    -        serviceUrl,
    -        userId: session.factors.user.id,
    +      await registerTOTP({ serviceConfig, userId: session.factors.user.id,
           })
             .then((resp) => {
               if (resp) {
    @@ -59,17 +51,13 @@ export default async function Page(props: {
               error = err;
             });
         } else if (method === "sms") {
    -      await addOTPSMS({
    -        serviceUrl,
    -        userId: session.factors.user.id,
    +      await addOTPSMS({ serviceConfig, userId: session.factors.user.id,
           }).catch((_error) => {
             // TODO: Throw this error?
             new Error("Could not add OTP via SMS");
           });
         } else if (method === "email") {
    -      await addOTPEmail({
    -        serviceUrl,
    -        userId: session.factors.user.id,
    +      await addOTPEmail({ serviceConfig, userId: session.factors.user.id,
           }).catch((_error) => {
             // TODO: Throw this error?
             new Error("Could not add OTP via Email");
    
  • apps/login/src/app/(login)/passkey/page.tsx+6 12 modified
    @@ -4,7 +4,7 @@ import { LoginPasskey } from "@/components/login-passkey";
     import { Translated } from "@/components/translated";
     import { UserAvatar } from "@/components/user-avatar";
     import { getSessionCookieById } from "@/lib/cookies";
    -import { getServiceUrlFromHeaders } from "@/lib/service-url";
    +import { getServiceConfig } from "@/lib/service-url";
     import { loadMostRecentSession } from "@/lib/session";
     import { getBrandingSettings, getSession } from "@/lib/zitadel";
     import { Metadata } from "next";
    @@ -22,20 +22,16 @@ export default async function Page(props: { searchParams: Promise<Record<string
       const { loginName, altPassword, requestId, organization, sessionId } = searchParams;
     
       const _headers = await headers();
    -  const { serviceUrl } = getServiceUrlFromHeaders(_headers);
    +  const { serviceConfig } = getServiceConfig(_headers);
     
       const sessionFactors = sessionId
    -    ? await loadSessionById(serviceUrl, sessionId, organization)
    -    : await loadMostRecentSession({
    -        serviceUrl,
    -        sessionParams: { loginName, organization },
    +    ? await loadSessionById(serviceConfig.baseUrl, sessionId, organization)
    +    : await loadMostRecentSession({ serviceConfig, sessionParams: { loginName, organization },
           });
     
       async function loadSessionById(serviceUrl: string, sessionId: string, organization?: string) {
         const recent = await getSessionCookieById({ sessionId, organization });
    -    return getSession({
    -      serviceUrl,
    -      sessionId: recent.id,
    +    return getSession({ serviceConfig, sessionId: recent.id,
           sessionToken: recent.token,
         }).then((response) => {
           if (response?.session) {
    @@ -44,9 +40,7 @@ export default async function Page(props: { searchParams: Promise<Record<string
         });
       }
     
    -  const branding = await getBrandingSettings({
    -    serviceUrl,
    -    organization,
    +  const branding = await getBrandingSettings({ serviceConfig, organization,
       });
     
       return (
    
  • apps/login/src/app/(login)/passkey/set/page.tsx+5 11 modified
    @@ -3,7 +3,7 @@ import { DynamicTheme } from "@/components/dynamic-theme";
     import { RegisterPasskey } from "@/components/register-passkey";
     import { Translated } from "@/components/translated";
     import { UserAvatar } from "@/components/user-avatar";
    -import { getServiceUrlFromHeaders } from "@/lib/service-url";
    +import { getServiceConfig } from "@/lib/service-url";
     import { loadMostRecentSession } from "@/lib/session";
     import { getBrandingSettings, getUserByID } from "@/lib/zitadel";
     import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
    @@ -23,31 +23,25 @@ export default async function Page(props: { searchParams: Promise<Record<string
       const { userId, loginName, prompt, organization, requestId, code, codeId } = searchParams;
     
       const _headers = await headers();
    -  const { serviceUrl } = getServiceUrlFromHeaders(_headers);
    +  const { serviceConfig } = getServiceConfig(_headers);
     
       // also allow no session to be found for userId-based flows
       let session: Session | undefined;
       if (loginName) {
    -    session = await loadMostRecentSession({
    -      serviceUrl,
    -      sessionParams: {
    +    session = await loadMostRecentSession({ serviceConfig, sessionParams: {
             loginName,
             organization,
           },
         });
       }
     
    -  const branding = await getBrandingSettings({
    -    serviceUrl,
    -    organization,
    +  const branding = await getBrandingSettings({ serviceConfig, organization,
       });
     
       let user: User | undefined;
       let displayName: string | undefined;
       if (userId) {
    -    const userResponse = await getUserByID({
    -      serviceUrl,
    -      userId,
    +    const userResponse = await getUserByID({ serviceConfig, userId,
         });
         user = userResponse.user;
     
    
  • apps/login/src/app/(login)/password/change/page.tsx+6 14 modified
    @@ -3,7 +3,7 @@ import { ChangePasswordForm } from "@/components/change-password-form";
     import { DynamicTheme } from "@/components/dynamic-theme";
     import { Translated } from "@/components/translated";
     import { UserAvatar } from "@/components/user-avatar";
    -import { getServiceUrlFromHeaders } from "@/lib/service-url";
    +import { getServiceConfig } from "@/lib/service-url";
     import { loadMostRecentSession } from "@/lib/session";
     import { getBrandingSettings, getLoginSettings, getPasswordComplexitySettings } from "@/lib/zitadel";
     import { Metadata } from "next";
    @@ -17,34 +17,26 @@ export async function generateMetadata(): Promise<Metadata> {
     
     export default async function Page(props: { searchParams: Promise<Record<string | number | symbol, string | undefined>> }) {
       const _headers = await headers();
    -  const { serviceUrl } = getServiceUrlFromHeaders(_headers);
    +  const { serviceConfig } = getServiceConfig(_headers);
     
       const searchParams = await props.searchParams;
     
       const { loginName, organization, requestId } = searchParams;
     
       // also allow no session to be found (ignoreUnkownUsername)
    -  const sessionFactors = await loadMostRecentSession({
    -    serviceUrl,
    -    sessionParams: {
    +  const sessionFactors = await loadMostRecentSession({ serviceConfig, sessionParams: {
           loginName,
           organization,
         },
       });
     
    -  const branding = await getBrandingSettings({
    -    serviceUrl,
    -    organization,
    +  const branding = await getBrandingSettings({ serviceConfig, organization,
       });
     
    -  const passwordComplexity = await getPasswordComplexitySettings({
    -    serviceUrl,
    -    organization: sessionFactors?.factors?.user?.organizationId,
    +  const passwordComplexity = await getPasswordComplexitySettings({ serviceConfig, organization: sessionFactors?.factors?.user?.organizationId,
       });
     
    -  const loginSettings = await getLoginSettings({
    -    serviceUrl,
    -    organization: sessionFactors?.factors?.user?.organizationId,
    +  const loginSettings = await getLoginSettings({ serviceConfig, organization: sessionFactors?.factors?.user?.organizationId,
       });
     
       return (
    
  • apps/login/src/app/(login)/password/page.tsx+6 14 modified
    @@ -3,7 +3,7 @@ import { DynamicTheme } from "@/components/dynamic-theme";
     import { PasswordForm } from "@/components/password-form";
     import { Translated } from "@/components/translated";
     import { UserAvatar } from "@/components/user-avatar";
    -import { getServiceUrlFromHeaders } from "@/lib/service-url";
    +import { getServiceConfig } from "@/lib/service-url";
     import { loadMostRecentSession } from "@/lib/session";
     import { getBrandingSettings, getDefaultOrg, getLoginSettings } from "@/lib/zitadel";
     import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb";
    @@ -21,13 +21,11 @@ export default async function Page(props: { searchParams: Promise<Record<string
       let { loginName, organization, requestId } = searchParams;
     
       const _headers = await headers();
    -  const { serviceUrl } = getServiceUrlFromHeaders(_headers);
    +  const { serviceConfig } = getServiceConfig(_headers);
     
       let defaultOrganization;
       if (!organization) {
    -    const org: Organization | null = await getDefaultOrg({
    -      serviceUrl,
    -    });
    +    const org: Organization | null = await getDefaultOrg({ serviceConfig, });
     
         if (org) {
           defaultOrganization = org.id;
    @@ -37,9 +35,7 @@ export default async function Page(props: { searchParams: Promise<Record<string
       // also allow no session to be found (ignoreUnkownUsername)
       let sessionFactors;
       try {
    -    sessionFactors = await loadMostRecentSession({
    -      serviceUrl,
    -      sessionParams: {
    +    sessionFactors = await loadMostRecentSession({ serviceConfig, sessionParams: {
             loginName,
             organization,
           },
    @@ -49,13 +45,9 @@ export default async function Page(props: { searchParams: Promise<Record<string
         console.warn(error);
       }
     
    -  const branding = await getBrandingSettings({
    -    serviceUrl,
    -    organization: organization ?? defaultOrganization,
    +  const branding = await getBrandingSettings({ serviceConfig, organization: organization ?? defaultOrganization,
       });
    -  const loginSettings = await getLoginSettings({
    -    serviceUrl,
    -    organization: organization ?? defaultOrganization,
    +  const loginSettings = await getLoginSettings({ serviceConfig, organization: organization ?? defaultOrganization,
       });
     
       return (
    
  • apps/login/src/app/(login)/password/set/page.tsx+7 17 modified
    @@ -3,7 +3,7 @@ import { DynamicTheme } from "@/components/dynamic-theme";
     import { SetPasswordForm } from "@/components/set-password-form";
     import { Translated } from "@/components/translated";
     import { UserAvatar } from "@/components/user-avatar";
    -import { getServiceUrlFromHeaders } from "@/lib/service-url";
    +import { getServiceConfig } from "@/lib/service-url";
     import { loadMostRecentSession } from "@/lib/session";
     import { getBrandingSettings, getLoginSettings, getPasswordComplexitySettings, getUserByID } from "@/lib/zitadel";
     import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
    @@ -23,41 +23,31 @@ export default async function Page(props: { searchParams: Promise<Record<string
       const { userId, loginName, organization, requestId, code, initial } = searchParams;
     
       const _headers = await headers();
    -  const { serviceUrl } = getServiceUrlFromHeaders(_headers);
    +  const { serviceConfig } = getServiceConfig(_headers);
     
       // also allow no session to be found (ignoreUnkownUsername)
       let session: Session | undefined;
       if (loginName) {
    -    session = await loadMostRecentSession({
    -      serviceUrl,
    -      sessionParams: {
    +    session = await loadMostRecentSession({ serviceConfig, sessionParams: {
             loginName,
             organization,
           },
         });
       }
     
    -  const branding = await getBrandingSettings({
    -    serviceUrl,
    -    organization,
    +  const branding = await getBrandingSettings({ serviceConfig, organization,
       });
     
    -  const passwordComplexity = await getPasswordComplexitySettings({
    -    serviceUrl,
    -    organization: session?.factors?.user?.organizationId,
    +  const passwordComplexity = await getPasswordComplexitySettings({ serviceConfig, organization: session?.factors?.user?.organizationId,
       });
     
    -  const loginSettings = await getLoginSettings({
    -    serviceUrl,
    -    organization,
    +  const loginSettings = await getLoginSettings({ serviceConfig, organization,
       });
     
       let user: User | undefined;
       let displayName: string | undefined;
       if (userId) {
    -    const userResponse = await getUserByID({
    -      serviceUrl,
    -      userId,
    +    const userResponse = await getUserByID({ serviceConfig, userId,
         });
         user = userResponse.user;
     
    
  • apps/login/src/app/(login)/register/page.tsx+8 20 modified
    @@ -3,7 +3,7 @@ import { DynamicTheme } from "@/components/dynamic-theme";
     import { RegisterForm } from "@/components/register-form";
     import { SignInWithIdp } from "@/components/sign-in-with-idp";
     import { Translated } from "@/components/translated";
    -import { getServiceUrlFromHeaders } from "@/lib/service-url";
    +import { getServiceConfig } from "@/lib/service-url";
     import {
       getActiveIdentityProviders,
       getBrandingSettings,
    @@ -29,39 +29,27 @@ export default async function Page(props: { searchParams: Promise<Record<string
       let { firstname, lastname, email, organization, requestId } = searchParams;
     
       const _headers = await headers();
    -  const { serviceUrl } = getServiceUrlFromHeaders(_headers);
    +  const { serviceConfig } = getServiceConfig(_headers);
     
       if (!organization) {
    -    const org: Organization | null = await getDefaultOrg({
    -      serviceUrl,
    -    });
    +    const org: Organization | null = await getDefaultOrg({ serviceConfig, });
         if (org) {
           organization = org.id;
         }
       }
     
    -  const legal = await getLegalAndSupportSettings({
    -    serviceUrl,
    -    organization,
    +  const legal = await getLegalAndSupportSettings({ serviceConfig, organization,
       });
    -  const passwordComplexitySettings = await getPasswordComplexitySettings({
    -    serviceUrl,
    -    organization,
    +  const passwordComplexitySettings = await getPasswordComplexitySettings({ serviceConfig, organization,
       });
     
    -  const branding = await getBrandingSettings({
    -    serviceUrl,
    -    organization,
    +  const branding = await getBrandingSettings({ serviceConfig, organization,
       });
     
    -  const loginSettings = await getLoginSettings({
    -    serviceUrl,
    -    organization,
    +  const loginSettings = await getLoginSettings({ serviceConfig, organization,
       });
     
    -  const identityProviders = await getActiveIdentityProviders({
    -    serviceUrl,
    -    orgId: organization,
    +  const identityProviders = await getActiveIdentityProviders({ serviceConfig, orgId: organization,
       }).then((resp) => {
         return resp.identityProviders.filter((idp) => {
           return idp.options?.isAutoCreation || idp.options?.isCreationAllowed; // check if IDP allows to create account automatically or manual creation is allowed
    
  • apps/login/src/app/(login)/register/password/page.tsx+7 17 modified
    @@ -1,7 +1,7 @@
     import { DynamicTheme } from "@/components/dynamic-theme";
     import { SetRegisterPasswordForm } from "@/components/set-register-password-form";
     import { Translated } from "@/components/translated";
    -import { getServiceUrlFromHeaders } from "@/lib/service-url";
    +import { getServiceConfig } from "@/lib/service-url";
     import {
       getBrandingSettings,
       getDefaultOrg,
    @@ -18,36 +18,26 @@ export default async function Page(props: { searchParams: Promise<Record<string
       let { firstname, lastname, email, organization, requestId } = searchParams;
     
       const _headers = await headers();
    -  const { serviceUrl } = getServiceUrlFromHeaders(_headers);
    +  const { serviceConfig } = getServiceConfig(_headers);
     
       if (!organization) {
    -    const org: Organization | null = await getDefaultOrg({
    -      serviceUrl,
    -    });
    +    const org: Organization | null = await getDefaultOrg({ serviceConfig, });
         if (org) {
           organization = org.id;
         }
       }
     
       const missingData = !firstname || !lastname || !email || !organization;
     
    -  const legal = await getLegalAndSupportSettings({
    -    serviceUrl,
    -    organization,
    +  const legal = await getLegalAndSupportSettings({ serviceConfig, organization,
       });
    -  const passwordComplexitySettings = await getPasswordComplexitySettings({
    -    serviceUrl,
    -    organization,
    +  const passwordComplexitySettings = await getPasswordComplexitySettings({ serviceConfig, organization,
       });
     
    -  const branding = await getBrandingSettings({
    -    serviceUrl,
    -    organization,
    +  const branding = await getBrandingSettings({ serviceConfig, organization,
       });
     
    -  const loginSettings = await getLoginSettings({
    -    serviceUrl,
    -    organization,
    +  const loginSettings = await getLoginSettings({ serviceConfig, organization,
       });
     
       return missingData ? (
    
  • apps/login/src/app/login/route.ts+7 16 modified
    @@ -1,8 +1,8 @@
     import { getAllSessions } from "@/lib/cookies";
    -import { getServiceUrlFromHeaders } from "@/lib/service-url";
    +import { getServiceConfig } from "@/lib/service-url";
     import { validateAuthRequest, isRSCRequest } from "@/lib/auth-utils";
     import { handleOIDCFlowInitiation, handleSAMLFlowInitiation, FlowInitiationParams } from "@/lib/server/flow-initiation";
    -import { listSessions } from "@/lib/zitadel";
    +import { listSessions, ServiceConfig } from "@/lib/zitadel";
     import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
     import { headers } from "next/headers";
     import { NextRequest, NextResponse } from "next/server";
    @@ -13,18 +13,15 @@ export const fetchCache = "default-no-store";
     // Add this to prevent RSC requests
     export const runtime = "nodejs";
     
    -async function loadSessions({ serviceUrl, ids }: { serviceUrl: string; ids: string[] }): Promise<Session[]> {
    -  const response = await listSessions({
    -    serviceUrl,
    -    ids: ids.filter((id: string | undefined) => !!id),
    -  });
    +async function loadSessions({ serviceConfig, ids }: { serviceConfig: ServiceConfig; ids: string[] }): Promise<Session[]> {
    +  const response = await listSessions({ serviceConfig, ids: ids.filter((id: string | undefined) => !!id) });
     
       return response?.sessions ?? [];
     }
     
     export async function GET(request: NextRequest) {
       const _headers = await headers();
    -  const { serviceUrl } = getServiceUrlFromHeaders(_headers);
    +  const { serviceConfig } = getServiceConfig(_headers);
     
       const searchParams = request.nextUrl.searchParams;
     
    @@ -43,17 +40,11 @@ export async function GET(request: NextRequest) {
       const ids = sessionCookies.map((s) => s.id);
       let sessions: Session[] = [];
       if (ids && ids.length) {
    -    sessions = await loadSessions({ serviceUrl, ids });
    +    sessions = await loadSessions({ serviceConfig, ids });
       }
     
       // Flow initiation - delegate to appropriate handler
    -  const flowParams: FlowInitiationParams = {
    -    serviceUrl,
    -    requestId,
    -    sessions,
    -    sessionCookies,
    -    request,
    -  };
    +  const flowParams: FlowInitiationParams = { serviceConfig, requestId, sessions, sessionCookies, request };
     
       if (requestId.startsWith("oidc_")) {
         return handleOIDCFlowInitiation(flowParams);
    
  • apps/login/src/app/(login)/signedin/page.tsx+9 17 modified
    @@ -5,9 +5,9 @@ import { Translated } from "@/components/translated";
     import { UserAvatar } from "@/components/user-avatar";
     import { getMostRecentCookieWithLoginname, getSessionCookieById } from "@/lib/cookies";
     import { completeDeviceAuthorization } from "@/lib/server/device";
    -import { getServiceUrlFromHeaders } from "@/lib/service-url";
    +import { getServiceConfig } from "@/lib/service-url";
     import { loadMostRecentSession } from "@/lib/session";
    -import { getBrandingSettings, getLoginSettings, getSession } from "@/lib/zitadel";
    +import { getBrandingSettings, getLoginSettings, getSession, ServiceConfig } from "@/lib/zitadel";
     import { Metadata } from "next";
     import { getTranslations } from "next-intl/server";
     import { headers } from "next/headers";
    @@ -18,11 +18,9 @@ export async function generateMetadata(): Promise<Metadata> {
       return { title: t("title", { user: "" }) };
     }
     
    -async function loadSessionById(serviceUrl: string, sessionId: string, organization?: string) {
    +async function loadSessionById(serviceConfig: ServiceConfig, sessionId: string, organization?: string) {
       const recent = await getSessionCookieById({ sessionId, organization });
    -  return getSession({
    -    serviceUrl,
    -    sessionId: recent.id,
    +  return getSession({ serviceConfig, sessionId: recent.id,
         sessionToken: recent.token,
       }).then((response) => {
         if (response?.session) {
    @@ -35,13 +33,11 @@ export default async function Page(props: { searchParams: Promise<any> }) {
       const searchParams = await props.searchParams;
     
       const _headers = await headers();
    -  const { serviceUrl } = getServiceUrlFromHeaders(_headers);
    +  const { serviceConfig } = getServiceConfig(_headers);
     
       const { loginName, requestId, organization, sessionId } = searchParams;
     
    -  const branding = await getBrandingSettings({
    -    serviceUrl,
    -    organization,
    +  const branding = await getBrandingSettings({ serviceConfig, organization,
       });
     
       // complete device authorization flow if device requestId is present
    @@ -75,17 +71,13 @@ export default async function Page(props: { searchParams: Promise<any> }) {
       }
     
       const sessionFactors = sessionId
    -    ? await loadSessionById(serviceUrl, sessionId, organization)
    -    : await loadMostRecentSession({
    -        serviceUrl,
    -        sessionParams: { loginName, organization },
    +    ? await loadSessionById(serviceConfig, sessionId, organization)
    +    : await loadMostRecentSession({ serviceConfig, sessionParams: { loginName, organization },
           });
     
       let loginSettings;
       if (!requestId) {
    -    loginSettings = await getLoginSettings({
    -      serviceUrl,
    -      organization,
    +    loginSettings = await getLoginSettings({ serviceConfig, organization,
         });
       }
     
    
  • apps/login/src/app/(login)/u2f/page.tsx+5 11 modified
    @@ -4,7 +4,7 @@ import { LoginPasskey } from "@/components/login-passkey";
     import { Translated } from "@/components/translated";
     import { UserAvatar } from "@/components/user-avatar";
     import { getSessionCookieById } from "@/lib/cookies";
    -import { getServiceUrlFromHeaders } from "@/lib/service-url";
    +import { getServiceConfig } from "@/lib/service-url";
     import { loadMostRecentSession } from "@/lib/session";
     import { getBrandingSettings, getSession } from "@/lib/zitadel";
     import { Metadata } from "next";
    @@ -22,25 +22,19 @@ export default async function Page(props: { searchParams: Promise<Record<string
       const { loginName, requestId, sessionId, organization } = searchParams;
     
       const _headers = await headers();
    -  const { serviceUrl } = getServiceUrlFromHeaders(_headers);
    +  const { serviceConfig } = getServiceConfig(_headers);
     
    -  const branding = await getBrandingSettings({
    -    serviceUrl,
    -    organization,
    +  const branding = await getBrandingSettings({ serviceConfig, organization,
       });
     
       const sessionFactors = sessionId
         ? await loadSessionById(sessionId, organization)
    -    : await loadMostRecentSession({
    -        serviceUrl,
    -        sessionParams: { loginName, organization },
    +    : await loadMostRecentSession({ serviceConfig, sessionParams: { loginName, organization },
           });
     
       async function loadSessionById(sessionId: string, organization?: string) {
         const recent = await getSessionCookieById({ sessionId, organization });
    -    return getSession({
    -      serviceUrl,
    -      sessionId: recent.id,
    +    return getSession({ serviceConfig, sessionId: recent.id,
           sessionToken: recent.token,
         }).then((response) => {
           if (response?.session) {
    
  • apps/login/src/app/(login)/u2f/set/page.tsx+4 8 modified
    @@ -3,7 +3,7 @@ import { DynamicTheme } from "@/components/dynamic-theme";
     import { RegisterU2f } from "@/components/register-u2f";
     import { Translated } from "@/components/translated";
     import { UserAvatar } from "@/components/user-avatar";
    -import { getServiceUrlFromHeaders } from "@/lib/service-url";
    +import { getServiceConfig } from "@/lib/service-url";
     import { loadMostRecentSession } from "@/lib/session";
     import { getBrandingSettings } from "@/lib/zitadel";
     import { Metadata } from "next";
    @@ -21,19 +21,15 @@ export default async function Page(props: { searchParams: Promise<Record<string
       const { loginName, organization, requestId, checkAfter } = searchParams;
     
       const _headers = await headers();
    -  const { serviceUrl } = getServiceUrlFromHeaders(_headers);
    +  const { serviceConfig } = getServiceConfig(_headers);
     
    -  const sessionFactors = await loadMostRecentSession({
    -    serviceUrl,
    -    sessionParams: {
    +  const sessionFactors = await loadMostRecentSession({ serviceConfig, sessionParams: {
           loginName,
           organization,
         },
       });
     
    -  const branding = await getBrandingSettings({
    -    serviceUrl,
    -    organization,
    +  const branding = await getBrandingSettings({ serviceConfig, organization,
       });
     
       return (
    
  • apps/login/src/app/(login)/verify/page.tsx+7 13 modified
    @@ -3,9 +3,9 @@ import { DynamicTheme } from "@/components/dynamic-theme";
     import { Translated } from "@/components/translated";
     import { UserAvatar } from "@/components/user-avatar";
     import { VerifyForm } from "@/components/verify-form";
    +import { getPublicHostWithProtocol } from "@/lib/server/host";
     import { sendEmailCode, sendInviteEmailCode } from "@/lib/server/verify";
    -import { getOriginalHostWithProtocol } from "@/lib/server/host";
    -import { getServiceUrlFromHeaders } from "@/lib/service-url";
    +import { getServiceConfig } from "@/lib/service-url";
     import { loadMostRecentSession } from "@/lib/session";
     import { getBrandingSettings, getUserByID } from "@/lib/zitadel";
     import { HumanUser, User } from "@zitadel/proto/zitadel/user/v2/user_pb";
    @@ -24,12 +24,9 @@ export default async function Page(props: { searchParams: Promise<any> }) {
       const { userId, loginName, code, organization, requestId, invite, send } = searchParams;
     
       const _headers = await headers();
    -  const { serviceUrl } = getServiceUrlFromHeaders(_headers);
    +  const { serviceConfig } = getServiceConfig(_headers);
     
    -  const branding = await getBrandingSettings({
    -    serviceUrl,
    -    organization,
    -  });
    +  const branding = await getBrandingSettings({ serviceConfig, organization });
     
       let sessionFactors;
       let user: User | undefined;
    @@ -43,7 +40,7 @@ export default async function Page(props: { searchParams: Promise<any> }) {
       const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? "";
     
       async function sendEmail(userId: string) {
    -    const hostWithProtocol = await getOriginalHostWithProtocol();
    +    const hostWithProtocol = await getPublicHostWithProtocol(_headers);
     
         if (invite === "true") {
           await sendInviteEmailCode({
    @@ -70,7 +67,7 @@ export default async function Page(props: { searchParams: Promise<any> }) {
     
       if ("loginName" in searchParams) {
         sessionFactors = await loadMostRecentSession({
    -      serviceUrl,
    +      serviceConfig,
           sessionParams: {
             loginName,
             organization,
    @@ -85,10 +82,7 @@ export default async function Page(props: { searchParams: Promise<any> }) {
           await sendEmail(userId);
         }
     
    -    const userResponse = await getUserByID({
    -      serviceUrl,
    -      userId,
    -    });
    +    const userResponse = await getUserByID({ serviceConfig, userId });
         if (userResponse) {
           user = userResponse.user;
           if (user?.type.case === "human") {
    
  • apps/login/src/app/(login)/verify/success/page.tsx+5 11 modified
    @@ -1,7 +1,7 @@
     import { DynamicTheme } from "@/components/dynamic-theme";
     import { Translated } from "@/components/translated";
     import { UserAvatar } from "@/components/user-avatar";
    -import { getServiceUrlFromHeaders } from "@/lib/service-url";
    +import { getServiceConfig } from "@/lib/service-url";
     import { loadMostRecentSession } from "@/lib/session";
     import { getBrandingSettings, getUserByID } from "@/lib/zitadel";
     import { HumanUser, User } from "@zitadel/proto/zitadel/user/v2/user_pb";
    @@ -11,18 +11,14 @@ export default async function Page(props: { searchParams: Promise<any> }) {
       const searchParams = await props.searchParams;
     
       const _headers = await headers();
    -  const { serviceUrl } = getServiceUrlFromHeaders(_headers);
    +  const { serviceConfig } = getServiceConfig(_headers);
     
       const { loginName, organization, userId } = searchParams;
     
    -  const branding = await getBrandingSettings({
    -    serviceUrl,
    -    organization,
    +  const branding = await getBrandingSettings({ serviceConfig, organization,
       });
     
    -  const sessionFactors = await loadMostRecentSession({
    -    serviceUrl,
    -    sessionParams: { loginName, organization },
    +  const sessionFactors = await loadMostRecentSession({ serviceConfig, sessionParams: { loginName, organization },
       }).catch((error) => {
         console.warn("Error loading session:", error);
       });
    @@ -33,9 +29,7 @@ export default async function Page(props: { searchParams: Promise<any> }) {
         throw Error("Failed to get user id");
       }
     
    -  const userResponse = await getUserByID({
    -    serviceUrl,
    -    userId: id,
    +  const userResponse = await getUserByID({ serviceConfig, userId: id,
       });
     
       let user: User | undefined;
    
  • apps/login/src/app/security/route.ts+3 3 modified
    @@ -1,16 +1,16 @@
     import { createServiceForHost } from "@/lib/service";
    -import { getServiceUrlFromHeaders } from "@/lib/service-url";
    +import { getServiceConfig } from "@/lib/service-url";
     import { Client } from "@zitadel/client";
     import { SettingsService } from "@zitadel/proto/zitadel/settings/v2/settings_service_pb";
     import { headers } from "next/headers";
     import { NextResponse } from "next/server";
     
     export async function GET() {
       const _headers = await headers();
    -  const { serviceUrl } = getServiceUrlFromHeaders(_headers);
    +  const { serviceConfig } = getServiceConfig(_headers);
     
       const settingsService: Client<typeof SettingsService> =
    -    await createServiceForHost(SettingsService, serviceUrl);
    +    await createServiceForHost(SettingsService, serviceConfig);
     
       const settings = await settingsService
         .getSecuritySettings({})
    
  • apps/login/src/i18n/request.ts+3 5 modified
    @@ -1,5 +1,5 @@
     import { LANGS, LANGUAGE_COOKIE_NAME, LANGUAGE_HEADER_NAME } from "@/lib/i18n";
    -import { getServiceUrlFromHeaders } from "@/lib/service-url";
    +import { getServiceConfig } from "@/lib/service-url";
     import { getHostedLoginTranslation } from "@/lib/zitadel";
     import { JsonObject } from "@zitadel/client";
     import deepmerge from "deepmerge";
    @@ -13,7 +13,7 @@ export default getRequestConfig(async () => {
       let locale: string = fallback;
     
       const _headers = await headers();
    -  const { serviceUrl } = getServiceUrlFromHeaders(_headers);
    +  const { serviceConfig } = getServiceConfig(_headers);
     
       const languageHeader = await (await headers()).get(LANGUAGE_HEADER_NAME);
       if (languageHeader) {
    @@ -34,9 +34,7 @@ export default getRequestConfig(async () => {
     
       let translations: JsonObject | {} = {};
       try {
    -    const i18nJSON = await getHostedLoginTranslation({
    -      serviceUrl,
    -      locale,
    +    const i18nJSON = await getHostedLoginTranslation({ serviceConfig, locale,
           organization: i18nOrganization,
         });
     
    
  • apps/login/src/lib/deployment.test.ts+77 0 added
    @@ -0,0 +1,77 @@
    +import { describe, expect, test, beforeEach, afterEach } from "vitest";
    +import { hasSystemUserCredentials, hasServiceUserToken } from "./deployment";
    +
    +describe("Deployment utilities", () => {
    +  const originalEnv = process.env;
    +
    +  beforeEach(() => {
    +    // Reset environment before each test
    +    process.env = { ...originalEnv };
    +  });
    +
    +  afterEach(() => {
    +    process.env = originalEnv;
    +  });
    +
    +  describe("hasSystemUserCredentials", () => {
    +    test("should return true when all system user credentials are present", () => {
    +      process.env.AUDIENCE = "https://api.zitadel.cloud";
    +      process.env.SYSTEM_USER_ID = "12345";
    +      process.env.SYSTEM_USER_PRIVATE_KEY = "-----BEGIN PRIVATE KEY-----\n...";
    +
    +      expect(hasSystemUserCredentials()).toBe(true);
    +    });
    +
    +    test("should return false when AUDIENCE is missing", () => {
    +      process.env.AUDIENCE = undefined as any;
    +      process.env.SYSTEM_USER_ID = "12345";
    +      process.env.SYSTEM_USER_PRIVATE_KEY = "-----BEGIN PRIVATE KEY-----\n...";
    +
    +      expect(hasSystemUserCredentials()).toBe(false);
    +    });
    +
    +    test("should return false when SYSTEM_USER_ID is missing", () => {
    +      process.env.AUDIENCE = "https://api.zitadel.cloud";
    +      process.env.SYSTEM_USER_ID = undefined as any;
    +      process.env.SYSTEM_USER_PRIVATE_KEY = "-----BEGIN PRIVATE KEY-----\n...";
    +
    +      expect(hasSystemUserCredentials()).toBe(false);
    +    });
    +
    +    test("should return false when SYSTEM_USER_PRIVATE_KEY is missing", () => {
    +      process.env.AUDIENCE = "https://api.zitadel.cloud";
    +      process.env.SYSTEM_USER_ID = "12345";
    +      process.env.SYSTEM_USER_PRIVATE_KEY = undefined as any;
    +
    +      expect(hasSystemUserCredentials()).toBe(false);
    +    });
    +
    +    test("should return false when all credentials are missing", () => {
    +      process.env.AUDIENCE = undefined as any;
    +      process.env.SYSTEM_USER_ID = undefined as any;
    +      process.env.SYSTEM_USER_PRIVATE_KEY = undefined as any;
    +
    +      expect(hasSystemUserCredentials()).toBe(false);
    +    });
    +  });
    +
    +  describe("hasServiceUserToken", () => {
    +    test("should return true when ZITADEL_SERVICE_USER_TOKEN is present", () => {
    +      process.env.ZITADEL_SERVICE_USER_TOKEN = "token123";
    +
    +      expect(hasServiceUserToken()).toBe(true);
    +    });
    +
    +    test("should return false when ZITADEL_SERVICE_USER_TOKEN is not set", () => {
    +      process.env.ZITADEL_SERVICE_USER_TOKEN = undefined as any;
    +
    +      expect(hasServiceUserToken()).toBe(false);
    +    });
    +
    +    test("should return false when ZITADEL_SERVICE_USER_TOKEN is empty string", () => {
    +      process.env.ZITADEL_SERVICE_USER_TOKEN = "";
    +
    +      expect(hasServiceUserToken()).toBe(false);
    +    });
    +  });
    +});
    
  • apps/login/src/lib/deployment.ts+24 0 added
    @@ -0,0 +1,24 @@
    +/**
    + * Checks if system user credentials are available for JWT authentication.
    + *
    + * System user authentication requires:
    + * - AUDIENCE: The API audience for JWT authentication
    + * - SYSTEM_USER_ID: The system user's ID
    + * - SYSTEM_USER_PRIVATE_KEY: The private key for JWT signing
    + *
    + * Both multi-tenant and self-hosted deployments can use system user authentication.
    + *
    + * @returns true if system user credentials are present, false otherwise
    + */
    +export function hasSystemUserCredentials(): boolean {
    +  return !!process.env.AUDIENCE && !!process.env.SYSTEM_USER_ID && !!process.env.SYSTEM_USER_PRIVATE_KEY;
    +}
    +
    +/**
    + * Checks if service user token is available for authentication.
    + *
    + * @returns true if ZITADEL_SERVICE_USER_TOKEN is present, false otherwise
    + */
    +export function hasServiceUserToken(): boolean {
    +  return !!process.env.ZITADEL_SERVICE_USER_TOKEN;
    +}
    
  • apps/login/src/lib/oidc.ts+6 20 modified
    @@ -1,36 +1,27 @@
     import { Cookie } from "@/lib/cookies";
     import { sendLoginname, SendLoginnameCommand } from "@/lib/server/loginname";
    -import { createCallback, getLoginSettings } from "@/lib/zitadel";
    +import { createCallback, getLoginSettings, ServiceConfig } from "@/lib/zitadel";
     import { create } from "@zitadel/client";
     import { CreateCallbackRequestSchema, SessionSchema } from "@zitadel/proto/zitadel/oidc/v2/oidc_service_pb";
     import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
     import { isSessionValid } from "./session";
     
     type LoginWithOIDCAndSession = {
    -  serviceUrl: string;
    +  serviceConfig: ServiceConfig;
       authRequest: string;
       sessionId: string;
       sessions: Session[];
       sessionCookies: Cookie[];
     };
    -export async function loginWithOIDCAndSession({
    -  serviceUrl,
    -  authRequest,
    -  sessionId,
    -  sessions,
    -  sessionCookies,
    -}: LoginWithOIDCAndSession): Promise<{ error: string } | { redirect: string }> {
    +export async function loginWithOIDCAndSession({ serviceConfig, authRequest, sessionId, sessions, sessionCookies }: LoginWithOIDCAndSession): Promise<{ error: string } | { redirect: string }> {
       console.log(`Login with session: ${sessionId} and authRequest: ${authRequest}`);
     
       const selectedSession = sessions.find((s) => s.id === sessionId);
     
       if (selectedSession && selectedSession.id) {
         console.log(`Found session ${selectedSession.id}`);
     
    -    const isValid = await isSessionValid({
    -      serviceUrl,
    -      session: selectedSession,
    -    });
    +    const isValid = await isSessionValid({ serviceConfig, session: selectedSession });
     
         console.log("Session is valid:", isValid);
     
    @@ -61,9 +52,7 @@ export async function loginWithOIDCAndSession({
           };
     
           try {
    -        const { callbackUrl } = await createCallback({
    -          serviceUrl,
    -          req: create(CreateCallbackRequestSchema, {
    +        const { callbackUrl } = await createCallback({ serviceConfig, req: create(CreateCallbackRequestSchema, {
                 authRequestId: authRequest,
                 callbackKind: {
                   case: "session",
    @@ -81,10 +70,7 @@ export async function loginWithOIDCAndSession({
             // handle already handled gracefully as these could come up if old emails with requestId are used (reset password, register emails etc.)
             console.error(error);
             if (error && typeof error === "object" && "code" in error && error?.code === 9) {
    -          const loginSettings = await getLoginSettings({
    -            serviceUrl,
    -            organization: selectedSession.factors?.user?.organizationId,
    -          });
    +          const loginSettings = await getLoginSettings({ serviceConfig, organization: selectedSession.factors?.user?.organizationId });
     
               if (loginSettings?.defaultRedirectUri) {
                 return { redirect: loginSettings.defaultRedirectUri };
    
  • apps/login/src/lib/saml.ts+6 20 modified
    @@ -1,6 +1,6 @@
     import { Cookie } from "@/lib/cookies";
     import { sendLoginname, SendLoginnameCommand } from "@/lib/server/loginname";
    -import { createResponse, getLoginSettings } from "@/lib/zitadel";
    +import { createResponse, getLoginSettings, ServiceConfig } from "@/lib/zitadel";
     import { create } from "@zitadel/client";
     import { CreateResponseRequestSchema } from "@zitadel/proto/zitadel/saml/v2/saml_service_pb";
     import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
    @@ -9,7 +9,7 @@ import { v4 as uuidv4 } from "uuid";
     import { isSessionValid } from "./session";
     
     type LoginWithSAMLAndSession = {
    -  serviceUrl: string;
    +  serviceConfig: ServiceConfig;
       samlRequest: string;
       sessionId: string;
       sessions: Session[];
    @@ -82,24 +82,15 @@ export async function getSAMLFormCookie(uid: string): Promise<string | null> {
       }
     }
     
    -export async function loginWithSAMLAndSession({
    -  serviceUrl,
    -  samlRequest,
    -  sessionId,
    -  sessions,
    -  sessionCookies,
    -}: LoginWithSAMLAndSession): Promise<{ error: string } | { redirect: string }> {
    +export async function loginWithSAMLAndSession({ serviceConfig, samlRequest, sessionId, sessions, sessionCookies }: LoginWithSAMLAndSession): Promise<{ error: string } | { redirect: string }> {
       console.log(`Login with session: ${sessionId} and samlRequest: ${samlRequest}`);
     
       const selectedSession = sessions.find((s) => s.id === sessionId);
     
       if (selectedSession && selectedSession.id) {
         console.log(`Found session ${selectedSession.id}`);
     
    -    const isValid = await isSessionValid({
    -      serviceUrl,
    -      session: selectedSession,
    -    });
    +    const isValid = await isSessionValid({ serviceConfig, session: selectedSession });
     
         console.log("Session is valid:", isValid);
     
    @@ -129,9 +120,7 @@ export async function loginWithSAMLAndSession({
     
           // works not with _rsc request
           try {
    -        const { url } = await createResponse({
    -          serviceUrl,
    -          req: create(CreateResponseRequestSchema, {
    +        const { url } = await createResponse({ serviceConfig, req: create(CreateResponseRequestSchema, {
                 samlRequestId: samlRequest,
                 responseKind: {
                   case: "session",
    @@ -149,10 +138,7 @@ export async function loginWithSAMLAndSession({
             console.error(error);
     
             if (error && typeof error === "object" && "code" in error && error?.code === 9) {
    -          const loginSettings = await getLoginSettings({
    -            serviceUrl,
    -            organization: selectedSession.factors?.user?.organizationId,
    -          });
    +          const loginSettings = await getLoginSettings({ serviceConfig, organization: selectedSession.factors?.user?.organizationId });
     
               if (loginSettings?.defaultRedirectUri) {
                 return { redirect: loginSettings.defaultRedirectUri };
    
  • apps/login/src/lib/self.ts+8 13 modified
    @@ -3,14 +3,11 @@
     import { createUserServiceClient } from "@zitadel/client/v2";
     import { headers } from "next/headers";
     import { getSessionCookieById } from "./cookies";
    -import { getServiceUrlFromHeaders } from "./service-url";
    -import { createServerTransport, getSession } from "./zitadel";
    -
    -const myUserService = async (serviceUrl: string, sessionToken: string) => {
    -  const transportPromise = await createServerTransport(
    -    sessionToken,
    -    serviceUrl,
    -  );
    +import { getServiceConfig } from "./service-url";
    +import { createServerTransport, getSession, ServiceConfig } from "./zitadel";
    +
    +const myUserService = async (serviceConfig: ServiceConfig, sessionToken: string) => {
    +  const transportPromise = await createServerTransport(sessionToken, serviceConfig);
       return createUserServiceClient(transportPromise);
     };
     
    @@ -22,21 +19,19 @@ export async function setMyPassword({
       password: string;
     }) {
       const _headers = await headers();
    -  const { serviceUrl } = getServiceUrlFromHeaders(_headers);
    +  const { serviceConfig } = getServiceConfig(_headers);
     
       const sessionCookie = await getSessionCookieById({ sessionId });
     
    -  const { session } = await getSession({
    -    serviceUrl,
    -    sessionId: sessionCookie.id,
    +  const { session } = await getSession({ serviceConfig, sessionId: sessionCookie.id,
         sessionToken: sessionCookie.token,
       });
     
       if (!session) {
         return { error: "Could not load session" };
       }
     
    -  const service = await myUserService(serviceUrl, `${sessionCookie.token}`);
    +  const service = await myUserService(serviceConfig, `${sessionCookie.token}`);
     
       if (!session?.factors?.user?.id) {
         return { error: "No user id found in session" };
    
  • apps/login/src/lib/server/auth-flow.ts+8 14 modified
    @@ -3,8 +3,8 @@
     import { getAllSessions } from "@/lib/cookies";
     import { loginWithOIDCAndSession } from "@/lib/oidc";
     import { loginWithSAMLAndSession } from "@/lib/saml";
    -import { getServiceUrlFromHeaders } from "@/lib/service-url";
    -import { listSessions } from "@/lib/zitadel";
    +import { getServiceConfig } from "@/lib/service-url";
    +import { listSessions, ServiceConfig } from "@/lib/zitadel";
     import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
     import { headers } from "next/headers";
     
    @@ -14,10 +14,8 @@ export interface AuthFlowParams {
       organization?: string;
     }
     
    -async function loadSessions({ serviceUrl, ids }: { serviceUrl: string; ids: string[] }): Promise<Session[]> {
    -  const response = await listSessions({
    -    serviceUrl,
    -    ids: ids.filter((id: string | undefined) => !!id),
    +async function loadSessions({ serviceConfig, ids }: { serviceConfig: ServiceConfig; ids: string[] }): Promise<Session[]> {
    +  const response = await listSessions({ serviceConfig, ids: ids.filter((id: string | undefined) => !!id),
       });
     
       return response?.sessions ?? [];
    @@ -33,21 +31,19 @@ export async function completeAuthFlow(command: AuthFlowParams): Promise<{ error
       const { sessionId, requestId } = command;
     
       const _headers = await headers();
    -  const { serviceUrl } = getServiceUrlFromHeaders(_headers);
    +  const { serviceConfig } = getServiceConfig(_headers);
     
       const sessionCookies = await getAllSessions();
       const ids = sessionCookies.map((s) => s.id);
       let sessions: Session[] = [];
     
       if (ids && ids.length) {
    -    sessions = await loadSessions({ serviceUrl, ids });
    +    sessions = await loadSessions({ serviceConfig, ids });
       }
     
       if (requestId.startsWith("oidc_")) {
         // Complete OIDC flow
    -    const result = await loginWithOIDCAndSession({
    -      serviceUrl,
    -      authRequest: requestId.replace("oidc_", ""),
    +    const result = await loginWithOIDCAndSession({ serviceConfig, authRequest: requestId.replace("oidc_", ""),
           sessionId,
           sessions,
           sessionCookies,
    @@ -62,9 +58,7 @@ export async function completeAuthFlow(command: AuthFlowParams): Promise<{ error
         return result;
       } else if (requestId.startsWith("saml_")) {
         // Complete SAML flow
    -    const result = await loginWithSAMLAndSession({
    -      serviceUrl,
    -      samlRequest: requestId.replace("saml_", ""),
    +    const result = await loginWithSAMLAndSession({ serviceConfig, samlRequest: requestId.replace("saml_", ""),
           sessionId,
           sessions,
           sessionCookies,
    
  • apps/login/src/lib/server/cookie.ts+13 25 modified
    @@ -14,7 +14,7 @@ import { Challenges, RequestChallenges } from "@zitadel/proto/zitadel/session/v2
     import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
     import { Checks } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
     import { headers } from "next/headers";
    -import { getServiceUrlFromHeaders } from "../service-url";
    +import { getServiceConfig } from "../service-url";
     
     type CustomCookieData = {
       id: string;
    @@ -46,7 +46,7 @@ export async function createSessionAndUpdateCookie(command: {
       lifetime?: Duration;
     }): Promise<Session> {
       const _headers = await headers();
    -  const { serviceUrl } = getServiceUrlFromHeaders(_headers);
    +  const { serviceConfig } = getServiceConfig(_headers);
     
       let sessionLifetime = command.lifetime;
     
    @@ -59,16 +59,12 @@ export async function createSessionAndUpdateCookie(command: {
         } as Duration; // for usecases where the lifetime is not specified (user discovery)
       }
     
    -  const createdSession = await createSessionFromChecks({
    -    serviceUrl,
    -    checks: command.checks,
    +  const createdSession = await createSessionFromChecks({ serviceConfig, checks: command.checks,
         lifetime: sessionLifetime,
       });
     
       if (createdSession) {
    -    return getSession({
    -      serviceUrl,
    -      sessionId: createdSession.sessionId,
    +    return getSession({ serviceConfig, sessionId: createdSession.sessionId,
           sessionToken: createdSession.sessionToken,
         }).then(async (response) => {
           if (response?.session && response.session?.factors?.user?.loginName) {
    @@ -89,7 +85,7 @@ export async function createSessionAndUpdateCookie(command: {
               sessionCookie.organization = response.session.factors.user.organizationId;
             }
     
    -        const securitySettings = await getSecuritySettings({ serviceUrl });
    +        const securitySettings = await getSecuritySettings({ serviceConfig });
             const iFrameEnabled = !!securitySettings?.embeddedIframe?.enabled;
     
             await addSessionToCookie({ session: sessionCookie, iFrameEnabled });
    @@ -119,7 +115,7 @@ export async function createSessionForIdpAndUpdateCookie({
       lifetime?: Duration;
     }): Promise<Session> {
       const _headers = await headers();
    -  const { serviceUrl } = getServiceUrlFromHeaders(_headers);
    +  const { serviceConfig } = getServiceConfig(_headers);
     
       let sessionLifetime = lifetime;
     
    @@ -132,9 +128,7 @@ export async function createSessionForIdpAndUpdateCookie({
         } as Duration;
       }
     
    -  const createdSession = await createSessionForUserIdAndIdpIntent({
    -    serviceUrl,
    -    userId,
    +  const createdSession = await createSessionForUserIdAndIdpIntent({ serviceConfig, userId,
         idpIntent,
         lifetime: sessionLifetime,
       }).catch((error: ErrorDetail | CredentialsCheckError) => {
    @@ -152,9 +146,7 @@ export async function createSessionForIdpAndUpdateCookie({
         throw "Could not create session";
       }
     
    -  const { session } = await getSession({
    -    serviceUrl,
    -    sessionId: createdSession.sessionId,
    +  const { session } = await getSession({ serviceConfig, sessionId: createdSession.sessionId,
         sessionToken: createdSession.sessionToken,
       });
     
    @@ -180,7 +172,7 @@ export async function createSessionForIdpAndUpdateCookie({
         sessionCookie.organization = session.factors.user.organizationId;
       }
     
    -  const securitySettings = await getSecuritySettings({ serviceUrl });
    +  const securitySettings = await getSecuritySettings({ serviceConfig });
       const iFrameEnabled = !!securitySettings?.embeddedIframe?.enabled;
     
       return addSessionToCookie({ session: sessionCookie, iFrameEnabled }).then(() => {
    @@ -200,11 +192,9 @@ export async function setSessionAndUpdateCookie(command: {
       lifetime: Duration;
     }) {
       const _headers = await headers();
    -  const { serviceUrl } = getServiceUrlFromHeaders(_headers);
    +  const { serviceConfig } = getServiceConfig(_headers);
     
    -  return setSession({
    -    serviceUrl,
    -    sessionId: command.recentCookie.id,
    +  return setSession({ serviceConfig, sessionId: command.recentCookie.id,
         sessionToken: command.recentCookie.token,
         challenges: command.challenges,
         checks: command.checks,
    @@ -227,9 +217,7 @@ export async function setSessionAndUpdateCookie(command: {
               sessionCookie.requestId = command.requestId;
             }
     
    -        return getSession({
    -          serviceUrl,
    -          sessionId: sessionCookie.id,
    +        return getSession({ serviceConfig, sessionId: sessionCookie.id,
               sessionToken: sessionCookie.token,
             }).then(async (response) => {
               if (!response?.session || !response.session.factors?.user?.loginName) {
    @@ -252,7 +240,7 @@ export async function setSessionAndUpdateCookie(command: {
                 newCookie.requestId = sessionCookie.requestId;
               }
     
    -          const securitySettings = await getSecuritySettings({ serviceUrl });
    +          const securitySettings = await getSecuritySettings({ serviceConfig });
               const iFrameEnabled = !!securitySettings?.embeddedIframe?.enabled;
     
               return updateSessionCookie({
    
  • apps/login/src/lib/server/device.ts+3 5 modified
    @@ -2,19 +2,17 @@
     
     import { authorizeOrDenyDeviceAuthorization } from "@/lib/zitadel";
     import { headers } from "next/headers";
    -import { getServiceUrlFromHeaders } from "../service-url";
    +import { getServiceConfig } from "../service-url";
     
     export async function completeDeviceAuthorization(
       deviceAuthorizationId: string,
       session?: { sessionId: string; sessionToken: string },
     ) {
       const _headers = await headers();
    -  const { serviceUrl } = getServiceUrlFromHeaders(_headers);
    +  const { serviceConfig } = getServiceConfig(_headers);
     
       // without the session, device auth request is denied
    -  return authorizeOrDenyDeviceAuthorization({
    -    serviceUrl,
    -    deviceAuthorizationId,
    +  return authorizeOrDenyDeviceAuthorization({ serviceConfig, deviceAuthorizationId,
         session,
       });
     }
    
  • apps/login/src/lib/server/flow-initiation.ts+16 39 modified
    @@ -9,6 +9,7 @@ import {
       getSAMLRequest,
       getSecuritySettings,
       startIdentityProviderFlow,
    +  ServiceConfig,
     } from "@/lib/zitadel";
     import { sendLoginname, SendLoginnameCommand } from "@/lib/server/loginname";
     import { idpTypeToSlug } from "@/lib/idp";
    @@ -47,7 +48,7 @@ const gotoAccounts = ({
     };
     
     export interface FlowInitiationParams {
    -  serviceUrl: string;
    +  serviceConfig: ServiceConfig;
       requestId: string;
       sessions: Session[];
       sessionCookies: any[];
    @@ -58,11 +59,9 @@ export interface FlowInitiationParams {
      * Handle OIDC flow initiation
      */
     export async function handleOIDCFlowInitiation(params: FlowInitiationParams): Promise<NextResponse> {
    -  const { serviceUrl, requestId, sessions, sessionCookies, request } = params;
    +  const { serviceConfig, requestId, sessions, sessionCookies, request } = params;
     
    -  const { authRequest } = await getAuthRequest({
    -    serviceUrl,
    -    authRequestId: requestId.replace("oidc_", ""),
    +  const { authRequest } = await getAuthRequest({ serviceConfig, authRequestId: requestId.replace("oidc_", ""),
       });
     
       let organization = "";
    @@ -85,9 +84,7 @@ export async function handleOIDCFlowInitiation(params: FlowInitiationParams): Pr
     
             console.log("Extracted org domain:", orgDomain);
             if (orgDomain) {
    -          const orgs = await getOrgsByDomain({
    -            serviceUrl,
    -            domain: orgDomain,
    +          const orgs = await getOrgsByDomain({ serviceConfig, domain: orgDomain,
               });
     
               if (orgs.result && orgs.result.length === 1) {
    @@ -102,9 +99,7 @@ export async function handleOIDCFlowInitiation(params: FlowInitiationParams): Pr
           const matched = IDP_SCOPE_REGEX.exec(idpScope);
           idpId = matched?.[1] ?? "";
     
    -      const identityProviders = await getActiveIdentityProviders({
    -        serviceUrl,
    -        orgId: organization ? organization : undefined,
    +      const identityProviders = await getActiveIdentityProviders({ serviceConfig, orgId: organization ? organization : undefined,
           }).then((resp) => {
             return resp.identityProviders;
           });
    @@ -137,9 +132,7 @@ export async function handleOIDCFlowInitiation(params: FlowInitiationParams): Pr
               params.set("organization", organization);
             }
     
    -        let url: string | null = await startIdentityProviderFlow({
    -          serviceUrl,
    -          idpId,
    +        let url: string | null = await startIdentityProviderFlow({ serviceConfig, idpId,
               urls: {
                 successUrl: `${origin}/idp/${provider}/process?` + new URLSearchParams(params),
                 failureUrl: `${origin}/idp/${provider}/failure?` + new URLSearchParams(params),
    @@ -215,13 +208,9 @@ export async function handleOIDCFlowInitiation(params: FlowInitiationParams): Pr
           }
           return NextResponse.redirect(loginNameUrl);
         } else if (authRequest.prompt.includes(Prompt.NONE)) {
    -      const securitySettings = await getSecuritySettings({
    -        serviceUrl,
    -      });
    +      const securitySettings = await getSecuritySettings({ serviceConfig, });
     
    -      const selectedSession = await findValidSession({
    -        serviceUrl,
    -        sessions,
    +      const selectedSession = await findValidSession({ serviceConfig, sessions,
             authRequest,
           });
     
    @@ -251,9 +240,7 @@ export async function handleOIDCFlowInitiation(params: FlowInitiationParams): Pr
             sessionToken: cookie.token,
           };
     
    -      const { callbackUrl } = await createCallback({
    -        serviceUrl,
    -        req: create(CreateCallbackRequestSchema, {
    +      const { callbackUrl } = await createCallback({ serviceConfig, req: create(CreateCallbackRequestSchema, {
               authRequestId: requestId.replace("oidc_", ""),
               callbackKind: {
                 case: "session",
    @@ -275,9 +262,7 @@ export async function handleOIDCFlowInitiation(params: FlowInitiationParams): Pr
     
           return callbackResponse;
         } else {
    -      let selectedSession = await findValidSession({
    -        serviceUrl,
    -        sessions,
    +      let selectedSession = await findValidSession({ serviceConfig, sessions,
             authRequest,
           });
     
    @@ -305,9 +290,7 @@ export async function handleOIDCFlowInitiation(params: FlowInitiationParams): Pr
           };
     
           try {
    -        const { callbackUrl } = await createCallback({
    -          serviceUrl,
    -          req: create(CreateCallbackRequestSchema, {
    +        const { callbackUrl } = await createCallback({ serviceConfig, req: create(CreateCallbackRequestSchema, {
                 authRequestId: requestId.replace("oidc_", ""),
                 callbackKind: {
                   case: "session",
    @@ -359,11 +342,9 @@ export async function handleOIDCFlowInitiation(params: FlowInitiationParams): Pr
      * Handle SAML flow initiation
      */
     export async function handleSAMLFlowInitiation(params: FlowInitiationParams): Promise<NextResponse> {
    -  const { serviceUrl, requestId, sessions, sessionCookies, request } = params;
    +  const { serviceConfig, requestId, sessions, sessionCookies, request } = params;
     
    -  const { samlRequest } = await getSAMLRequest({
    -    serviceUrl,
    -    samlRequestId: requestId.replace("saml_", ""),
    +  const { samlRequest } = await getSAMLRequest({ serviceConfig, samlRequestId: requestId.replace("saml_", ""),
       });
     
       if (!samlRequest) {
    @@ -378,9 +359,7 @@ export async function handleSAMLFlowInitiation(params: FlowInitiationParams): Pr
       }
     
       // Try to find a valid session
    -  let selectedSession = await findValidSession({
    -    serviceUrl,
    -    sessions,
    +  let selectedSession = await findValidSession({ serviceConfig, sessions,
         samlRequest,
       });
     
    @@ -410,9 +389,7 @@ export async function handleSAMLFlowInitiation(params: FlowInitiationParams): Pr
       };
     
       try {
    -    const { url, binding } = await createResponse({
    -      serviceUrl,
    -      req: create(CreateResponseRequestSchema, {
    +    const { url, binding } = await createResponse({ serviceConfig, req: create(CreateResponseRequestSchema, {
             samlRequestId: requestId.replace("saml_", ""),
             responseKind: {
               case: "session",
    
  • apps/login/src/lib/server/host.test.ts+162 161 modified
    @@ -1,10 +1,5 @@
     import { describe, expect, test, vi, beforeEach, afterEach } from "vitest";
    -import { getOriginalHost, getOriginalHostWithProtocol } from "./host";
    -
    -// Mock the Next.js headers function
    -vi.mock("next/headers", () => ({
    -  headers: vi.fn(),
    -}));
    +import { getInstanceHost, getPublicHostWithProtocol, getPublicHost } from "./host";
     
     describe("Host utility functions", () => {
       beforeEach(() => {
    @@ -15,283 +10,289 @@ describe("Host utility functions", () => {
         vi.restoreAllMocks();
       });
     
    -  describe("getOriginalHost", () => {
    -    test("should return x-forwarded-host when available", async () => {
    -      const { headers } = await import("next/headers");
    +  describe("getInstanceHost", () => {
    +    test("should use x-zitadel-instance-host when available", () => {
           const mockHeaders = {
             get: vi.fn((key: string) => {
    -          if (key === "x-forwarded-host") return "zitadel.com";
    -          if (key === "x-original-host") return "backup.com";
    -          if (key === "host") return "internal.vercel.app";
    +          if (key === "x-zitadel-instance-host") return "instance.zitadel.cloud";
    +          if (key === "x-zitadel-forward-host") return "forward.zitadel.cloud";
               return null;
             }),
    -      };
    +      } as any;
     
    -      vi.mocked(headers).mockResolvedValue(mockHeaders as any);
    -
    -      const result = await getOriginalHost();
    -      expect(result).toBe("zitadel.com");
    -      expect(mockHeaders.get).toHaveBeenCalledWith("x-forwarded-host");
    +      const result = getInstanceHost(mockHeaders);
    +      expect(result).toBe("instance.zitadel.cloud");
    +      expect(mockHeaders.get).toHaveBeenCalledWith("x-zitadel-instance-host");
         });
     
    -    test("should fall back to x-original-host when x-forwarded-host is not available", async () => {
    -      const { headers } = await import("next/headers");
    +    test("should use x-zitadel-forward-host when x-zitadel-instance-host is not available", () => {
           const mockHeaders = {
             get: vi.fn((key: string) => {
    -          if (key === "x-forwarded-host") return null;
    -          if (key === "x-original-host") return "original.com";
    -          if (key === "host") return "internal.vercel.app";
    +          if (key === "x-zitadel-instance-host") return null;
    +          if (key === "x-zitadel-forward-host") return "forward.zitadel.cloud";
               return null;
             }),
    -      };
    -
    -      vi.mocked(headers).mockResolvedValue(mockHeaders as any);
    +      } as any;
     
    -      const result = await getOriginalHost();
    -      expect(result).toBe("original.com");
    -      expect(mockHeaders.get).toHaveBeenCalledWith("x-forwarded-host");
    -      expect(mockHeaders.get).toHaveBeenCalledWith("x-original-host");
    +      const result = getInstanceHost(mockHeaders);
    +      expect(result).toBe("forward.zitadel.cloud");
    +      expect(mockHeaders.get).toHaveBeenCalledWith("x-zitadel-instance-host");
    +      expect(mockHeaders.get).toHaveBeenCalledWith("x-zitadel-forward-host");
         });
     
    -    test("should fall back to host when forwarded headers are not available", async () => {
    -      const { headers } = await import("next/headers");
    +    test("should return null when neither x-zitadel-instance-host nor x-zitadel-forward-host are available", () => {
           const mockHeaders = {
             get: vi.fn((key: string) => {
    -          if (key === "x-forwarded-host") return null;
    -          if (key === "x-original-host") return null;
    -          if (key === "host") return "fallback.com";
    +          if (key === "x-zitadel-instance-host") return null;
    +          if (key === "x-zitadel-forward-host") return null;
    +          if (key === "x-forwarded-host") return "accounts.mycompany.com";
    +          if (key === "host") return "internal.server";
               return null;
             }),
    -      };
    -
    -      vi.mocked(headers).mockResolvedValue(mockHeaders as any);
    -
    -      const result = await getOriginalHost();
    -      expect(result).toBe("fallback.com");
    -      expect(mockHeaders.get).toHaveBeenCalledWith("x-forwarded-host");
    -      expect(mockHeaders.get).toHaveBeenCalledWith("x-original-host");
    -      expect(mockHeaders.get).toHaveBeenCalledWith("host");
    -    });
    -
    -    test("should throw error when no host is found", async () => {
    -      const { headers } = await import("next/headers");
    -      const mockHeaders = {
    -        get: vi.fn(() => null),
    -      };
    -
    -      vi.mocked(headers).mockResolvedValue(mockHeaders as any);
    -
    -      await expect(getOriginalHost()).rejects.toThrow("No host found in headers");
    -    });
    +      } as any;
     
    -    test("should throw error when host is empty string", async () => {
    -      const { headers } = await import("next/headers");
    -      const mockHeaders = {
    -        get: vi.fn(() => ""),
    -      };
    -
    -      vi.mocked(headers).mockResolvedValue(mockHeaders as any);
    -
    -      await expect(getOriginalHost()).rejects.toThrow("No host found in headers");
    -    });
    -
    -    test("should throw error when host is not a string", async () => {
    -      const { headers } = await import("next/headers");
    -      const mockHeaders = {
    -        get: vi.fn(() => 123),
    -      };
    -
    -      vi.mocked(headers).mockResolvedValue(mockHeaders as any);
    -
    -      await expect(getOriginalHost()).rejects.toThrow("No host found in headers");
    +      const result = getInstanceHost(mockHeaders);
    +      expect(result).toBeNull();
    +      expect(mockHeaders.get).toHaveBeenCalledWith("x-zitadel-instance-host");
    +      expect(mockHeaders.get).toHaveBeenCalledWith("x-zitadel-forward-host");
         });
       });
     
    -  describe("getOriginalHostWithProtocol", () => {
    -    test("should return https for production domain", async () => {
    -      const { headers } = await import("next/headers");
    +  describe("getPublicHostWithProtocol", () => {
    +    test("should return https for production domain", () => {
           const mockHeaders = {
             get: vi.fn(() => "zitadel.com"),
    -      };
    -
    -      vi.mocked(headers).mockResolvedValue(mockHeaders as any);
    +      } as any;
     
    -      const result = await getOriginalHostWithProtocol();
    +      const result = getPublicHostWithProtocol(mockHeaders);
           expect(result).toBe("https://zitadel.com");
         });
     
    -    test("should return http for localhost", async () => {
    -      const { headers } = await import("next/headers");
    +    test("should return http for localhost", () => {
           const mockHeaders = {
             get: vi.fn(() => "localhost:3000"),
    -      };
    +      } as any;
     
    -      vi.mocked(headers).mockResolvedValue(mockHeaders as any);
    -
    -      const result = await getOriginalHostWithProtocol();
    +      const result = getPublicHostWithProtocol(mockHeaders);
           expect(result).toBe("http://localhost:3000");
         });
     
    -    test("should return http for localhost without port", async () => {
    -      const { headers } = await import("next/headers");
    +    test("should return http for localhost without port", () => {
           const mockHeaders = {
             get: vi.fn(() => "localhost"),
    -      };
    -
    -      vi.mocked(headers).mockResolvedValue(mockHeaders as any);
    +      } as any;
     
    -      const result = await getOriginalHostWithProtocol();
    +      const result = getPublicHostWithProtocol(mockHeaders);
           expect(result).toBe("http://localhost");
         });
     
    -    test("should return https for custom domain", async () => {
    -      const { headers } = await import("next/headers");
    +    test("should return https for custom domain", () => {
           const mockHeaders = {
             get: vi.fn(() => "auth.company.com"),
    -      };
    +      } as any;
     
    -      vi.mocked(headers).mockResolvedValue(mockHeaders as any);
    -
    -      const result = await getOriginalHostWithProtocol();
    +      const result = getPublicHostWithProtocol(mockHeaders);
           expect(result).toBe("https://auth.company.com");
         });
       });
     
       describe("Real-world scenarios", () => {
    -    test("should handle Vercel rewrite scenario", async () => {
    -      const { headers } = await import("next/headers");
    +    test("should handle Vercel rewrite scenario", () => {
           const mockHeaders = {
             get: vi.fn((key: string) => {
               // Simulate Vercel rewrite: zitadel.com/login -> login-zitadel-qa.vercel.app
               if (key === "x-forwarded-host") return "zitadel.com";
               if (key === "host") return "login-zitadel-qa.vercel.app";
               return null;
             }),
    -      };
    -
    -      vi.mocked(headers).mockResolvedValue(mockHeaders as any);
    +      } as any;
     
    -      const result = await getOriginalHostWithProtocol();
    +      const result = getPublicHostWithProtocol(mockHeaders);
           expect(result).toBe("https://zitadel.com");
         });
     
    -    test("should handle CloudFlare proxy scenario", async () => {
    -      const { headers } = await import("next/headers");
    +    test("should handle CloudFlare proxy scenario", () => {
           const mockHeaders = {
             get: vi.fn((key: string) => {
               if (key === "x-forwarded-host") return "auth.company.com";
               if (key === "x-original-host") return null;
               if (key === "host") return "cloudflare-worker.workers.dev";
               return null;
             }),
    -      };
    -
    -      vi.mocked(headers).mockResolvedValue(mockHeaders as any);
    +      } as any;
     
    -      const result = await getOriginalHost();
    +      const result = getPublicHost(mockHeaders);
           expect(result).toBe("auth.company.com");
         });
     
    -    test("should handle development environment", async () => {
    -      const { headers } = await import("next/headers");
    +    test("should handle development environment", () => {
           const mockHeaders = {
             get: vi.fn((key: string) => {
               if (key === "host") return "localhost:3000";
               return null;
             }),
    -      };
    +      } as any;
     
    -      vi.mocked(headers).mockResolvedValue(mockHeaders as any);
    -
    -      const result = await getOriginalHostWithProtocol();
    +      const result = getPublicHostWithProtocol(mockHeaders);
           expect(result).toBe("http://localhost:3000");
         });
     
    -    test("should handle staging environment with subdomain", async () => {
    -      const { headers } = await import("next/headers");
    +    test("should handle staging environment with subdomain", () => {
           const mockHeaders = {
             get: vi.fn((key: string) => {
               if (key === "x-forwarded-host") return "staging-auth.company.com";
               if (key === "host") return "staging-internal.vercel.app";
               return null;
             }),
    -      };
    -
    -      vi.mocked(headers).mockResolvedValue(mockHeaders as any);
    +      } as any;
     
    -      const result = await getOriginalHostWithProtocol();
    +      const result = getPublicHostWithProtocol(mockHeaders);
           expect(result).toBe("https://staging-auth.company.com");
         });
    +
    +    test("should prioritize x-zitadel-instance-host in multi-tenant scenario", () => {
    +      const mockHeaders = {
    +        get: vi.fn((key: string) => {
    +          if (key === "x-zitadel-instance-host") return "customer.zitadel.cloud";
    +          if (key === "x-forwarded-host") return "accounts.company.com";
    +          if (key === "host") return "internal.vercel.app";
    +          return null;
    +        }),
    +      } as any;
    +
    +      const result = getInstanceHost(mockHeaders);
    +      expect(result).toBe("customer.zitadel.cloud");
    +    });
       });
     
       describe("Edge cases", () => {
    -    test("should handle IPv4 addresses", async () => {
    -      const { headers } = await import("next/headers");
    +    test("should handle IPv4 addresses", () => {
           const mockHeaders = {
             get: vi.fn(() => "192.168.1.100:3000"),
    -      };
    -
    -      vi.mocked(headers).mockResolvedValue(mockHeaders as any);
    +      } as any;
     
    -      const result = await getOriginalHostWithProtocol();
    +      const result = getPublicHostWithProtocol(mockHeaders);
           expect(result).toBe("https://192.168.1.100:3000");
         });
     
    -    test("should handle IPv6 addresses", async () => {
    -      const { headers } = await import("next/headers");
    +    test("should handle IPv6 addresses", () => {
           const mockHeaders = {
             get: vi.fn(() => "[::1]:3000"),
    -      };
    +      } as any;
     
    -      vi.mocked(headers).mockResolvedValue(mockHeaders as any);
    -
    -      const result = await getOriginalHostWithProtocol();
    +      const result = getPublicHostWithProtocol(mockHeaders);
           expect(result).toBe("https://[::1]:3000");
         });
     
    -    test("should handle hosts with ports", async () => {
    -      const { headers } = await import("next/headers");
    +    test("should handle hosts with ports", () => {
           const mockHeaders = {
             get: vi.fn(() => "zitadel.com:8080"),
    -      };
    -
    -      vi.mocked(headers).mockResolvedValue(mockHeaders as any);
    +      } as any;
     
    -      const result = await getOriginalHostWithProtocol();
    +      const result = getPublicHostWithProtocol(mockHeaders);
           expect(result).toBe("https://zitadel.com:8080");
         });
     
    -    test("should handle localhost with different ports", async () => {
    -      const { headers } = await import("next/headers");
    +    test("should handle localhost with different ports", () => {
           const mockHeaders = {
             get: vi.fn(() => "localhost:8080"),
    -      };
    -
    -      vi.mocked(headers).mockResolvedValue(mockHeaders as any);
    +      } as any;
     
    -      const result = await getOriginalHostWithProtocol();
    +      const result = getPublicHostWithProtocol(mockHeaders);
           expect(result).toBe("http://localhost:8080");
         });
    +  });
    +
    +  describe("getPublicHost", () => {
    +    test("should use x-zitadel-public-host when available", () => {
    +      const mockHeaders = {
    +        get: vi.fn((key: string) => {
    +          if (key === "x-zitadel-public-host") return "public.zitadel.cloud";
    +          if (key === "x-forwarded-host") return "accounts.company.com";
    +          return null;
    +        }),
    +      } as any;
    +
    +      const result = getPublicHost(mockHeaders);
    +      expect(result).toBe("public.zitadel.cloud");
    +      expect(mockHeaders.get).toHaveBeenCalledWith("x-zitadel-public-host");
    +    });
     
    -    test("should handle priority order correctly", async () => {
    -      const { headers } = await import("next/headers");
    +    test("should use x-zitadel-forward-host when x-zitadel-public-host is not available", () => {
           const mockHeaders = {
             get: vi.fn((key: string) => {
    -          // All headers are present, should return x-forwarded-host (highest priority)
    -          if (key === "x-forwarded-host") return "priority1.com";
    -          if (key === "x-original-host") return "priority2.com";
    -          if (key === "host") return "priority3.com";
    +          if (key === "x-zitadel-public-host") return null;
    +          if (key === "x-zitadel-forward-host") return "forward.zitadel.cloud";
    +          if (key === "x-forwarded-host") return "accounts.company.com";
               return null;
             }),
    -      };
    +      } as any;
    +
    +      const result = getPublicHost(mockHeaders);
    +      expect(result).toBe("forward.zitadel.cloud");
    +      expect(mockHeaders.get).toHaveBeenCalledWith("x-zitadel-public-host");
    +      expect(mockHeaders.get).toHaveBeenCalledWith("x-zitadel-forward-host");
    +    });
    +
    +    test("should use x-forwarded-host when neither x-zitadel-public-host nor x-zitadel-forward-host is available", () => {
    +      const mockHeaders = {
    +        get: vi.fn((key: string) => {
    +          if (key === "x-zitadel-public-host") return null;
    +          if (key === "x-zitadel-forward-host") return null;
    +          if (key === "x-forwarded-host") return "accounts.company.com";
    +          if (key === "host") return "internal.server";
    +          return null;
    +        }),
    +      } as any;
    +
    +      const result = getPublicHost(mockHeaders);
    +      expect(result).toBe("accounts.company.com");
    +      expect(mockHeaders.get).toHaveBeenCalledWith("x-zitadel-public-host");
    +      expect(mockHeaders.get).toHaveBeenCalledWith("x-zitadel-forward-host");
    +      expect(mockHeaders.get).toHaveBeenCalledWith("x-forwarded-host");
    +    });
     
    -      vi.mocked(headers).mockResolvedValue(mockHeaders as any);
    +    test("should fall back to host when x-forwarded-host is not available", () => {
    +      const mockHeaders = {
    +        get: vi.fn((key: string) => {
    +          if (key === "x-zitadel-public-host") return null;
    +          if (key === "x-zitadel-forward-host") return null;
    +          if (key === "x-forwarded-host") return null;
    +          if (key === "host") return "localhost:3000";
    +          return null;
    +        }),
    +      } as any;
     
    -      const result = await getOriginalHost();
    -      expect(result).toBe("priority1.com");
    -      // Should only call x-forwarded-host since it's available
    +      const result = getPublicHost(mockHeaders);
    +      expect(result).toBe("localhost:3000");
           expect(mockHeaders.get).toHaveBeenCalledWith("x-forwarded-host");
    -      expect(mockHeaders.get).toHaveBeenCalledTimes(1);
    +      expect(mockHeaders.get).toHaveBeenCalledWith("host");
    +    });
    +
    +    test("should throw error when no host is found", () => {
    +      const mockHeaders = {
    +        get: vi.fn(() => null),
    +      } as any;
    +
    +      expect(() => getPublicHost(mockHeaders)).toThrow("No host found in headers");
    +    });
    +
    +    test("should differ from getInstanceHost when x-zitadel-instance-host is present", () => {
    +      const mockHeaders = {
    +        get: vi.fn((key: string) => {
    +          if (key === "x-zitadel-instance-host") return "instance.zitadel.cloud";
    +          if (key === "x-forwarded-host") return "accounts.company.com";
    +          if (key === "host") return "internal.server";
    +          return null;
    +        }),
    +      } as any;
    +
    +      const instanceHost = getInstanceHost(mockHeaders);
    +      const publicHost = getPublicHost(mockHeaders);
    +
    +      expect(instanceHost).toBe("instance.zitadel.cloud");
    +      expect(publicHost).toBe("accounts.company.com");
    +      expect(instanceHost).not.toBe(publicHost);
         });
       });
     });
    
  • apps/login/src/lib/server/host.ts+30 30 modified
    @@ -1,48 +1,48 @@
    -import { headers } from "next/headers";
    +import { ReadonlyHeaders } from "next/dist/server/web/spec-extension/adapters/headers";
     
     /**
      * Gets the original host that the user sees in their browser URL.
      * When using rewrites this function prioritizes forwarded headers that preserve the original host.
      *
    - * ⚠️ SERVER-SIDE ONLY: This function can only be used in:
    - * - Server Actions (functions with "use server")
    - * - Server Components (React components that run on the server)
    - * - Route Handlers (API routes)
    - * - Middleware
    - *
      * @returns The host string (e.g., "zitadel.com")
      * @throws Error if no host is found
      */
    -export async function getOriginalHost(): Promise<string> {
    -  const _headers = await headers();
    -
    -  // Priority order:
    -  // 1. x-forwarded-host - Set by proxies/CDNs with the original host
    -  // 2. x-original-host - Alternative header sometimes used
    -  // 3. host - Fallback to the current host header
    -  const host = _headers.get("x-forwarded-host") || _headers.get("x-original-host") || _headers.get("host");
    -
    -  if (!host || typeof host !== "string") {
    -    throw new Error("No host found in headers");
    -  }
    +export function getInstanceHost(headers: ReadonlyHeaders): string | null {
    +  // use standard proxy headers (x-forwarded-host → host) for both multi-tenant and self-hosted, do not use x-zitadel-instance-host
    +  const instanceHost = headers.get("x-zitadel-instance-host") || headers.get("x-zitadel-forward-host");
     
    -  return host;
    +  return instanceHost;
     }
     
     /**
    - * Gets the original host with protocol prefix.
    - * Automatically detects if localhost should use http:// or https://
    + * Gets the public host that the user sees in their browser URL.
    + * Only considers standard proxy headers (x-forwarded-host and host).
    + * Does NOT include x-zitadel-instance-host.
      *
    - * ⚠️ SERVER-SIDE ONLY: This function can only be used in:
    - * - Server Actions (functions with "use server")
    - * - Server Components (React components that run on the server)
    - * - Route Handlers (API routes)
    - * - Middleware
    + * Use this when you need the public-facing host that the user actually sees,
    + * not the internal instance host used for API routing.
      *
    - * @returns The full URL prefix (e.g., "https://zitadel.com")
    + * @returns The public host string (e.g., "accounts.company.com")
    + * @throws Error if no host is found
      */
    -export async function getOriginalHostWithProtocol(): Promise<string> {
    -  const host = await getOriginalHost();
    +export function getPublicHost(headers: ReadonlyHeaders): string {
    +  // Only use standard proxy headers (x-zitadel-public-host → x-zitadel-forward-host → x-forwarded-host → host)
    +  // Do NOT use x-zitadel-instance-host as it may differ from what the user sees
    +  const publicHost =
    +    headers.get("x-zitadel-public-host") ||
    +    headers.get("x-zitadel-forward-host") ||
    +    headers.get("x-forwarded-host") ||
    +    headers.get("host");
    +
    +  if (!publicHost || typeof publicHost !== "string") {
    +    throw new Error("No host found in headers");
    +  }
    +
    +  return publicHost;
    +}
    +
    +export function getPublicHostWithProtocol(headers: ReadonlyHeaders): string {
    +  const host = getPublicHost(headers);
       const protocol = host.includes("localhost") ? "http://" : "https://";
       return `${protocol}${host}`;
     }
    
  • apps/login/src/lib/server/idp-intent.test.ts+30 30 modified
    @@ -12,7 +12,7 @@ vi.mock("@zitadel/client", () => ({
     }));
     
     vi.mock("../service-url", () => ({
    -  getServiceUrlFromHeaders: vi.fn(),
    +  getServiceConfig: vi.fn(),
     }));
     
     vi.mock("../zitadel", () => ({
    @@ -111,7 +111,7 @@ describe("processIDPCallback", () => {
     
         // Import mocked modules
         const { headers } = await import("next/headers");
    -    const { getServiceUrlFromHeaders } = await import("../service-url");
    +    const { getServiceConfig } = await import("../service-url");
         const {
           retrieveIDPIntent,
           getIDPByID,
    @@ -129,7 +129,7 @@ describe("processIDPCallback", () => {
     
         // Setup mocks
         mockHeaders = vi.mocked(headers);
    -    mockGetServiceUrlFromHeaders = vi.mocked(getServiceUrlFromHeaders);
    +    mockGetServiceUrlFromHeaders = vi.mocked(getServiceConfig);
         mockRetrieveIDPIntent = vi.mocked(retrieveIDPIntent);
         mockGetIDPByID = vi.mocked(getIDPByID);
         mockUpdateHuman = vi.mocked(updateHuman);
    @@ -146,7 +146,7 @@ describe("processIDPCallback", () => {
         // Default mock implementations
         mockHeaders.mockResolvedValue({} as any);
         mockGetServiceUrlFromHeaders.mockReturnValue({
    -      serviceUrl: "https://api.example.com",
    +      serviceConfig: { baseUrl: "https://api.example.com" },
         });
         mockRetrieveIDPIntent.mockResolvedValue(defaultIntent);
         mockGetIDPByID.mockResolvedValue(defaultIdp);
    @@ -257,7 +257,7 @@ describe("processIDPCallback", () => {
           const result = await processIDPCallback(defaultParams);
     
           expect(mockRetrieveIDPIntent).toHaveBeenCalledWith({
    -        serviceUrl: "https://api.example.com",
    +        serviceConfig: { baseUrl: "https://api.example.com" },
             id: "intent123",
             token: "token123",
           });
    @@ -287,7 +287,7 @@ describe("processIDPCallback", () => {
           await processIDPCallback(defaultParams);
     
           expect(mockUpdateHuman).toHaveBeenCalledWith({
    -        serviceUrl: "https://api.example.com",
    +        serviceConfig: { baseUrl: "https://api.example.com" },
             request: expect.objectContaining({
               userId: "user123",
               profile: defaultIntent.updateHumanUser.profile,
    @@ -353,7 +353,7 @@ describe("processIDPCallback", () => {
           const result = await processIDPCallback(linkParams);
     
           expect(mockAddIDPLink).toHaveBeenCalledWith({
    -        serviceUrl: "https://api.example.com",
    +        serviceConfig: { baseUrl: "https://api.example.com" },
             idp: {
               id: "idp123",
               userId: "user123",
    @@ -442,12 +442,12 @@ describe("processIDPCallback", () => {
           const result = await processIDPCallback(defaultParams);
     
           expect(mockListUsers).toHaveBeenCalledWith({
    -        serviceUrl: "https://api.example.com",
    +        serviceConfig: { baseUrl: "https://api.example.com" },
             email: "test@example.com",
             organizationId: "org123",
           });
           expect(mockAddIDPLink).toHaveBeenCalledWith({
    -        serviceUrl: "https://api.example.com",
    +        serviceConfig: { baseUrl: "https://api.example.com" },
             idp: {
               id: "idp123",
               userId: "user123",
    @@ -523,7 +523,7 @@ describe("processIDPCallback", () => {
           const result = await processIDPCallback(defaultParams);
     
           expect(mockListUsers).toHaveBeenCalledWith({
    -        serviceUrl: "https://api.example.com",
    +        serviceConfig: { baseUrl: "https://api.example.com" },
             userName: "testuser",
             organizationId: "org123",
           });
    @@ -557,7 +557,7 @@ describe("processIDPCallback", () => {
           const result = await processIDPCallback(defaultParams);
     
           expect(mockAddHuman).toHaveBeenCalledWith({
    -        serviceUrl: "https://api.example.com",
    +        serviceConfig: { baseUrl: "https://api.example.com" },
             request: expect.objectContaining({
               username: "testuser",
               profile: defaultIntent.addHumanUser.profile,
    @@ -602,11 +602,11 @@ describe("processIDPCallback", () => {
           });
     
           expect(mockGetOrgsByDomain).toHaveBeenCalledWith({
    -        serviceUrl: "https://api.example.com",
    +        serviceConfig: { baseUrl: "https://api.example.com" },
             domain: "example.com",
           });
           expect(mockAddHuman).toHaveBeenCalledWith({
    -        serviceUrl: "https://api.example.com",
    +        serviceConfig: { baseUrl: "https://api.example.com" },
             request: expect.objectContaining({
               organization: expect.objectContaining({
                 org: { case: "orgId", value: "org-from-domain" },
    @@ -625,10 +625,10 @@ describe("processIDPCallback", () => {
           });
     
           expect(mockGetDefaultOrg).toHaveBeenCalledWith({
    -        serviceUrl: "https://api.example.com",
    +        serviceConfig: { baseUrl: "https://api.example.com" },
           });
           expect(mockAddHuman).toHaveBeenCalledWith({
    -        serviceUrl: "https://api.example.com",
    +        serviceConfig: { baseUrl: "https://api.example.com" },
             request: expect.objectContaining({
               organization: expect.objectContaining({
                 org: { case: "orgId", value: "default-org" },
    @@ -901,14 +901,14 @@ describe("validateIDPLinkingPermissions", () => {
           });
     
           const result = await validateIDPLinkingPermissions({
    -        serviceUrl: "https://api.example.com",
    +        serviceConfig: { baseUrl: "https://api.example.com" },
             userOrganizationId: "org123",
             idpId: "idp123",
           });
     
           expect(result).toBe(false);
           expect(mockGetLoginSettings).toHaveBeenCalledWith({
    -        serviceUrl: "https://api.example.com",
    +        serviceConfig: { baseUrl: "https://api.example.com" },
             organization: "org123",
           });
           expect(mockGetActiveIdentityProviders).not.toHaveBeenCalled();
    @@ -918,7 +918,7 @@ describe("validateIDPLinkingPermissions", () => {
           mockGetLoginSettings.mockResolvedValue(undefined);
     
           const result = await validateIDPLinkingPermissions({
    -        serviceUrl: "https://api.example.com",
    +        serviceConfig: { baseUrl: "https://api.example.com" },
             userOrganizationId: "org123",
             idpId: "idp123",
           });
    @@ -930,7 +930,7 @@ describe("validateIDPLinkingPermissions", () => {
           mockGetLoginSettings.mockResolvedValue({});
     
           const result = await validateIDPLinkingPermissions({
    -        serviceUrl: "https://api.example.com",
    +        serviceConfig: { baseUrl: "https://api.example.com" },
             userOrganizationId: "org123",
             idpId: "idp123",
           });
    @@ -952,14 +952,14 @@ describe("validateIDPLinkingPermissions", () => {
           });
     
           const result = await validateIDPLinkingPermissions({
    -        serviceUrl: "https://api.example.com",
    +        serviceConfig: { baseUrl: "https://api.example.com" },
             userOrganizationId: "org123",
             idpId: "idp123",
           });
     
           expect(result).toBe(false);
           expect(mockGetActiveIdentityProviders).toHaveBeenCalledWith({
    -        serviceUrl: "https://api.example.com",
    +        serviceConfig: { baseUrl: "https://api.example.com" },
             orgId: "org123",
             linking_allowed: true,
           });
    @@ -974,7 +974,7 @@ describe("validateIDPLinkingPermissions", () => {
           });
     
           const result = await validateIDPLinkingPermissions({
    -        serviceUrl: "https://api.example.com",
    +        serviceConfig: { baseUrl: "https://api.example.com" },
             userOrganizationId: "org123",
             idpId: "idp123",
           });
    @@ -989,7 +989,7 @@ describe("validateIDPLinkingPermissions", () => {
           mockGetActiveIdentityProviders.mockResolvedValue({});
     
           const result = await validateIDPLinkingPermissions({
    -        serviceUrl: "https://api.example.com",
    +        serviceConfig: { baseUrl: "https://api.example.com" },
             userOrganizationId: "org123",
             idpId: "idp123",
           });
    @@ -1011,18 +1011,18 @@ describe("validateIDPLinkingPermissions", () => {
           });
     
           const result = await validateIDPLinkingPermissions({
    -        serviceUrl: "https://api.example.com",
    +        serviceConfig: { baseUrl: "https://api.example.com" },
             userOrganizationId: "org123",
             idpId: "idp123",
           });
     
           expect(result).toBe(true);
           expect(mockGetLoginSettings).toHaveBeenCalledWith({
    -        serviceUrl: "https://api.example.com",
    +        serviceConfig: { baseUrl: "https://api.example.com" },
             organization: "org123",
           });
           expect(mockGetActiveIdentityProviders).toHaveBeenCalledWith({
    -        serviceUrl: "https://api.example.com",
    +        serviceConfig: { baseUrl: "https://api.example.com" },
             orgId: "org123",
             linking_allowed: true,
           });
    @@ -1042,7 +1042,7 @@ describe("validateIDPLinkingPermissions", () => {
           });
     
           const result = await validateIDPLinkingPermissions({
    -        serviceUrl: "https://api.example.com",
    +        serviceConfig: { baseUrl: "https://api.example.com" },
             userOrganizationId: "org123",
             idpId: "idp123",
           });
    @@ -1057,7 +1057,7 @@ describe("validateIDPLinkingPermissions", () => {
     
           await expect(
             validateIDPLinkingPermissions({
    -          serviceUrl: "https://api.example.com",
    +          serviceConfig: { baseUrl: "https://api.example.com" },
               userOrganizationId: "org123",
               idpId: "idp123",
             }),
    @@ -1072,7 +1072,7 @@ describe("validateIDPLinkingPermissions", () => {
     
           await expect(
             validateIDPLinkingPermissions({
    -          serviceUrl: "https://api.example.com",
    +          serviceConfig: { baseUrl: "https://api.example.com" },
               userOrganizationId: "org123",
               idpId: "idp123",
             }),
    @@ -1088,7 +1088,7 @@ describe("validateIDPLinkingPermissions", () => {
           });
     
           const result = await validateIDPLinkingPermissions({
    -        serviceUrl: "https://api.example.com",
    +        serviceConfig: { baseUrl: "https://api.example.com" },
             userOrganizationId: "org123",
             idpId: "idp123",
           });
    
  • apps/login/src/lib/server/idp-intent.ts+26 55 modified
    @@ -1,6 +1,6 @@
     "use server";
     
    -import { getServiceUrlFromHeaders } from "@/lib/service-url";
    +import { getServiceConfig } from "@/lib/service-url";
     import {
       retrieveIDPIntent,
       getIDPByID,
    @@ -13,6 +13,7 @@ import {
       getActiveIdentityProviders,
       getUserByID,
       getDefaultOrg,
    +  ServiceConfig,
     } from "@/lib/zitadel";
     import { headers } from "next/headers";
     import { create } from "@zitadel/client";
    @@ -31,28 +32,24 @@ const ORG_SUFFIX_REGEX = /(?<=@)(.+)/;
     async function resolveOrganizationForUser({
       organization,
       addHumanUser,
    -  serviceUrl,
    +  serviceConfig,
     }: {
       organization?: string;
       addHumanUser?: AddHumanUserRequest;
    -  serviceUrl: string;
    +  serviceConfig: ServiceConfig;
     }): Promise<string | undefined> {
       if (organization) return organization;
     
       if (addHumanUser?.username && ORG_SUFFIX_REGEX.test(addHumanUser.username)) {
         const matched = ORG_SUFFIX_REGEX.exec(addHumanUser.username);
         const suffix = matched?.[1] ?? "";
     
    -    const orgs = await getOrgsByDomain({
    -      serviceUrl,
    -      domain: suffix,
    +    const orgs = await getOrgsByDomain({ serviceConfig, domain: suffix,
         });
         const orgToCheckForDiscovery = orgs.result && orgs.result.length === 1 ? orgs.result[0].id : undefined;
     
         if (orgToCheckForDiscovery) {
    -      const orgLoginSettings = await getLoginSettings({
    -        serviceUrl,
    -        organization: orgToCheckForDiscovery,
    +      const orgLoginSettings = await getLoginSettings({ serviceConfig, organization: orgToCheckForDiscovery,
           });
           if (orgLoginSettings?.allowDomainDiscovery) {
             return orgToCheckForDiscovery;
    @@ -61,7 +58,7 @@ async function resolveOrganizationForUser({
       }
     
       // Fallback to default organization if no org was resolved through discovery
    -  const defaultOrg = await getDefaultOrg({ serviceUrl });
    +  const defaultOrg = await getDefaultOrg({ serviceConfig });
       return defaultOrg?.id;
     }
     
    @@ -72,29 +69,23 @@ async function resolveOrganizationForUser({
      * 2. The specific IDP is activated for the organization
      *
      */
    -export async function validateIDPLinkingPermissions({
    -  serviceUrl,
    -  userOrganizationId,
    +export async function validateIDPLinkingPermissions({ serviceConfig, userOrganizationId,
       idpId,
     }: {
    -  serviceUrl: string;
    +  serviceConfig: ServiceConfig;
       userOrganizationId: string;
       idpId: string;
     }): Promise<boolean> {
       // Check organization login settings
    -  const loginSettings = await getLoginSettings({
    -    serviceUrl,
    -    organization: userOrganizationId,
    +  const loginSettings = await getLoginSettings({ serviceConfig, organization: userOrganizationId,
       });
     
       if (!loginSettings?.allowExternalIdp) {
         return false;
       }
     
       // Check if the IDP is activated for the organization and allows linking
    -  const activeIDPs = await getActiveIdentityProviders({
    -    serviceUrl,
    -    orgId: userOrganizationId,
    +  const activeIDPs = await getActiveIdentityProviders({ serviceConfig, orgId: userOrganizationId,
         linking_allowed: true,
       });
     
    @@ -132,7 +123,7 @@ export async function processIDPCallback({
       postErrorRedirectUrl?: string;
     }): Promise<{ redirect?: string; error?: string }> {
       const _headers = await headers();
    -  const { serviceUrl } = getServiceUrlFromHeaders(_headers);
    +  const { serviceConfig } = getServiceConfig(_headers);
     
       const t = await getTranslations("idp");
     
    @@ -155,9 +146,7 @@ export async function processIDPCallback({
         });
     
         // Consume the single-use token ONCE
    -    const intent = await retrieveIDPIntent({
    -      serviceUrl,
    -      id,
    +    const intent = await retrieveIDPIntent({ serviceConfig, id,
           token,
         });
     
    @@ -171,9 +160,7 @@ export async function processIDPCallback({
         }
     
         // Get IDP configuration
    -    const idp = await getIDPByID({
    -      serviceUrl,
    -      id: idpInformation.idpId,
    +    const idp = await getIDPByID({ serviceConfig, id: idpInformation.idpId,
         });
     
         if (!idp) {
    @@ -207,9 +194,7 @@ export async function processIDPCallback({
           // Auto-update user if enabled
           if (options?.isAutoUpdate && updateHumanUser) {
             try {
    -          await updateHuman({
    -            serviceUrl,
    -            request: create(UpdateHumanUserRequestSchema, {
    +          await updateHuman({ serviceConfig, request: create(UpdateHumanUserRequestSchema, {
                   userId: userId,
                   profile: updateHumanUser.profile,
                   email: updateHumanUser.email,
    @@ -259,7 +244,7 @@ export async function processIDPCallback({
     
           try {
             // Get user to retrieve their organization
    -        const targetUser = await getUserByID({ serviceUrl, userId });
    +        const targetUser = await getUserByID({ serviceConfig, userId });
     
             if (!targetUser || !targetUser.details?.resourceOwner) {
               console.error("[IDP Process] User not found or missing organization");
    @@ -268,9 +253,7 @@ export async function processIDPCallback({
             }
     
             // Validate IDP linking permissions
    -        const isAllowed = await validateIDPLinkingPermissions({
    -          serviceUrl,
    -          userOrganizationId: targetUser.details.resourceOwner,
    +        const isAllowed = await validateIDPLinkingPermissions({ serviceConfig, userOrganizationId: targetUser.details.resourceOwner,
               idpId: idpInformation.idpId,
             });
     
    @@ -280,9 +263,7 @@ export async function processIDPCallback({
               return { redirect: `/idp/${provider}/linking-failed?${params}&error=validation_failed` };
             }
     
    -        await addIDPLink({
    -          serviceUrl,
    -          idp: {
    +        await addIDPLink({ serviceConfig, idp: {
                 id: idpInformation.idpId,
                 userId: idpInformation.userId,
                 userName: idpInformation.userName,
    @@ -328,21 +309,17 @@ export async function processIDPCallback({
           const email = addHumanUser?.email?.email;
     
           if (options.autoLinking === AutoLinkingOption.EMAIL && email) {
    -        foundUser = await listUsers({ serviceUrl, email, organizationId: organization }).then((response) => {
    +        foundUser = await listUsers({ serviceConfig, email, organizationId: organization }).then((response) => {
               return response.result ? response.result[0] : null;
             });
           } else if (options.autoLinking === AutoLinkingOption.USERNAME) {
    -        foundUser = await listUsers({
    -          serviceUrl,
    -          userName: idpInformation.userName,
    +        foundUser = await listUsers({ serviceConfig, userName: idpInformation.userName,
               organizationId: organization,
             }).then((response) => {
               return response.result ? response.result[0] : null;
             });
           } else {
    -        foundUser = await listUsers({
    -          serviceUrl,
    -          userName: idpInformation.userName,
    +        foundUser = await listUsers({ serviceConfig, userName: idpInformation.userName,
               email,
               organizationId: organization,
             }).then((response) => {
    @@ -359,9 +336,7 @@ export async function processIDPCallback({
               }
     
               // Validate IDP linking permissions
    -          const isAllowed = await validateIDPLinkingPermissions({
    -            serviceUrl,
    -            userOrganizationId: foundUser.details.resourceOwner,
    +          const isAllowed = await validateIDPLinkingPermissions({ serviceConfig, userOrganizationId: foundUser.details.resourceOwner,
                 idpId: idpInformation.idpId,
               });
     
    @@ -371,9 +346,7 @@ export async function processIDPCallback({
                 return { redirect: `/idp/${provider}/linking-failed?${params}&error=validation_failed` };
               }
     
    -          await addIDPLink({
    -            serviceUrl,
    -            idp: {
    +          await addIDPLink({ serviceConfig, idp: {
                   id: idpInformation.idpId,
                   userId: idpInformation.userId,
                   userName: idpInformation.userName,
    @@ -419,7 +392,7 @@ export async function processIDPCallback({
           const orgToRegisterOn = await resolveOrganizationForUser({
             organization,
             addHumanUser,
    -        serviceUrl,
    +        serviceConfig,
           });
     
           if (!orgToRegisterOn) {
    @@ -438,9 +411,7 @@ export async function processIDPCallback({
           });
     
           try {
    -        const newUser = await addHuman({
    -          serviceUrl,
    -          request: addHumanUserWithOrganization,
    +        const newUser = await addHuman({ serviceConfig, request: addHumanUserWithOrganization,
             });
             console.log("[IDP Process] User auto-created successfully, creating session");
     
    @@ -480,7 +451,7 @@ export async function processIDPCallback({
           const orgToRegisterOn = await resolveOrganizationForUser({
             organization,
             addHumanUser,
    -        serviceUrl,
    +        serviceConfig,
           });
     
           if (!orgToRegisterOn) {
    
  • apps/login/src/lib/server/idp.test.ts+15 11 modified
    @@ -13,11 +13,12 @@ vi.mock("next/navigation", () => ({
     }));
     
     vi.mock("../service-url", () => ({
    -  getServiceUrlFromHeaders: vi.fn(),
    +  getServiceConfig: vi.fn(),
     }));
     
     vi.mock("./host", () => ({
    -  getOriginalHost: vi.fn(),
    +  getInstanceHost: vi.fn(),
    +  getPublicHost: vi.fn(),
     }));
     
     vi.mock("../zitadel", () => ({
    @@ -27,30 +28,33 @@ vi.mock("../zitadel", () => ({
     describe("redirectToIdp", () => {
       let mockHeaders: any;
       let mockGetServiceUrlFromHeaders: any;
    -  let mockGetOriginalHost: any;
    +  let mockGetInstanceHost: any;
    +  let mockGetPublicHost: any;
       let mockStartIdentityProviderFlow: any;
     
       beforeEach(async () => {
         vi.clearAllMocks();
     
         // Import mocked modules
         const { headers } = await import("next/headers");
    -    const { getServiceUrlFromHeaders } = await import("../service-url");
    -    const { getOriginalHost } = await import("./host");
    +    const { getServiceConfig } = await import("../service-url");
    +    const { getInstanceHost, getPublicHost } = await import("./host");
         const { startIdentityProviderFlow } = await import("../zitadel");
     
         // Setup mocks
         mockHeaders = vi.mocked(headers);
    -    mockGetServiceUrlFromHeaders = vi.mocked(getServiceUrlFromHeaders);
    -    mockGetOriginalHost = vi.mocked(getOriginalHost);
    +    mockGetServiceUrlFromHeaders = vi.mocked(getServiceConfig);
    +    mockGetInstanceHost = vi.mocked(getInstanceHost);
    +    mockGetPublicHost = vi.mocked(getPublicHost);
         mockStartIdentityProviderFlow = vi.mocked(startIdentityProviderFlow);
     
         // Default mock implementations
         mockHeaders.mockResolvedValue({} as any);
         mockGetServiceUrlFromHeaders.mockReturnValue({
    -      serviceUrl: "https://api.example.com",
    +      serviceConfig: { baseUrl: "https://api.example.com" },
         });
    -    mockGetOriginalHost.mockResolvedValue("example.com");
    +    mockGetInstanceHost.mockReturnValue("example.com");
    +    mockGetPublicHost.mockReturnValue("example.com");
       });
     
       afterEach(() => {
    @@ -76,7 +80,7 @@ describe("redirectToIdp", () => {
           }
     
           expect(mockStartIdentityProviderFlow).toHaveBeenCalledWith({
    -        serviceUrl: "https://api.example.com",
    +        serviceConfig: { baseUrl: "https://api.example.com" },
             idpId: "idp123",
             urls: {
               successUrl: expect.stringContaining("postErrorRedirectUrl=https%3A%2F%2Fapp.example.com%2Ferror"),
    @@ -115,7 +119,7 @@ describe("redirectToIdp", () => {
           }
     
           expect(mockStartIdentityProviderFlow).toHaveBeenCalledWith({
    -        serviceUrl: "https://api.example.com",
    +        serviceConfig: { baseUrl: "https://api.example.com" },
             idpId: "idp123",
             urls: {
               successUrl: expect.not.stringContaining("postErrorRedirectUrl"),
    
  • apps/login/src/lib/server/idp.ts+15 23 modified
    @@ -6,21 +6,22 @@ import {
       listAuthenticationMethodTypes,
       startIdentityProviderFlow,
       startLDAPIdentityProviderFlow,
    +  ServiceConfig,
     } from "@/lib/zitadel";
     import { headers } from "next/headers";
     import { redirect } from "next/navigation";
     import { completeFlowOrGetUrl } from "../client";
    -import { getServiceUrlFromHeaders } from "../service-url";
    +import { getServiceConfig } from "../service-url";
     import { checkEmailVerification, checkMFAFactors } from "../verify-helper";
     import { createSessionForIdpAndUpdateCookie } from "./cookie";
    -import { getOriginalHost } from "./host";
    +import { getPublicHost } from "./host";
     
     export type RedirectToIdpState = { error?: string | null } | undefined;
     
     export async function redirectToIdp(prevState: RedirectToIdpState, formData: FormData): Promise<RedirectToIdpState> {
       const _headers = await headers();
    -  const { serviceUrl } = getServiceUrlFromHeaders(_headers);
    -  const host = await getOriginalHost();
    +  const { serviceConfig } = getServiceConfig(_headers);
    +  const host = getPublicHost(_headers);
     
       const params = new URLSearchParams();
     
    @@ -43,7 +44,7 @@ export async function redirectToIdp(prevState: RedirectToIdpState, formData: For
       }
     
       const response = await startIDPFlow({
    -    serviceUrl,
    +    serviceConfig,
         host,
         idpId,
         successUrl: `/idp/${provider}/process?` + params.toString(),
    @@ -62,7 +63,7 @@ export async function redirectToIdp(prevState: RedirectToIdpState, formData: For
     }
     
     export type StartIDPFlowCommand = {
    -  serviceUrl: string;
    +  serviceConfig: ServiceConfig;
       host: string;
       idpId: string;
       successUrl: string;
    @@ -73,7 +74,7 @@ async function startIDPFlow(command: StartIDPFlowCommand) {
       const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? "";
     
       const url = await startIdentityProviderFlow({
    -    serviceUrl: command.serviceUrl,
    +    serviceConfig: command.serviceConfig,
         idpId: command.idpId,
         urls: {
           successUrl: `${command.host.includes("localhost") ? "http://" : "https://"}${command.host}${basePath}${command.successUrl}`,
    @@ -103,25 +104,19 @@ export type CreateNewSessionCommand = {
     export async function createNewSessionFromIdpIntent(command: CreateNewSessionCommand) {
       const _headers = await headers();
     
    -  const { serviceUrl } = getServiceUrlFromHeaders(_headers);
    +  const { serviceConfig } = getServiceConfig(_headers);
     
       if (!command.userId || !command.idpIntent) {
         throw new Error("No userId or loginName provided");
       }
     
    -  const userResponse = await getUserByID({
    -    serviceUrl,
    -    userId: command.userId,
    -  });
    +  const userResponse = await getUserByID({ serviceConfig, userId: command.userId });
     
       if (!userResponse || !userResponse.user) {
         return { error: "User not found in the system" };
       }
     
    -  const loginSettings = await getLoginSettings({
    -    serviceUrl,
    -    organization: userResponse.user.details?.resourceOwner,
    -  });
    +  const loginSettings = await getLoginSettings({ serviceConfig, organization: userResponse.user.details?.resourceOwner });
     
       const session = await createSessionForIdpAndUpdateCookie({
         userId: command.userId,
    @@ -146,17 +141,14 @@ export async function createNewSessionFromIdpIntent(command: CreateNewSessionCom
       // check if user has MFA methods
       let authMethods;
       if (session.factors?.user?.id) {
    -    const response = await listAuthenticationMethodTypes({
    -      serviceUrl,
    -      userId: session.factors.user.id,
    -    });
    +    const response = await listAuthenticationMethodTypes({ serviceConfig, userId: session.factors.user.id });
         if (response.authMethodTypes && response.authMethodTypes.length) {
           authMethods = response.authMethodTypes;
         }
       }
     
       const mfaFactorCheck = await checkMFAFactors(
    -    serviceUrl,
    +    serviceConfig,
         session,
         loginSettings,
         authMethods || [], // Pass empty array if no auth methods
    @@ -193,14 +185,14 @@ type createNewSessionForLDAPCommand = {
     export async function createNewSessionForLDAP(command: createNewSessionForLDAPCommand) {
       const _headers = await headers();
     
    -  const { serviceUrl } = getServiceUrlFromHeaders(_headers);
    +  const { serviceConfig } = getServiceConfig(_headers);
     
       if (!command.username || !command.password) {
         return { error: "No username or password provided" };
       }
     
       const response = await startLDAPIdentityProviderFlow({
    -    serviceUrl,
    +    serviceConfig,
         idpId: command.idpId,
         username: command.username,
         password: command.password,
    
  • apps/login/src/lib/server/loginname.test.ts+18 14 modified
    @@ -15,7 +15,7 @@ vi.mock("@zitadel/client", () => ({
     }));
     
     vi.mock("../service-url", () => ({
    -  getServiceUrlFromHeaders: vi.fn(),
    +  getServiceConfig: vi.fn(),
     }));
     
     vi.mock("../idp", () => ({
    @@ -39,7 +39,8 @@ vi.mock("./cookie", () => ({
     }));
     
     vi.mock("./host", () => ({
    -  getOriginalHost: vi.fn(),
    +  getInstanceHost: vi.fn(),
    +  getPublicHost: vi.fn(),
     }));
     
     // this returns the key itself that can be checked not the translated value
    @@ -57,7 +58,8 @@ describe("sendLoginname", () => {
       let mockCreateSessionAndUpdateCookie: any;
       let mockListAuthenticationMethodTypes: any;
       let mockListIDPLinks: any;
    -  let mockGetOriginalHost: any;
    +  let mockGetInstanceHost: any;
    +  let mockGetPublicHost: any;
       let mockStartIdentityProviderFlow: any;
       let mockGetActiveIdentityProviders: any;
       let mockGetIDPByID: any;
    @@ -70,7 +72,7 @@ describe("sendLoginname", () => {
         // Import mocked modules
         const { headers } = await import("next/headers");
         const { create } = await import("@zitadel/client");
    -    const { getServiceUrlFromHeaders } = await import("../service-url");
    +    const { getServiceConfig } = await import("../service-url");
         const {
           getLoginSettings,
           searchUsers,
    @@ -81,19 +83,20 @@ describe("sendLoginname", () => {
           getOrgsByDomain,
         } = await import("../zitadel");
         const { createSessionAndUpdateCookie } = await import("./cookie");
    -    const { getOriginalHost } = await import("./host");
    +    const { getInstanceHost, getPublicHost } = await import("./host");
         const { idpTypeToSlug } = await import("../idp");
     
         // Setup mocks
         mockHeaders = vi.mocked(headers);
         mockCreate = vi.mocked(create);
    -    mockGetServiceUrlFromHeaders = vi.mocked(getServiceUrlFromHeaders);
    +    mockGetServiceUrlFromHeaders = vi.mocked(getServiceConfig);
         mockGetLoginSettings = vi.mocked(getLoginSettings);
         mockSearchUsers = vi.mocked(searchUsers);
         mockCreateSessionAndUpdateCookie = vi.mocked(createSessionAndUpdateCookie);
         mockListAuthenticationMethodTypes = vi.mocked(listAuthenticationMethodTypes);
         mockListIDPLinks = vi.mocked(listIDPLinks);
    -    mockGetOriginalHost = vi.mocked(getOriginalHost);
    +    mockGetInstanceHost = vi.mocked(getInstanceHost);
    +    mockGetPublicHost = vi.mocked(getPublicHost);
         mockStartIdentityProviderFlow = vi.mocked(startIdentityProviderFlow);
         mockGetActiveIdentityProviders = vi.mocked(getActiveIdentityProviders);
         mockGetIDPByID = vi.mocked(getIDPByID);
    @@ -102,8 +105,9 @@ describe("sendLoginname", () => {
     
         // Default mock implementations
         mockHeaders.mockResolvedValue({} as any);
    -    mockGetServiceUrlFromHeaders.mockReturnValue({ serviceUrl: "https://api.example.com" });
    -    mockGetOriginalHost.mockResolvedValue("example.com");
    +    mockGetServiceUrlFromHeaders.mockReturnValue({ serviceConfig: { baseUrl: "https://api.example.com" } });
    +    mockGetInstanceHost.mockReturnValue("example.com");
    +    mockGetPublicHost.mockReturnValue("example.com");
         mockIdpTypeToSlug.mockReturnValue("google");
         mockGetIDPByID.mockResolvedValue({
           id: "idp123",
    @@ -243,7 +247,7 @@ describe("sendLoginname", () => {
     
             expect(result).toEqual({ redirect: "https://idp.example.com/auth" });
             expect(mockListIDPLinks).toHaveBeenCalledWith({
    -          serviceUrl: "https://api.example.com",
    +          serviceConfig: { baseUrl: "https://api.example.com" },
               userId: "user123",
             });
           });
    @@ -283,7 +287,7 @@ describe("sendLoginname", () => {
     
             expect(result).toEqual({ redirect: "https://org-idp.example.com/auth" });
             expect(mockGetActiveIdentityProviders).toHaveBeenCalledWith({
    -          serviceUrl: "https://api.example.com",
    +          serviceConfig: { baseUrl: "https://api.example.com" },
               orgId: "org123", // User's organization from resourceOwner
             });
           });
    @@ -523,7 +527,7 @@ describe("sendLoginname", () => {
     
           // Verify org discovery was called with correct domain
           expect(mockGetOrgsByDomain).toHaveBeenCalledWith({
    -        serviceUrl: "https://api.example.com",
    +        serviceConfig: { baseUrl: "https://api.example.com" },
             domain: "example.com",
           });
         });
    @@ -562,13 +566,13 @@ describe("sendLoginname", () => {
     
           // Verify org discovery was called
           expect(mockGetOrgsByDomain).toHaveBeenCalledWith({
    -        serviceUrl: "https://api.example.com",
    +        serviceConfig: { baseUrl: "https://api.example.com" },
             domain: "company.com",
           });
     
           // Verify IDP redirect was called with discovered org
           expect(mockGetActiveIdentityProviders).toHaveBeenCalledWith({
    -        serviceUrl: "https://api.example.com",
    +        serviceConfig: { baseUrl: "https://api.example.com" },
             orgId: "discovered-org-456",
           });
         });
    
  • apps/login/src/lib/server/loginname.ts+18 42 modified
    @@ -9,7 +9,7 @@ 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 { getServiceUrlFromHeaders } from "../service-url";
    +import { getServiceConfig } from "../service-url";
     import {
       getActiveIdentityProviders,
       getIDPByID,
    @@ -22,7 +22,7 @@ import {
       startIdentityProviderFlow,
     } from "../zitadel";
     import { createSessionAndUpdateCookie } from "./cookie";
    -import { getOriginalHost } from "./host";
    +import { getPublicHost } from "./host";
     import { IDPLink } from "@zitadel/proto/zitadel/user/v2/idp_pb";
     
     export type SendLoginnameCommand = {
    @@ -36,21 +36,18 @@ const ORG_SUFFIX_REGEX = /(?<=@)(.+)/;
     
     export async function sendLoginname(command: SendLoginnameCommand) {
       const _headers = await headers();
    -  const { serviceUrl } = getServiceUrlFromHeaders(_headers);
    +  const { serviceConfig } = getServiceConfig(_headers);
     
       const t = await getTranslations("loginname");
     
    -  const loginSettingsByContext = await getLoginSettings({
    -    serviceUrl,
    -    organization: command.organization,
    -  });
    +  const loginSettingsByContext = await getLoginSettings({ serviceConfig, organization: command.organization });
     
       if (!loginSettingsByContext) {
         return { error: t("errors.couldNotGetLoginSettings") };
       }
     
       let searchUsersRequest: SearchUsersCommand = {
    -    serviceUrl,
    +    serviceConfig,
         searchValue: command.loginName,
         organizationId: command.organization,
         loginSettings: loginSettingsByContext,
    @@ -88,28 +85,22 @@ export async function sendLoginname(command: SendLoginnameCommand) {
         // If userId is provided, check for user-specific IDP links first
         let identityProviders: IDPLink[] = [];
         if (userId) {
    -      identityProviders = await listIDPLinks({
    -        serviceUrl,
    -        userId,
    -      }).then((resp) => {
    +      identityProviders = await listIDPLinks({ serviceConfig, userId }).then((resp) => {
             return resp.result;
           });
         }
     
         // If no IDP links exist for the user (or no userId provided), try to get active IDPs from the organization
         if (identityProviders.length === 0) {
    -      const activeIdps = await getActiveIdentityProviders({
    -        serviceUrl,
    -        orgId: organization,
    -      }).then((resp) => {
    +      const activeIdps = await getActiveIdentityProviders({ serviceConfig, orgId: organization }).then((resp) => {
             return resp.identityProviders;
           });
     
           // If exactly one active IDP exists in the organization, redirect to it
           if (activeIdps.length === 1) {
             const _headers = await headers();
    -        const { serviceUrl } = getServiceUrlFromHeaders(_headers);
    -        const host = await getOriginalHost();
    +        const { serviceConfig } = getServiceConfig(_headers);
    +        const host = getPublicHost(_headers);
     
             const identityProviderType = activeIdps[0].type;
             const provider = idpTypeToSlug(identityProviderType);
    @@ -131,7 +122,7 @@ export async function sendLoginname(command: SendLoginnameCommand) {
             const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? "";
     
             const url = await startIdentityProviderFlow({
    -          serviceUrl,
    +          serviceConfig,
               idpId: activeIdps[0].id,
               urls: {
                 successUrl:
    @@ -153,15 +144,12 @@ export async function sendLoginname(command: SendLoginnameCommand) {
     
         if (identityProviders.length === 1) {
           const _headers = await headers();
    -      const { serviceUrl } = getServiceUrlFromHeaders(_headers);
    -      const host = await getOriginalHost();
    +      const { serviceConfig } = getServiceConfig(_headers);
    +      const host = getPublicHost(_headers);
     
           const identityProviderId = identityProviders[0].idpId;
     
    -      const idp = await getIDPByID({
    -        serviceUrl,
    -        id: identityProviderId,
    -      });
    +      const idp = await getIDPByID({ serviceConfig, id: identityProviderId });
     
           const idpType = idp?.type;
     
    @@ -189,7 +177,7 @@ export async function sendLoginname(command: SendLoginnameCommand) {
           const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? "";
     
           const url = await startIdentityProviderFlow({
    -        serviceUrl,
    +        serviceConfig,
             idpId: idp.id,
             urls: {
               successUrl:
    @@ -216,10 +204,7 @@ export async function sendLoginname(command: SendLoginnameCommand) {
         const user = users[0];
         const userId = users[0].userId;
     
    -    const userLoginSettings = await getLoginSettings({
    -      serviceUrl,
    -      organization: user.details?.resourceOwner,
    -    });
    +    const userLoginSettings = await getLoginSettings({ serviceConfig, organization: user.details?.resourceOwner });
     
         // compare with the concatenated suffix when set
         const concatLoginname = command.suffix ? `${command.loginName}@${command.suffix}` : command.loginName;
    @@ -273,10 +258,7 @@ export async function sendLoginname(command: SendLoginnameCommand) {
         // Resolve organization from command or session
         const organization = command.organization ?? session.factors?.user?.organizationId;
     
    -    const methods = await listAuthenticationMethodTypes({
    -      serviceUrl,
    -      userId: session.factors?.user?.id,
    -    });
    +    const methods = await listAuthenticationMethodTypes({ serviceConfig, userId: session.factors?.user?.id });
     
         // always resend invite if user has no auth method set
         if (!methods.authMethodTypes || !methods.authMethodTypes.length) {
    @@ -418,18 +400,12 @@ export async function sendLoginname(command: SendLoginnameCommand) {
         const suffix = matched?.[1] ?? "";
     
         // this just returns orgs where the suffix is set as primary domain
    -    const orgs = await getOrgsByDomain({
    -      serviceUrl,
    -      domain: suffix,
    -    });
    +    const orgs = await getOrgsByDomain({ serviceConfig, domain: suffix });
     
         const orgToCheckForDiscovery = orgs.result && orgs.result.length === 1 ? orgs.result[0].id : undefined;
     
         if (orgToCheckForDiscovery) {
    -      const orgLoginSettings = await getLoginSettings({
    -        serviceUrl,
    -        organization: orgToCheckForDiscovery,
    -      });
    +      const orgLoginSettings = await getLoginSettings({ serviceConfig, organization: orgToCheckForDiscovery });
     
           if (orgLoginSettings?.allowDomainDiscovery) {
             console.log("org discovery successful, using org:", orgToCheckForDiscovery);
    
  • apps/login/src/lib/server/oidc.ts+3 5 modified
    @@ -2,14 +2,12 @@
     
     import { getDeviceAuthorizationRequest as zitadelGetDeviceAuthorizationRequest } from "@/lib/zitadel";
     import { headers } from "next/headers";
    -import { getServiceUrlFromHeaders } from "../service-url";
    +import { getServiceConfig } from "../service-url";
     
     export async function getDeviceAuthorizationRequest(userCode: string) {
       const _headers = await headers();
    -  const { serviceUrl } = getServiceUrlFromHeaders(_headers);
    +  const { serviceConfig } = getServiceConfig(_headers);
     
    -  return zitadelGetDeviceAuthorizationRequest({
    -    serviceUrl,
    -    userCode,
    +  return zitadelGetDeviceAuthorizationRequest({ serviceConfig, userCode,
       });
     }
    
  • apps/login/src/lib/server/passkeys.test.ts+3 3 modified
    @@ -14,7 +14,7 @@ vi.mock("@zitadel/client", () => ({
     }));
     
     vi.mock("../service-url", () => ({
    -  getServiceUrlFromHeaders: vi.fn(),
    +  getServiceConfig: vi.fn(),
     }));
     
     vi.mock("../zitadel", () => ({
    @@ -62,7 +62,7 @@ describe("sendPasskey", () => {
     
         // Import mocked modules
         const { headers } = await import("next/headers");
    -    const { getServiceUrlFromHeaders } = await import("../service-url");
    +    const { getServiceConfig } = await import("../service-url");
         const { getLoginSettings, getUserByID } = await import("../zitadel");
         const { setSessionAndUpdateCookie } = await import("./cookie");
         const { getSessionCookieById, getSessionCookieByLoginName, getMostRecentSessionCookie } = await import("../cookies");
    @@ -71,7 +71,7 @@ describe("sendPasskey", () => {
     
         // Setup mocks
         mockHeaders = vi.mocked(headers);
    -    mockGetServiceUrlFromHeaders = vi.mocked(getServiceUrlFromHeaders);
    +    mockGetServiceUrlFromHeaders = vi.mocked(getServiceConfig);
         mockGetLoginSettings = vi.mocked(getLoginSettings);
         mockGetUserByID = vi.mocked(getUserByID);
         mockSetSessionAndUpdateCookie = vi.mocked(setSessionAndUpdateCookie);
    
  • apps/login/src/lib/server/passkeys.ts+16 47 modified
    @@ -20,10 +20,10 @@ import { headers } from "next/headers";
     import { userAgent } from "next/server";
     import { getTranslations } from "next-intl/server";
     import { getMostRecentSessionCookie, getSessionCookieById, getSessionCookieByLoginName } from "../cookies";
    -import { getServiceUrlFromHeaders } from "../service-url";
    +import { getServiceConfig } from "../service-url";
     import { checkEmailVerification, checkUserVerification } from "../verify-helper";
     import { createSessionAndUpdateCookie, setSessionAndUpdateCookie } from "./cookie";
    -import { getOriginalHost } from "./host";
    +import { getPublicHost } from "./host";
     import { completeFlowOrGetUrl } from "../client";
     
     type VerifyPasskeyCommand = {
    @@ -63,8 +63,8 @@ export async function registerPasskeyLink(
       }
     
       const _headers = await headers();
    -  const { serviceUrl } = getServiceUrlFromHeaders(_headers);
    -  const host = await getOriginalHost();
    +  const { serviceConfig } = getServiceConfig(_headers);
    +  const host = getPublicHost(_headers);
     
       let session: GetSessionResponse | undefined;
       let createdSession: Session | undefined;
    @@ -74,11 +74,7 @@ export async function registerPasskeyLink(
       if (command.sessionId) {
         // Session-based flow (existing logic)
         const sessionCookie = await getSessionCookieById({ sessionId: command.sessionId });
    -    session = await getSession({
    -      serviceUrl,
    -      sessionId: sessionCookie.id,
    -      sessionToken: sessionCookie.token,
    -    });
    +    session = await getSession({ serviceConfig, sessionId: sessionCookie.id, sessionToken: sessionCookie.token });
     
         if (!session?.session?.factors?.user?.id) {
           return { error: "Could not determine user from session" };
    @@ -89,10 +85,7 @@ export async function registerPasskeyLink(
         const sessionValid = isSessionValid(session.session);
     
         if (!sessionValid.valid) {
    -      const authmethods = await listAuthenticationMethodTypes({
    -        serviceUrl,
    -        userId: currentUserId,
    -      });
    +      const authmethods = await listAuthenticationMethodTypes({ serviceConfig, userId: currentUserId });
     
           // if the user has no authmethods set, we need to check if the user was verified
           if (authmethods.authMethodTypes.length !== 0) {
    @@ -117,10 +110,7 @@ export async function registerPasskeyLink(
             code: command.code,
           };
         } else {
    -      const codeResponse = await createPasskeyRegistrationLink({
    -        serviceUrl,
    -        userId: currentUserId,
    -      });
    +      const codeResponse = await createPasskeyRegistrationLink({ serviceConfig, userId: currentUserId });
     
           if (!codeResponse?.code?.code) {
             return { error: "Could not create registration link" };
    @@ -136,10 +126,7 @@ export async function registerPasskeyLink(
         };
     
         // Check if user exists
    -    const userResponse = await getUserByID({
    -      serviceUrl,
    -      userId: currentUserId,
    -    });
    +    const userResponse = await getUserByID({ serviceConfig, userId: currentUserId });
     
         if (!userResponse || !userResponse.user) {
           return { error: "User not found" };
    @@ -179,17 +166,12 @@ export async function registerPasskeyLink(
         throw new Error("Could not determine user");
       }
     
    -  return registerPasskey({
    -    serviceUrl,
    -    userId: currentUserId,
    -    code: registerCode,
    -    domain: hostname,
    -  });
    +  return registerPasskey({ serviceConfig, userId: currentUserId, code: registerCode, domain: hostname });
     }
     
     export async function verifyPasskeyRegistration(command: VerifyPasskeyCommand) {
       const _headers = await headers();
    -  const { serviceUrl } = getServiceUrlFromHeaders(_headers);
    +  const { serviceConfig } = getServiceConfig(_headers);
     
       if (!command.sessionId && !command.userId) {
         throw new Error("Either sessionId or userId must be provided");
    @@ -214,11 +196,7 @@ export async function verifyPasskeyRegistration(command: VerifyPasskeyCommand) {
         const sessionCookie = await getSessionCookieById({
           sessionId: command.sessionId,
         });
    -    const session = await getSession({
    -      serviceUrl,
    -      sessionId: sessionCookie.id,
    -      sessionToken: sessionCookie.token,
    -    });
    +    const session = await getSession({ serviceConfig, sessionId: sessionCookie.id, sessionToken: sessionCookie.token });
         const userId = session?.session?.factors?.user?.id;
     
         if (!userId) {
    @@ -231,18 +209,15 @@ export async function verifyPasskeyRegistration(command: VerifyPasskeyCommand) {
         currentUserId = command.userId!;
     
         // Verify user exists
    -    const userResponse = await getUserByID({
    -      serviceUrl,
    -      userId: currentUserId,
    -    });
    +    const userResponse = await getUserByID({ serviceConfig, userId: currentUserId });
     
         if (!userResponse || !userResponse.user) {
           throw new Error("User not found");
         }
       }
     
       return zitadelVerifyPasskeyRegistration({
    -    serviceUrl,
    +    serviceConfig,
         request: create(VerifyPasskeyRegistrationRequestSchema, {
           passkeyId: command.passkeyId,
           publicKeyCredential: command.publicKeyCredential,
    @@ -279,12 +254,9 @@ export async function sendPasskey(command: SendPasskeyCommand) {
       }
     
       const _headers = await headers();
    -  const { serviceUrl } = getServiceUrlFromHeaders(_headers);
    +  const { serviceConfig } = getServiceConfig(_headers);
     
    -  const loginSettings = await getLoginSettings({
    -    serviceUrl,
    -    organization,
    -  });
    +  const loginSettings = await getLoginSettings({ serviceConfig, organization });
     
       let lifetime = command.lifetime; // Use provided lifetime first
     
    @@ -318,10 +290,7 @@ export async function sendPasskey(command: SendPasskeyCommand) {
     
       let userResponse;
       try {
    -    userResponse = await getUserByID({
    -      serviceUrl,
    -      userId: session?.factors?.user?.id,
    -    });
    +    userResponse = await getUserByID({ serviceConfig, userId: session?.factors?.user?.id });
       } catch (error) {
         console.error("Error fetching user by ID:", error);
         return { error: t("verify.errors.couldNotGetUser") };
    
  • apps/login/src/lib/server/password.ts+32 74 modified
    @@ -10,6 +10,7 @@ import {
       listAuthenticationMethodTypes,
       listUsers,
       passwordReset,
    +  ServiceConfig,
       setPassword,
       setUserPassword,
     } from "@/lib/zitadel";
    @@ -22,8 +23,8 @@ import { SetPasswordRequestSchema } from "@zitadel/proto/zitadel/user/v2/user_se
     import { headers } from "next/headers";
     import { completeFlowOrGetUrl } from "../client";
     import { getSessionCookieById, getSessionCookieByLoginName } from "../cookies";
    -import { getServiceUrlFromHeaders } from "../service-url";
    -import { getOriginalHostWithProtocol } from "./host";
    +import { getServiceConfig } from "../service-url";
    +import { getPublicHostWithProtocol } from "./host";
     import {
       checkEmailVerification,
       checkMFAFactors,
    @@ -41,18 +42,14 @@ type ResetPasswordCommand = {
     
     export async function resetPassword(command: ResetPasswordCommand) {
       const _headers = await headers();
    -  const { serviceUrl } = getServiceUrlFromHeaders(_headers);
    +  const { serviceConfig } = getServiceConfig(_headers);
     
       const t = await getTranslations("password");
     
       // Get the original host that the user sees with protocol
    -  const hostWithProtocol = await getOriginalHostWithProtocol();
    +  const hostWithProtocol = await getPublicHostWithProtocol(_headers);
     
    -  const users = await listUsers({
    -    serviceUrl,
    -    loginName: command.loginName,
    -    organizationId: command.organization,
    -  });
    +  const users = await listUsers({ serviceConfig, loginName: command.loginName, organizationId: command.organization });
     
       if (!users.details || users.details.totalResult !== BigInt(1) || !users.result[0].userId) {
         return { error: t("errors.couldNotSendResetLink") };
    @@ -62,7 +59,7 @@ export async function resetPassword(command: ResetPasswordCommand) {
       const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? "";
     
       return passwordReset({
    -    serviceUrl,
    +    serviceConfig,
         userId,
         urlTemplate:
           `${hostWithProtocol}${basePath}/password/set?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}` +
    @@ -79,7 +76,7 @@ export type UpdateSessionCommand = {
     
     export async function sendPassword(command: UpdateSessionCommand): Promise<{ error: string } | { redirect: string }> {
       const _headers = await headers();
    -  const { serviceUrl } = getServiceUrlFromHeaders(_headers);
    +  const { serviceConfig } = getServiceConfig(_headers);
       const t = await getTranslations("password");
     
       let sessionCookie = await getSessionCookieByLoginName({
    @@ -94,11 +91,7 @@ export async function sendPassword(command: UpdateSessionCommand): Promise<{ err
       let loginSettings: LoginSettings | undefined;
     
       if (!sessionCookie) {
    -    const users = await listUsers({
    -      serviceUrl,
    -      loginName: command.loginName,
    -      organizationId: command.organization,
    -    });
    +    const users = await listUsers({ serviceConfig, loginName: command.loginName, organizationId: command.organization });
     
         if (users.details?.totalResult == BigInt(1) && users.result[0].userId) {
           user = users.result[0];
    @@ -108,10 +101,7 @@ export async function sendPassword(command: UpdateSessionCommand): Promise<{ err
             password: { password: command.checks.password?.password },
           });
     
    -      loginSettings = await getLoginSettings({
    -        serviceUrl,
    -        organization: command.organization,
    -      });
    +      loginSettings = await getLoginSettings({ serviceConfig, organization: command.organization });
     
           try {
             session = await createSessionAndUpdateCookie({
    @@ -121,10 +111,7 @@ export async function sendPassword(command: UpdateSessionCommand): Promise<{ err
             });
           } catch (error: any) {
             if ("failedAttempts" in error && error.failedAttempts) {
    -          const lockoutSettings = await getLockoutSettings({
    -            serviceUrl,
    -            orgId: command.organization,
    -          });
    +          const lockoutSettings = await getLockoutSettings({ serviceConfig, orgId: command.organization });
     
               const hasLimit =
                 lockoutSettings?.maxPasswordAttempts !== undefined && lockoutSettings?.maxPasswordAttempts > BigInt(0);
    @@ -149,10 +136,7 @@ export async function sendPassword(command: UpdateSessionCommand): Promise<{ err
         // this is a fake error message to hide that the user does not even exist
         return { error: t("errors.couldNotVerifyPassword") };
       } else {
    -    loginSettings = await getLoginSettings({
    -      serviceUrl,
    -      organization: sessionCookie.organization,
    -    });
    +    loginSettings = await getLoginSettings({ serviceConfig, organization: sessionCookie.organization });
     
         if (!loginSettings) {
           return { error: "Could not load login settings" };
    @@ -177,10 +161,7 @@ export async function sendPassword(command: UpdateSessionCommand): Promise<{ err
           });
         } catch (error: any) {
           if ("failedAttempts" in error && error.failedAttempts) {
    -        const lockoutSettings = await getLockoutSettings({
    -          serviceUrl,
    -          orgId: command.organization,
    -        });
    +        const lockoutSettings = await getLockoutSettings({ serviceConfig, orgId: command.organization });
     
             const hasLimit =
               lockoutSettings?.maxPasswordAttempts !== undefined && lockoutSettings?.maxPasswordAttempts > BigInt(0);
    @@ -202,10 +183,7 @@ export async function sendPassword(command: UpdateSessionCommand): Promise<{ err
           return { error: t("errors.couldNotCreateSessionForUser") };
         }
     
    -    const userResponse = await getUserByID({
    -      serviceUrl,
    -      userId: session?.factors?.user?.id,
    -    });
    +    const userResponse = await getUserByID({ serviceConfig, userId: session?.factors?.user?.id });
     
         if (!userResponse.user) {
           return { error: t("errors.userNotFound") };
    @@ -216,7 +194,7 @@ export async function sendPassword(command: UpdateSessionCommand): Promise<{ err
     
       if (!loginSettings) {
         loginSettings = await getLoginSettings({
    -      serviceUrl,
    +      serviceConfig,
           organization: command.organization ?? session.factors?.user?.organizationId,
         });
       }
    @@ -228,7 +206,7 @@ export async function sendPassword(command: UpdateSessionCommand): Promise<{ err
       const humanUser = user.type.case === "human" ? user.type.value : undefined;
     
       const expirySettings = await getPasswordExpirySettings({
    -    serviceUrl,
    +    serviceConfig,
         orgId: command.organization ?? session.factors?.user?.organizationId,
       });
     
    @@ -260,10 +238,7 @@ export async function sendPassword(command: UpdateSessionCommand): Promise<{ err
       // if password, check if user has MFA methods
       let authMethods;
       if (command.checks && command.checks.password && session.factors?.user?.id) {
    -    const response = await listAuthenticationMethodTypes({
    -      serviceUrl,
    -      userId: session.factors.user.id,
    -    });
    +    const response = await listAuthenticationMethodTypes({ serviceConfig, userId: session.factors.user.id });
         if (response.authMethodTypes && response.authMethodTypes.length) {
           authMethods = response.authMethodTypes;
         }
    @@ -274,7 +249,7 @@ export async function sendPassword(command: UpdateSessionCommand): Promise<{ err
       }
     
       const mfaFactorCheck = await checkMFAFactors(
    -    serviceUrl,
    +    serviceConfig,
         session,
         loginSettings,
         authMethods,
    @@ -331,14 +306,11 @@ export async function sendPassword(command: UpdateSessionCommand): Promise<{ err
     // this function lets users with code set a password or users with valid User Verification Check
     export async function changePassword(command: { code?: string; userId: string; password: string }) {
       const _headers = await headers();
    -  const { serviceUrl } = getServiceUrlFromHeaders(_headers);
    +  const { serviceConfig } = getServiceConfig(_headers);
       const t = await getTranslations("password");
     
       // check for init state
    -  const { user } = await getUserByID({
    -    serviceUrl,
    -    userId: command.userId,
    -  });
    +  const { user } = await getUserByID({ serviceConfig, userId: command.userId });
     
       if (!user || user.userId !== command.userId) {
         return { error: t("errors.couldNotSendResetLink") };
    @@ -351,10 +323,7 @@ export async function changePassword(command: { code?: string; userId: string; p
     
       // check if the user has no password set in order to set a password
       if (!command.code) {
    -    const authmethods = await listAuthenticationMethodTypes({
    -      serviceUrl,
    -      userId,
    -    });
    +    const authmethods = await listAuthenticationMethodTypes({ serviceConfig, userId });
     
         // if the user has no authmethods set, we need to check if the user was verified
         if (authmethods.authMethodTypes.length !== 0) {
    @@ -371,12 +340,7 @@ export async function changePassword(command: { code?: string; userId: string; p
         }
       }
     
    -  return setUserPassword({
    -    serviceUrl,
    -    userId,
    -    password: command.password,
    -    code: command.code,
    -  });
    +  return setUserPassword({ serviceConfig, userId, password: command.password, code: command.code });
     }
     
     type CheckSessionAndSetPasswordCommand = {
    @@ -386,7 +350,7 @@ type CheckSessionAndSetPasswordCommand = {
     
     export async function checkSessionAndSetPassword({ sessionId, password }: CheckSessionAndSetPasswordCommand) {
       const _headers = await headers();
    -  const { serviceUrl } = getServiceUrlFromHeaders(_headers);
    +  const { serviceConfig } = getServiceConfig(_headers);
       const t = await getTranslations("password");
     
       let sessionCookie;
    @@ -400,7 +364,7 @@ export async function checkSessionAndSetPassword({ sessionId, password }: CheckS
       let session;
       try {
         const sessionResponse = await getSession({
    -      serviceUrl,
    +      serviceConfig,
           sessionId: sessionCookie.id,
           sessionToken: sessionCookie.token,
         });
    @@ -424,10 +388,7 @@ export async function checkSessionAndSetPassword({ sessionId, password }: CheckS
       // check if the user has no password set in order to set a password
       let authmethods;
       try {
    -    authmethods = await listAuthenticationMethodTypes({
    -      serviceUrl,
    -      userId: session.factors.user.id,
    -    });
    +    authmethods = await listAuthenticationMethodTypes({ serviceConfig, userId: session.factors.user.id });
       } catch (error) {
         console.error("Error getting auth methods:", error);
         return { error: "Could not load auth methods" };
    @@ -439,10 +400,7 @@ export async function checkSessionAndSetPassword({ sessionId, password }: CheckS
     
       let loginSettings;
       try {
    -    loginSettings = await getLoginSettings({
    -      serviceUrl,
    -      organization: session.factors.user.organizationId,
    -    });
    +    loginSettings = await getLoginSettings({ serviceConfig, organization: session.factors.user.organizationId });
       } catch (error) {
         console.error("Error getting login settings:", error);
         return { error: "Could not load login settings" };
    @@ -453,24 +411,24 @@ export async function checkSessionAndSetPassword({ sessionId, password }: CheckS
       // if the user has no MFA but MFA is enforced, we can set a password otherwise we use the token of the user
       if (forceMfa) {
         console.log("Set password using service account due to enforced MFA without existing MFA methods");
    -    return setPassword({ serviceUrl, payload }).catch((error) => {
    +    return setPassword({ serviceConfig, payload }).catch((error) => {
           // throw error if failed precondition (ex. User is not yet initialized)
           if (error.code === 9 && error.message) {
             return { error: t("errors.failedPrecondition") };
           }
           return { error: "Could not set password" };
         });
       } else {
    -    const transport = async (serviceUrl: string, token: string) => {
    -      return createServerTransport(token, serviceUrl);
    +    const transport = async (serviceConfig: ServiceConfig, token: string) => {
    +      return createServerTransport(token, serviceConfig);
         };
     
    -    const myUserService = async (serviceUrl: string, sessionToken: string) => {
    -      const transportPromise = await transport(serviceUrl, sessionToken);
    +    const myUserService = async (serviceConfig: ServiceConfig, sessionToken: string) => {
    +      const transportPromise = await transport(serviceConfig, sessionToken);
           return createUserServiceClient(transportPromise);
         };
     
    -    const selfService = await myUserService(serviceUrl, `${sessionCookie.token}`);
    +    const selfService = await myUserService(serviceConfig, sessionCookie.token);
     
         return selfService
           .setPassword(
    
  • apps/login/src/lib/server/register.ts+12 26 modified
    @@ -7,7 +7,7 @@ import { Factors } from "@zitadel/proto/zitadel/session/v2/session_pb";
     import { ChecksJson, ChecksSchema } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
     import { cookies, headers } from "next/headers";
     import { getTranslations } from "next-intl/server";
    -import { getServiceUrlFromHeaders } from "../service-url";
    +import { getServiceConfig } from "../service-url";
     import { checkEmailVerification, checkMFAFactors } from "../verify-helper";
     import { getOrSetFingerprintId } from "../fingerprint";
     import crypto from "crypto";
    @@ -30,11 +30,9 @@ export type RegisterUserResponse = {
     export async function registerUser(command: RegisterUserCommand) {
       const t = await getTranslations("register");
       const _headers = await headers();
    -  const { serviceUrl } = getServiceUrlFromHeaders(_headers);
    +  const { serviceConfig } = getServiceConfig(_headers);
     
    -  const addResponse = await addHumanUser({
    -    serviceUrl,
    -    email: command.email,
    +  const addResponse = await addHumanUser({ serviceConfig, email: command.email,
         firstName: command.firstName,
         lastName: command.lastName,
         password: command.password ? command.password : undefined,
    @@ -45,9 +43,7 @@ export async function registerUser(command: RegisterUserCommand) {
         return { error: t("errors.couldNotCreateUser") };
       }
     
    -  const loginSettings = await getLoginSettings({
    -    serviceUrl,
    -    organization: command.organization,
    +  const loginSettings = await getLoginSettings({ serviceConfig, organization: command.organization,
       });
     
       let checkPayload: any = {
    @@ -100,9 +96,7 @@ export async function registerUser(command: RegisterUserCommand) {
     
         return { redirect: "/passkey/set?" + params };
       } else {
    -    const userResponse = await getUserByID({
    -      serviceUrl,
    -      userId: session?.factors?.user?.id,
    +    const userResponse = await getUserByID({ serviceConfig, userId: session?.factors?.user?.id,
         });
     
         if (!userResponse.user) {
    @@ -162,11 +156,9 @@ export async function registerUserAndLinkToIDP(command: RegisterUserAndLinkToIDP
       const t = await getTranslations("register");
     
       const _headers = await headers();
    -  const { serviceUrl } = getServiceUrlFromHeaders(_headers);
    +  const { serviceConfig } = getServiceConfig(_headers);
     
    -  const addUserResponse = await addHumanUser({
    -    serviceUrl,
    -    email: command.email,
    +  const addUserResponse = await addHumanUser({ serviceConfig, email: command.email,
         firstName: command.firstName,
         lastName: command.lastName,
         organization: command.organization,
    @@ -176,14 +168,10 @@ export async function registerUserAndLinkToIDP(command: RegisterUserAndLinkToIDP
         return { error: t("errors.couldNotCreateUser") };
       }
     
    -  const loginSettings = await getLoginSettings({
    -    serviceUrl,
    -    organization: command.organization,
    +  const loginSettings = await getLoginSettings({ serviceConfig, organization: command.organization,
       });
     
    -  const idpLink = await addIDPLink({
    -    serviceUrl,
    -    idp: {
    +  const idpLink = await addIDPLink({ serviceConfig, idp: {
           id: command.idpId,
           userId: command.idpUserId,
           userName: command.idpUserName,
    @@ -207,7 +195,7 @@ export async function registerUserAndLinkToIDP(command: RegisterUserAndLinkToIDP
       }
     
       // const userResponse = await getUserByID({
    -  //   serviceUrl,
    +  //   serviceConfig.baseUrl,
       //   userId: session?.factors?.user?.id,
       // });
     
    @@ -227,9 +215,7 @@ export async function registerUserAndLinkToIDP(command: RegisterUserAndLinkToIDP
       // check if user has MFA methods
       let authMethods;
       if (session.factors?.user?.id) {
    -    const response = await listAuthenticationMethodTypes({
    -      serviceUrl,
    -      userId: session.factors.user.id,
    +    const response = await listAuthenticationMethodTypes({ serviceConfig, userId: session.factors.user.id,
         });
         if (response.authMethodTypes && response.authMethodTypes.length) {
           authMethods = response.authMethodTypes;
    @@ -239,7 +225,7 @@ export async function registerUserAndLinkToIDP(command: RegisterUserAndLinkToIDP
       // Always check MFA factors, even if no auth methods are configured
       // This ensures that force MFA settings are respected
       const mfaFactorCheck = await checkMFAFactors(
    -    serviceUrl,
    +    serviceConfig,
         session,
         loginSettings,
         authMethods || [], // Pass empty array if no auth methods
    
  • apps/login/src/lib/server/session.ts+14 26 modified
    @@ -21,8 +21,8 @@ import {
       getSessionCookieByLoginName,
       removeSessionFromCookie,
     } from "../cookies";
    -import { getServiceUrlFromHeaders } from "../service-url";
    -import { getOriginalHost } from "./host";
    +import { getServiceConfig } from "../service-url";
    +import { getPublicHost } from "./host";
     
     export async function skipMFAAndContinueWithNextUrl({
       userId,
    @@ -38,14 +38,11 @@ export async function skipMFAAndContinueWithNextUrl({
       organization?: string;
     }): Promise<{ redirect: string } | { error: string }> {
       const _headers = await headers();
    -  const { serviceUrl } = getServiceUrlFromHeaders(_headers);
    +  const { serviceConfig } = getServiceConfig(_headers);
     
    -  const loginSettings = await getLoginSettings({
    -    serviceUrl,
    -    organization: organization,
    -  });
    +  const loginSettings = await getLoginSettings({ serviceConfig, organization: organization });
     
    -  await humanMFAInitSkipped({ serviceUrl, userId });
    +  await humanMFAInitSkipped({ serviceConfig, userId });
     
       if (requestId && sessionId) {
         return completeFlowOrGetUrl(
    @@ -73,14 +70,11 @@ export type ContinueWithSessionCommand = Session & { requestId?: string };
     
     export async function continueWithSession({ requestId, ...session }: ContinueWithSessionCommand) {
       const _headers = await headers();
    -  const { serviceUrl } = getServiceUrlFromHeaders(_headers);
    +  const { serviceConfig } = getServiceConfig(_headers);
     
       const t = await getTranslations("error");
     
    -  const loginSettings = await getLoginSettings({
    -    serviceUrl,
    -    organization: session.factors?.user?.organizationId,
    -  });
    +  const loginSettings = await getLoginSettings({ serviceConfig, organization: session.factors?.user?.organizationId });
     
       if (requestId && session.id && session.factors?.user) {
         return completeFlowOrGetUrl(
    @@ -130,8 +124,8 @@ export async function updateSession(options: UpdateSessionCommand) {
       }
     
       const _headers = await headers();
    -  const { serviceUrl } = getServiceUrlFromHeaders(_headers);
    -  const host = await getOriginalHost();
    +  const { serviceConfig } = getServiceConfig(_headers);
    +  const host = getPublicHost(_headers);
     
       if (!host) {
         return { error: "Could not get host" };
    @@ -143,10 +137,7 @@ export async function updateSession(options: UpdateSessionCommand) {
         challenges.webAuthN.domain = hostname;
       }
     
    -  const loginSettings = await getLoginSettings({
    -    serviceUrl,
    -    organization,
    -  });
    +  const loginSettings = await getLoginSettings({ serviceConfig, organization });
     
       let lifetime = checks?.webAuthN
         ? loginSettings?.multiFactorCheckLifetime // TODO different lifetime for webauthn u2f/passkey
    @@ -177,10 +168,7 @@ export async function updateSession(options: UpdateSessionCommand) {
       // if password, check if user has MFA methods
       let authMethods;
       if (checks && checks.password && session.factors?.user?.id) {
    -    const response = await listAuthenticationMethodTypes({
    -      serviceUrl,
    -      userId: session.factors.user.id,
    -    });
    +    const response = await listAuthenticationMethodTypes({ serviceConfig, userId: session.factors.user.id });
         if (response.authMethodTypes && response.authMethodTypes.length) {
           authMethods = response.authMethodTypes;
         }
    @@ -200,19 +188,19 @@ type ClearSessionOptions = {
     
     export async function clearSession(options: ClearSessionOptions) {
       const _headers = await headers();
    -  const { serviceUrl } = getServiceUrlFromHeaders(_headers);
    +  const { serviceConfig } = getServiceConfig(_headers);
     
       const { sessionId } = options;
     
       const sessionCookie = await getSessionCookieById({ sessionId });
     
       const deleteResponse = await deleteSession({
    -    serviceUrl,
    +    serviceConfig,
         sessionId: sessionCookie.id,
         sessionToken: sessionCookie.token,
       });
     
    -  const securitySettings = await getSecuritySettings({ serviceUrl });
    +  const securitySettings = await getSecuritySettings({ serviceConfig });
       const iFrameEnabled = !!securitySettings?.embeddedIframe?.enabled;
     
       if (!deleteResponse) {
    
  • apps/login/src/lib/server/u2f.ts+9 17 modified
    @@ -6,8 +6,8 @@ import { VerifyU2FRegistrationRequestSchema } from "@zitadel/proto/zitadel/user/
     import { headers } from "next/headers";
     import { userAgent } from "next/server";
     import { getSessionCookieById } from "../cookies";
    -import { getServiceUrlFromHeaders } from "../service-url";
    -import { getOriginalHost } from "./host";
    +import { getServiceConfig } from "../service-url";
    +import { getPublicHost } from "./host";
     
     type RegisterU2FCommand = {
       sessionId: string;
    @@ -22,8 +22,8 @@ type VerifyU2FCommand = {
     
     export async function addU2F(command: RegisterU2FCommand) {
       const _headers = await headers();
    -  const { serviceUrl } = getServiceUrlFromHeaders(_headers);
    -  const host = await getOriginalHost();
    +  const { serviceConfig } = getServiceConfig(_headers);
    +  const host = getPublicHost(_headers);
     
       const sessionCookie = await getSessionCookieById({
         sessionId: command.sessionId,
    @@ -33,11 +33,7 @@ export async function addU2F(command: RegisterU2FCommand) {
         return { error: "Could not get session" };
       }
     
    -  const session = await getSession({
    -    serviceUrl,
    -    sessionId: sessionCookie.id,
    -    sessionToken: sessionCookie.token,
    -  });
    +  const session = await getSession({ serviceConfig, sessionId: sessionCookie.id, sessionToken: sessionCookie.token });
     
       const [hostname] = host.split(":");
     
    @@ -51,12 +47,12 @@ export async function addU2F(command: RegisterU2FCommand) {
         return { error: "Could not get session" };
       }
     
    -  return registerU2F({ serviceUrl, userId, domain: hostname });
    +  return registerU2F({ serviceConfig, userId, domain: hostname });
     }
     
     export async function verifyU2F(command: VerifyU2FCommand) {
       const _headers = await headers();
    -  const { serviceUrl } = getServiceUrlFromHeaders(_headers);
    +  const { serviceConfig } = getServiceConfig(_headers);
       let passkeyName = command.passkeyName;
       if (!passkeyName) {
         const headersList = await headers();
    @@ -71,11 +67,7 @@ export async function verifyU2F(command: VerifyU2FCommand) {
         sessionId: command.sessionId,
       });
     
    -  const session = await getSession({
    -    serviceUrl,
    -    sessionId: sessionCookie.id,
    -    sessionToken: sessionCookie.token,
    -  });
    +  const session = await getSession({ serviceConfig, sessionId: sessionCookie.id, sessionToken: sessionCookie.token });
     
       const userId = session?.session?.factors?.user?.id;
     
    @@ -90,5 +82,5 @@ export async function verifyU2F(command: VerifyU2FCommand) {
         userId,
       });
     
    -  return verifyU2FRegistration({ serviceUrl, request });
    +  return verifyU2FRegistration({ serviceConfig, request });
     }
    
  • apps/login/src/lib/server/verify.ts+27 58 modified
    @@ -20,30 +20,26 @@ import { cookies, headers } from "next/headers";
     import { completeFlowOrGetUrl } from "../client";
     import { getSessionCookieByLoginName } from "../cookies";
     import { getOrSetFingerprintId } from "../fingerprint";
    -import { getServiceUrlFromHeaders } from "../service-url";
    +import { getServiceConfig } from "../service-url";
     import { loadMostRecentSession } from "../session";
     import { checkMFAFactors } from "../verify-helper";
     import { createSessionAndUpdateCookie } from "./cookie";
    -import { getOriginalHostWithProtocol } from "./host";
    +import { getPublicHostWithProtocol } from "./host";
     import { getTranslations } from "next-intl/server";
     
     export async function verifyTOTP(code: string, loginName?: string, organization?: string) {
       const _headers = await headers();
    -  const { serviceUrl } = getServiceUrlFromHeaders(_headers);
    +  const { serviceConfig } = getServiceConfig(_headers);
     
       return loadMostRecentSession({
    -    serviceUrl,
    +    serviceConfig,
         sessionParams: {
           loginName,
           organization,
         },
       }).then((session) => {
         if (session?.factors?.user?.id) {
    -      return verifyTOTPRegistration({
    -        serviceUrl,
    -        code,
    -        userId: session.factors.user.id,
    -      });
    +      return verifyTOTPRegistration({ serviceConfig, code, userId: session.factors.user.id });
         } else {
           throw Error("No user id found in session.");
         }
    @@ -62,22 +58,14 @@ type VerifyUserByEmailCommand = {
     export async function sendVerification(command: VerifyUserByEmailCommand) {
       const t = await getTranslations("verify");
       const _headers = await headers();
    -  const { serviceUrl } = getServiceUrlFromHeaders(_headers);
    +  const { serviceConfig } = getServiceConfig(_headers);
     
       const verifyResponse = command.isInvite
    -    ? await verifyInviteCode({
    -        serviceUrl,
    -        userId: command.userId,
    -        verificationCode: command.code,
    -      }).catch((error) => {
    +    ? await verifyInviteCode({ serviceConfig, userId: command.userId, verificationCode: command.code }).catch((error) => {
             console.warn(error);
             return { error: t("errors.couldNotVerifyInvite") };
           })
    -    : await verifyEmail({
    -        serviceUrl,
    -        userId: command.userId,
    -        verificationCode: command.code,
    -      }).catch((error) => {
    +    : await verifyEmail({ serviceConfig, userId: command.userId, verificationCode: command.code }).catch((error) => {
             console.warn(error);
             return { error: t("errors.couldNotVerifyEmail") };
           });
    @@ -91,10 +79,7 @@ export async function sendVerification(command: VerifyUserByEmailCommand) {
       }
     
       let session: Session | undefined;
    -  const userResponse = await getUserByID({
    -    serviceUrl,
    -    userId: command.userId,
    -  });
    +  const userResponse = await getUserByID({ serviceConfig, userId: command.userId });
     
       if (!userResponse || !userResponse.user) {
         return { error: t("errors.couldNotLoadUser") };
    @@ -110,22 +95,17 @@ export async function sendVerification(command: VerifyUserByEmailCommand) {
       });
     
       if (sessionCookie) {
    -    session = await getSession({
    -      serviceUrl,
    -      sessionId: sessionCookie.id,
    -      sessionToken: sessionCookie.token,
    -    }).then((response) => {
    -      if (response?.session) {
    -        return response.session;
    -      }
    -    });
    +    session = await getSession({ serviceConfig, sessionId: sessionCookie.id, sessionToken: sessionCookie.token }).then(
    +      (response) => {
    +        if (response?.session) {
    +          return response.session;
    +        }
    +      },
    +    );
       }
     
       // load auth methods for user
    -  const authMethodResponse = await listAuthenticationMethodTypes({
    -    serviceUrl,
    -    userId: user.userId,
    -  });
    +  const authMethodResponse = await listAuthenticationMethodTypes({ serviceConfig, userId: user.userId });
     
       if (!authMethodResponse || !authMethodResponse.authMethodTypes) {
         return { error: t("errors.couldNotLoadAuthenticators") };
    @@ -203,14 +183,11 @@ export async function sendVerification(command: VerifyUserByEmailCommand) {
         return { redirect: `/verify/success?${verifySuccessParams}` };
       }
     
    -  const loginSettings = await getLoginSettings({
    -    serviceUrl,
    -    organization: user.details?.resourceOwner,
    -  });
    +  const loginSettings = await getLoginSettings({ serviceConfig, organization: user.details?.resourceOwner });
     
       // redirect to mfa factor if user has one, or redirect to set one up
       const mfaFactorCheck = await checkMFAFactors(
    -    serviceUrl,
    +    serviceConfig,
         session,
         loginSettings,
         authMethodResponse.authMethodTypes,
    @@ -253,14 +230,14 @@ type resendVerifyEmailCommand = {
     export async function resendVerification(command: resendVerifyEmailCommand) {
       const t = await getTranslations("verify");
       const _headers = await headers();
    -  const { serviceUrl } = getServiceUrlFromHeaders(_headers);
    -  const hostWithProtocol = await getOriginalHostWithProtocol();
    +  const { serviceConfig } = getServiceConfig(_headers);
    +  const hostWithProtocol = await getPublicHostWithProtocol(_headers);
     
       const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? "";
     
       return command.isInvite
         ? createInviteCode({
    -        serviceUrl,
    +        serviceConfig,
             userId: command.userId,
             urlTemplate:
               `${hostWithProtocol}${basePath}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}&invite=true` +
    @@ -272,8 +249,8 @@ export async function resendVerification(command: resendVerifyEmailCommand) {
             return { error: t("errors.couldNotResendInvite") };
           })
         : zitadelSendEmailCode({
    +        serviceConfig,
             userId: command.userId,
    -        serviceUrl,
             urlTemplate:
               `${hostWithProtocol}${basePath}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}` +
               (command.requestId ? `&requestId=${command.requestId}` : ""),
    @@ -287,22 +264,14 @@ type SendEmailCommand = {
     
     export async function sendEmailCode(command: SendEmailCommand) {
       const _headers = await headers();
    -  const { serviceUrl } = getServiceUrlFromHeaders(_headers);
    +  const { serviceConfig } = getServiceConfig(_headers);
     
    -  return zitadelSendEmailCode({
    -    serviceUrl,
    -    userId: command.userId,
    -    urlTemplate: command.urlTemplate,
    -  });
    +  return zitadelSendEmailCode({ serviceConfig, userId: command.userId, urlTemplate: command.urlTemplate });
     }
     
     export async function sendInviteEmailCode(command: SendEmailCommand) {
       const _headers = await headers();
    -  const { serviceUrl } = getServiceUrlFromHeaders(_headers);
    +  const { serviceConfig } = getServiceConfig(_headers);
     
    -  return createInviteCode({
    -    serviceUrl,
    -    userId: command.userId,
    -    urlTemplate: command.urlTemplate,
    -  });
    +  return createInviteCode({ serviceConfig, userId: command.userId, urlTemplate: command.urlTemplate });
     }
    
  • apps/login/src/lib/service.ts+15 19 modified
    @@ -7,7 +7,8 @@ import { SessionService } from "@zitadel/proto/zitadel/session/v2/session_servic
     import { SettingsService } from "@zitadel/proto/zitadel/settings/v2/settings_service_pb";
     import { UserService } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
     import { systemAPIToken } from "./api";
    -import { createServerTransport } from "./zitadel";
    +import { hasSystemUserCredentials, hasServiceUserToken } from "./deployment";
    +import { createServerTransport, ServiceConfig } from "./zitadel";
     
     type ServiceClass =
       | typeof IdentityProviderService
    @@ -18,32 +19,27 @@ type ServiceClass =
       | typeof SettingsService
       | typeof SAMLService;
     
    -export async function createServiceForHost<T extends ServiceClass>(
    -  service: T,
    -  serviceUrl: string,
    -) {
    +export async function createServiceForHost<T extends ServiceClass>(service: T, serviceConfig: ServiceConfig) {
       let token;
     
    -  // if we are running in a multitenancy context, use the system user token
    -  if (
    -    process.env.AUDIENCE &&
    -    process.env.SYSTEM_USER_ID &&
    -    process.env.SYSTEM_USER_PRIVATE_KEY
    -  ) {
    +  // Determine authentication method based on available credentials
    +  // Prefer system user JWT if available, fallback to service user token
    +  if (hasSystemUserCredentials()) {
         token = await systemAPIToken();
    -  } else if (process.env.ZITADEL_SERVICE_USER_TOKEN) {
    +  } else if (hasServiceUserToken()) {
    +    // Use service user token authentication (self-hosted)
         token = process.env.ZITADEL_SERVICE_USER_TOKEN;
    +  } else {
    +    throw new Error(
    +      "No authentication credentials found. Set either system user credentials (AUDIENCE, SYSTEM_USER_ID, SYSTEM_USER_PRIVATE_KEY) or ZITADEL_SERVICE_USER_TOKEN",
    +    );
       }
     
    -  if (!serviceUrl) {
    -    throw new Error("No instance url found");
    +  if (!serviceConfig) {
    +    throw new Error("No service config found");
       }
     
    -  if (!token) {
    -    throw new Error("No token found");
    -  }
    -
    -  const transport = createServerTransport(token, serviceUrl);
    +  const transport = createServerTransport(token, serviceConfig);
     
       return createClientFor<T>(service)(transport);
     }
    
  • apps/login/src/lib/service-url.test.ts+217 0 added
    @@ -0,0 +1,217 @@
    +import { describe, expect, test, beforeEach, afterEach, vi } from "vitest";
    +import { getServiceConfig, constructUrl } from "./service-url";
    +import { NextRequest } from "next/server";
    +
    +describe("Service URL utilities", () => {
    +  const originalEnv = process.env;
    +
    +  beforeEach(() => {
    +    process.env = { ...originalEnv };
    +  });
    +
    +  afterEach(() => {
    +    process.env = originalEnv;
    +  });
    +
    +  describe("getServiceConfig", () => {
    +    test("should throw when ZITADEL_API_URL is not set", () => {
    +      process.env.ZITADEL_API_URL = undefined as any;
    +
    +      const mockHeaders = {
    +        get: vi.fn(() => null),
    +      } as any;
    +
    +      expect(() => getServiceConfig(mockHeaders)).toThrow("ZITADEL_API_URL is not set");
    +    });
    +
    +    test("should return only baseUrl when x-zitadel-forward-host is not present (self-hosted)", () => {
    +      process.env.ZITADEL_API_URL = "https://zitadel.mycompany.com";
    +
    +      const mockHeaders = {
    +        get: vi.fn((key: string) => {
    +          if (key === "x-zitadel-forward-host") return null;
    +          if (key === "host") return "mycompany.com";
    +          return null;
    +        }),
    +      } as any;
    +
    +      const result = getServiceConfig(mockHeaders);
    +
    +      expect(result.serviceConfig.baseUrl).toBe("https://zitadel.mycompany.com");
    +      expect(result.serviceConfig.instanceHost).toBeUndefined();
    +      expect(result.serviceConfig.publicHost).toBe("mycompany.com");
    +    });
    +
    +    test("should use x-zitadel-forward-host when present (multi-tenant)", () => {
    +      process.env.ZITADEL_API_URL = "https://api.zitadel.cloud";
    +
    +      const mockHeaders = {
    +        get: vi.fn((key: string) => {
    +          if (key === "x-zitadel-forward-host") return "customer.zitadel.cloud";
    +          if (key === "host") return "customer.zitadel.cloud";
    +          return null;
    +        }),
    +      } as any;
    +
    +      const result = getServiceConfig(mockHeaders);
    +
    +      expect(result.serviceConfig.baseUrl).toBe("https://api.zitadel.cloud");
    +      expect(result.serviceConfig.instanceHost).toBe("customer.zitadel.cloud");
    +      expect(result.serviceConfig.publicHost).toBe("customer.zitadel.cloud");
    +    });
    +
    +    test("should strip protocol from instanceHost and publicHost", () => {
    +      process.env.ZITADEL_API_URL = "https://api.zitadel.cloud";
    +
    +      const mockHeaders = {
    +        get: vi.fn((key: string) => {
    +          if (key === "x-zitadel-forward-host") return "https://customer.zitadel.cloud";
    +          if (key === "host") return "customer.zitadel.cloud";
    +          return null;
    +        }),
    +      } as any;
    +
    +      const result = getServiceConfig(mockHeaders);
    +
    +      expect(result.serviceConfig.instanceHost).toBe("customer.zitadel.cloud");
    +      expect(result.serviceConfig.publicHost).toBe("customer.zitadel.cloud");
    +    });
    +
    +    test("should throw when host header is missing", () => {
    +      process.env.ZITADEL_API_URL = "https://api.zitadel.cloud";
    +
    +      const mockHeaders = {
    +        get: vi.fn((key: string) => {
    +          if (key === "x-zitadel-forward-host") return null;
    +          if (key === "host") return null;
    +          return null;
    +        }),
    +      } as any;
    +
    +      expect(() => getServiceConfig(mockHeaders)).toThrow("No host found in headers");
    +    });
    +
    +    test("should handle host with port number", () => {
    +      process.env.ZITADEL_API_URL = "https://api.zitadel.cloud";
    +
    +      const mockHeaders = {
    +        get: vi.fn((key: string) => {
    +          if (key === "x-zitadel-forward-host") return "customer.zitadel.cloud:443";
    +          if (key === "host") return "customer.zitadel.cloud:443";
    +          return null;
    +        }),
    +      } as any;
    +
    +      const result = getServiceConfig(mockHeaders);
    +
    +      expect(result.serviceConfig.publicHost).toBe("customer.zitadel.cloud:443");
    +    });
    +  });
    +
    +  describe("constructUrl", () => {
    +    test("should construct URL with x-zitadel-forward-host when present", () => {
    +      process.env.NEXT_PUBLIC_BASE_PATH = "";
    +      const mockRequest = {
    +        headers: {
    +          get: vi.fn((key: string) => {
    +            if (key === "x-zitadel-forward-host") return "customer.zitadel.cloud";
    +            if (key === "host") return "customer.zitadel.cloud";
    +            return null;
    +          }),
    +        },
    +        nextUrl: {
    +          protocol: "https:",
    +        },
    +      } as any;
    +
    +      const result = constructUrl(mockRequest as NextRequest, "/test");
    +
    +      expect(result.hostname).toBe("customer.zitadel.cloud");
    +      expect(result.pathname).toBe("/test");
    +      expect(result.protocol).toBe("https:");
    +    });
    +
    +    test("should fall back to x-forwarded-host when x-zitadel-forward-host is not present", () => {
    +      process.env.NEXT_PUBLIC_BASE_PATH = "";
    +      const mockRequest = {
    +        headers: {
    +          get: vi.fn((key: string) => {
    +            if (key === "x-zitadel-forward-host") return null;
    +            if (key === "x-forwarded-host") return "mycompany.com";
    +            return null;
    +          }),
    +        },
    +        nextUrl: {
    +          protocol: "https:",
    +        },
    +      } as any;
    +
    +      const result = constructUrl(mockRequest as NextRequest, "/oauth/authorize");
    +
    +      expect(result.hostname).toBe("mycompany.com");
    +      expect(result.pathname).toBe("/oauth/authorize");
    +    });
    +
    +    test("should fall back to host header when no forwarded headers present", () => {
    +      const mockRequest = {
    +        headers: {
    +          get: vi.fn((key: string) => {
    +            if (key === "x-zitadel-forward-host") return null;
    +            if (key === "x-forwarded-host") return null;
    +            if (key === "host") return "localhost:3000";
    +            return null;
    +          }),
    +        },
    +        nextUrl: {
    +          protocol: "http:",
    +        },
    +      } as any;
    +
    +      const result = constructUrl(mockRequest as NextRequest, "/test");
    +
    +      expect(result.hostname).toBe("localhost");
    +      expect(result.port).toBe("3000");
    +    });
    +
    +    test("should use protocol from nextUrl.protocol (not from headers)", () => {
    +      const mockRequest = {
    +        headers: {
    +          get: vi.fn((key: string) => {
    +            // Even if x-forwarded-proto is present, it should be ignored
    +            if (key === "x-forwarded-proto") return "http";
    +            if (key === "host") return "example.com";
    +            return null;
    +          }),
    +        },
    +        nextUrl: {
    +          protocol: "https:", // This should be used
    +        },
    +      } as any;
    +
    +      const result = constructUrl(mockRequest as NextRequest, "/test");
    +
    +      // Should use https: from nextUrl.protocol, not http from header
    +      expect(result.protocol).toBe("https:");
    +    });
    +
    +    test("should include base path when NEXT_PUBLIC_BASE_PATH is set", () => {
    +      process.env.NEXT_PUBLIC_BASE_PATH = "/login";
    +
    +      const mockRequest = {
    +        headers: {
    +          get: vi.fn((key: string) => {
    +            if (key === "host") return "example.com";
    +            return null;
    +          }),
    +        },
    +        nextUrl: {
    +          protocol: "https:",
    +        },
    +      } as any;
    +
    +      const result = constructUrl(mockRequest as NextRequest, "/test");
    +
    +      expect(result.pathname).toBe("/login/test");
    +    });
    +  });
    +});
    
  • apps/login/src/lib/service-url.ts+28 39 modified
    @@ -1,58 +1,47 @@
     import { ReadonlyHeaders } from "next/dist/server/web/spec-extension/adapters/headers";
     import { NextRequest } from "next/server";
    +import { ServiceConfig } from "./zitadel";
    +import { getInstanceHost, getPublicHost } from "./server/host";
     
     /**
    - * Extracts the service url and region from the headers if used in a multitenant context (host, x-zitadel-forward-host header)
    - * or falls back to the ZITADEL_API_URL for a self hosting deployment
    - * or falls back to the host header for a self hosting deployment using custom domains
    - * @param headers
    - * @returns the service url and region from the headers
    - * @throws if the service url could not be determined
    + * Extracts the service URL based on deployment mode and configuration.
      *
    + * Priority:
    + * 1. ZITADEL_API_URL (required) - Used by both self-hosted and multi-tenant
    + * 2. x-zitadel-forward-host (multi-tenant only) - Set by Zitadel proxy
    + * 3. host header (multi-tenant fallback) - For dynamic host resolution
    + *
    + * @param headers - Request headers
    + * @returns Object containing the service Configuration
    + * @throws Error if the service Configuration could not be determined
      */
    -export function getServiceUrlFromHeaders(headers: ReadonlyHeaders): {
    -  serviceUrl: string;
    -} {
    -  let instanceUrl;
     
    -  const forwardedHost = headers.get("x-zitadel-forward-host");
    -  // use the forwarded host if available (multitenant), otherwise fall back to the host of the deployment itself
    -  if (forwardedHost) {
    -    instanceUrl = forwardedHost;
    -    instanceUrl = instanceUrl.startsWith("http://")
    -      ? instanceUrl
    -      : `https://${instanceUrl}`;
    -  } else if (process.env.ZITADEL_API_URL) {
    -    instanceUrl = process.env.ZITADEL_API_URL;
    -  } else {
    -    const host = headers.get("host");
    +function stripProtocol(url: string): string {
    +  return url.replace(/^https?:\/\//, "");
    +}
     
    -    if (host) {
    -      const [hostname] = host.split(":");
    -      if (hostname !== "localhost") {
    -        instanceUrl = host.startsWith("http") ? host : `https://${host}`;
    -      }
    -    }
    +export function getServiceConfig(headers: ReadonlyHeaders): { serviceConfig: ServiceConfig } {
    +  if (!process.env.ZITADEL_API_URL) {
    +    throw new Error("ZITADEL_API_URL is not set");
       }
     
    -  if (!instanceUrl) {
    -    throw new Error("Service URL could not be determined");
    -  }
    +  // use forwarded host from proxy - headers are forwarded to the APIs.
    +  const instanceHost = getInstanceHost(headers);
    +  const publicHost = getPublicHost(headers);
     
       return {
    -    serviceUrl: instanceUrl,
    +    serviceConfig: {
    +      baseUrl: process.env.ZITADEL_API_URL,
    +      ...(instanceHost && { instanceHost: stripProtocol(instanceHost) }),
    +      ...(publicHost && { publicHost: stripProtocol(publicHost) }),
    +    },
       };
     }
     
     export function constructUrl(request: NextRequest, path: string) {
    -  const forwardedProto = request.headers.get("x-forwarded-proto")
    -    ? `${request.headers.get("x-forwarded-proto")}:`
    -    : request.nextUrl.protocol;
    +  const protocol = request.nextUrl.protocol;
     
    -  const forwardedHost =
    -    request.headers.get("x-zitadel-forward-host") ??
    -    request.headers.get("x-forwarded-host") ??
    -    request.headers.get("host");
    +  const forwardedHost = getPublicHost(request.headers);
       const basePath = process.env.NEXT_PUBLIC_BASE_PATH || "";
    -  return new URL(`${basePath}${path}`, `${forwardedProto}//${forwardedHost}`);
    +  return new URL(`${basePath}${path}`, `${protocol}//${forwardedHost}`);
     }
    
  • apps/login/src/lib/session.test.ts+36 36 modified
    @@ -101,7 +101,7 @@ describe("isSessionValid", () => {
             factors: undefined,
           });
     
    -      const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
    +      const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
     
           expect(result).toBe(false);
           expect(consoleSpy).toHaveBeenCalledWith("Session has no user");
    @@ -120,7 +120,7 @@ describe("isSessionValid", () => {
             authMethodTypes: [],
           } as any);
     
    -      const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
    +      const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
     
           expect(result).toBe(false);
           expect(consoleSpy).toHaveBeenCalledWith("Session is expired", expect.any(String));
    @@ -147,7 +147,7 @@ describe("isSessionValid", () => {
             authMethodTypes: [],
           } as any);
     
    -      const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
    +      const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
     
           expect(result).toBe(false);
         });
    @@ -179,7 +179,7 @@ describe("isSessionValid", () => {
             forceMfaLocalOnly: false,
           } as any);
     
    -      const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
    +      const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
     
           expect(result).toBe(true);
         });
    @@ -207,7 +207,7 @@ describe("isSessionValid", () => {
             forceMfaLocalOnly: false,
           } as any);
     
    -      const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
    +      const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
     
           expect(result).toBe(true);
         });
    @@ -242,7 +242,7 @@ describe("isSessionValid", () => {
     
           vi.mocked(verifyHelperModule.shouldEnforceMFA).mockReturnValue(true);
     
    -      const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
    +      const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
     
           expect(result).toBe(false);
           expect(consoleSpy).toHaveBeenCalledWith(
    @@ -278,7 +278,7 @@ describe("isSessionValid", () => {
             authMethodTypes: [AuthenticationMethodType.OTP_EMAIL],
           } as any);
     
    -      const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
    +      const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
     
           expect(result).toBe(true);
         });
    @@ -307,7 +307,7 @@ describe("isSessionValid", () => {
             authMethodTypes: [AuthenticationMethodType.U2F],
           } as any);
     
    -      const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
    +      const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
     
           expect(result).toBe(true);
         });
    @@ -342,7 +342,7 @@ describe("isSessionValid", () => {
             authMethodTypes: [AuthenticationMethodType.TOTP, AuthenticationMethodType.OTP_EMAIL],
           } as any);
     
    -      const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
    +      const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
     
           expect(result).toBe(true);
         });
    @@ -372,7 +372,7 @@ describe("isSessionValid", () => {
     
           vi.mocked(verifyHelperModule.shouldEnforceMFA).mockReturnValue(false);
     
    -      const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
    +      const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
     
           expect(result).toBe(true);
         });
    @@ -409,7 +409,7 @@ describe("isSessionValid", () => {
     
           vi.mocked(verifyHelperModule.shouldEnforceMFA).mockReturnValue(false);
     
    -      const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
    +      const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
     
           expect(result).toBe(true);
         });
    @@ -447,7 +447,7 @@ describe("isSessionValid", () => {
     
           vi.mocked(verifyHelperModule.shouldEnforceMFA).mockReturnValue(true);
     
    -      const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
    +      const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
     
           expect(result).toBe(false);
           expect(consoleSpy).toHaveBeenCalledWith(
    @@ -499,7 +499,7 @@ describe("isSessionValid", () => {
     
           vi.mocked(verifyHelperModule.shouldEnforceMFA).mockReturnValue(false);
     
    -      const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
    +      const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
     
           // This should be true - if it's false, the original bug still exists
           expect(result).toBe(true);
    @@ -539,7 +539,7 @@ describe("isSessionValid", () => {
     
           vi.mocked(verifyHelperModule.shouldEnforceMFA).mockReturnValue(false);
     
    -      const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
    +      const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
     
           // With our fix, this should be true (session is valid)
           // With the old logic, this would have been false (bug)
    @@ -576,7 +576,7 @@ describe("isSessionValid", () => {
     
           vi.mocked(verifyHelperModule.shouldEnforceMFA).mockReturnValue(false);
     
    -      const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
    +      const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
     
           expect(result).toBe(true);
         });
    @@ -611,7 +611,7 @@ describe("isSessionValid", () => {
     
           vi.mocked(verifyHelperModule.shouldEnforceMFA).mockReturnValue(true);
     
    -      const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
    +      const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
     
           expect(result).toBe(false);
           expect(consoleSpy).toHaveBeenCalledWith("Session has no valid multifactor", expect.any(Object));
    @@ -647,7 +647,7 @@ describe("isSessionValid", () => {
             forceMfaLocalOnly: false,
           } as any);
     
    -      const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
    +      const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
     
           expect(result).toBe(true);
         });
    @@ -681,7 +681,7 @@ describe("isSessionValid", () => {
             forceMfaLocalOnly: true,
           } as any);
     
    -      const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
    +      const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
     
           expect(result).toBe(true);
         });
    @@ -716,15 +716,15 @@ describe("isSessionValid", () => {
     
           vi.mocked(verifyHelperModule.shouldEnforceMFA).mockReturnValue(true);
     
    -      const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
    +      const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
     
           expect(result).toBe(false);
           expect(zitadelModule.getLoginSettings).toHaveBeenCalledWith({
    -        serviceUrl: mockServiceUrl,
    +        serviceConfig: { baseUrl: mockServiceUrl },
             organization: mockOrganizationId,
           });
           expect(zitadelModule.listAuthenticationMethodTypes).toHaveBeenCalledWith({
    -        serviceUrl: mockServiceUrl,
    +        serviceConfig: { baseUrl: mockServiceUrl },
             userId: mockUserId,
           });
           consoleSpy.mockRestore();
    @@ -777,7 +777,7 @@ describe("isSessionValid", () => {
             },
           } as any);
     
    -      const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
    +      const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
     
           expect(result).toBe(false);
           expect(consoleSpy).toHaveBeenCalledWith(
    @@ -831,7 +831,7 @@ describe("isSessionValid", () => {
             },
           } as any);
     
    -      const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
    +      const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
     
           expect(result).toBe(true);
         });
    @@ -866,7 +866,7 @@ describe("isSessionValid", () => {
     
           vi.mocked(verifyHelperModule.shouldEnforceMFA).mockReturnValue(false);
     
    -      const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
    +      const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
     
           expect(result).toBe(true);
           // getUserByID should not be called when EMAIL_VERIFICATION is disabled
    @@ -902,7 +902,7 @@ describe("isSessionValid", () => {
             forceMfaLocalOnly: false,
           } as any);
     
    -      const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
    +      const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
     
           expect(result).toBe(true);
         });
    @@ -938,12 +938,12 @@ describe("isSessionValid", () => {
             authMethodTypes: [],
           } as any);
     
    -      const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
    +      const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
     
           expect(result).toBe(true);
           expect(verifyHelperModule.shouldEnforceMFA).toHaveBeenCalledWith(session, expect.any(Object));
           expect(zitadelModule.getLoginSettings).toHaveBeenCalledWith({
    -        serviceUrl: mockServiceUrl,
    +        serviceConfig: { baseUrl: mockServiceUrl },
             organization: mockOrganizationId,
           });
         });
    @@ -979,16 +979,16 @@ describe("isSessionValid", () => {
             authMethodTypes: [AuthenticationMethodType.TOTP, AuthenticationMethodType.OTP_EMAIL],
           } as any);
     
    -      const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
    +      const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
     
           expect(result).toBe(false);
           expect(verifyHelperModule.shouldEnforceMFA).toHaveBeenCalledWith(session, expect.any(Object));
           expect(zitadelModule.getLoginSettings).toHaveBeenCalledWith({
    -        serviceUrl: mockServiceUrl,
    +        serviceConfig: { baseUrl: mockServiceUrl },
             organization: mockOrganizationId,
           });
           expect(zitadelModule.listAuthenticationMethodTypes).toHaveBeenCalledWith({
    -        serviceUrl: mockServiceUrl,
    +        serviceConfig: { baseUrl: mockServiceUrl },
             userId: mockUserId,
           });
         });
    @@ -1024,12 +1024,12 @@ describe("isSessionValid", () => {
             authMethodTypes: [AuthenticationMethodType.TOTP, AuthenticationMethodType.OTP_EMAIL],
           } as any);
     
    -      const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
    +      const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
     
           expect(result).toBe(true);
           expect(verifyHelperModule.shouldEnforceMFA).toHaveBeenCalledWith(session, expect.any(Object));
           expect(zitadelModule.getLoginSettings).toHaveBeenCalledWith({
    -        serviceUrl: mockServiceUrl,
    +        serviceConfig: { baseUrl: mockServiceUrl },
             organization: mockOrganizationId,
           });
           // Should not call listAuthenticationMethodTypes since shouldEnforceMFA returned false
    @@ -1067,7 +1067,7 @@ describe("isSessionValid", () => {
             authMethodTypes: [AuthenticationMethodType.TOTP],
           } as any);
     
    -      const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
    +      const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
     
           expect(result).toBe(true);
         });
    @@ -1105,12 +1105,12 @@ describe("isSessionValid", () => {
             authMethodTypes: [AuthenticationMethodType.TOTP],
           } as any);
     
    -      const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
    +      const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
     
           expect(result).toBe(true);
           expect(verifyHelperModule.shouldEnforceMFA).toHaveBeenCalledWith(session, expect.any(Object));
           expect(zitadelModule.getLoginSettings).toHaveBeenCalledWith({
    -        serviceUrl: mockServiceUrl,
    +        serviceConfig: { baseUrl: mockServiceUrl },
             organization: mockOrganizationId,
           });
           // Should not call listAuthenticationMethodTypes since shouldEnforceMFA returned false
    @@ -1146,7 +1146,7 @@ describe("isSessionValid", () => {
             forceMfaLocalOnly: false,
           } as any);
     
    -      const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
    +      const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
     
           expect(result).toBe(true);
         });
    
  • apps/login/src/lib/session.ts+11 27 modified
    @@ -6,37 +6,30 @@ import { GetSessionResponse } from "@zitadel/proto/zitadel/session/v2/session_se
     import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
     import { getMostRecentCookieWithLoginname } from "./cookies";
     import { shouldEnforceMFA } from "./verify-helper";
    -import { getLoginSettings, getSession, getUserByID, listAuthenticationMethodTypes } from "./zitadel";
    +import { getLoginSettings, getSession, getUserByID, listAuthenticationMethodTypes, ServiceConfig } from "./zitadel";
     
     type LoadMostRecentSessionParams = {
    -  serviceUrl: string;
    +  serviceConfig: ServiceConfig;
       sessionParams: {
         loginName?: string;
         organization?: string;
       };
     };
     
    -export async function loadMostRecentSession({
    -  serviceUrl,
    -  sessionParams,
    -}: LoadMostRecentSessionParams): Promise<Session | undefined> {
    +export async function loadMostRecentSession({ serviceConfig, sessionParams }: LoadMostRecentSessionParams): Promise<Session | undefined> {
       const recent = await getMostRecentCookieWithLoginname({
         loginName: sessionParams.loginName,
         organization: sessionParams.organization,
       });
     
    -  return getSession({
    -    serviceUrl,
    -    sessionId: recent.id,
    -    sessionToken: recent.token,
    -  }).then((resp: GetSessionResponse) => resp.session);
    +  return getSession({ serviceConfig, sessionId: recent.id, sessionToken: recent.token }).then((resp: GetSessionResponse) => resp.session);
     }
     
     /**
      * mfa is required, session is not valid anymore (e.g. session expired, user logged out, etc.)
      * to check for mfa for automatically selected session -> const response = await listAuthenticationMethodTypes(userId);
      **/
    -export async function isSessionValid({ serviceUrl, session }: { serviceUrl: string; session: Session }): Promise<boolean> {
    +export async function isSessionValid({ serviceConfig, session }: { serviceConfig: ServiceConfig; session: Session }): Promise<boolean> {
       // session can't be checked without user
       if (!session.factors?.user) {
         console.warn("Session has no user");
    @@ -51,20 +44,14 @@ export async function isSessionValid({ serviceUrl, session }: { serviceUrl: stri
       const validPasskey = session?.factors?.webAuthN?.verifiedAt;
     
       // Get login settings to determine if MFA is actually required by policy
    -  const loginSettings = await getLoginSettings({
    -    serviceUrl,
    -    organization: session.factors?.user?.organizationId,
    -  });
    +  const loginSettings = await getLoginSettings({ serviceConfig, organization: session.factors?.user?.organizationId });
     
       // Use the existing shouldEnforceMFA function to determine if MFA is required
       const isMfaRequired = shouldEnforceMFA(session, loginSettings);
     
       // Only enforce MFA validation if MFA is required by policy
       if (isMfaRequired) {
    -    const authMethodTypes = await listAuthenticationMethodTypes({
    -      serviceUrl,
    -      userId: session.factors.user.id,
    -    });
    +    const authMethodTypes = await listAuthenticationMethodTypes({ serviceConfig, userId: session.factors.user.id });
     
         const authMethods = authMethodTypes.authMethodTypes;
         // Filter to only MFA methods (exclude PASSWORD and PASSKEY)
    @@ -134,10 +121,7 @@ export async function isSessionValid({ serviceUrl, session }: { serviceUrl: stri
     
       // Check email verification if EMAIL_VERIFICATION environment variable is enabled
       if (process.env.EMAIL_VERIFICATION === "true") {
    -    const userResponse = await getUserByID({
    -      serviceUrl,
    -      userId: session.factors.user.id,
    -    });
    +    const userResponse = await getUserByID({ serviceConfig, userId: session.factors.user.id });
     
         const humanUser = userResponse?.user?.type.case === "human" ? userResponse?.user.type.value : undefined;
     
    @@ -151,12 +135,12 @@ export async function isSessionValid({ serviceUrl, session }: { serviceUrl: stri
     }
     
     export async function findValidSession({
    -  serviceUrl,
    +  serviceConfig,
       sessions,
       authRequest,
       samlRequest,
     }: {
    -  serviceUrl: string;
    +  serviceConfig: ServiceConfig;
       sessions: Session[];
       authRequest?: AuthRequest;
       samlRequest?: SAMLRequest;
    @@ -189,7 +173,7 @@ export async function findValidSession({
     
       // return the first valid session according to settings
       for (const session of sessionsWithHint) {
    -    if (await isSessionValid({ serviceUrl, session })) {
    +    if (await isSessionValid({ serviceConfig, session })) {
           return session;
         }
       }
    
  • apps/login/src/lib/verify-helper.ts+3 6 modified
    @@ -8,7 +8,7 @@ import crypto from "crypto";
     import moment from "moment";
     import { cookies } from "next/headers";
     import { getFingerprintIdCookie } from "./fingerprint";
    -import { getUserByID } from "./zitadel";
    +import { getUserByID, ServiceConfig } from "./zitadel";
     
     export function checkPasswordChangeRequired(
       expirySettings: PasswordExpirySettings | undefined,
    @@ -82,7 +82,7 @@ export function checkEmailVerification(session: Session, humanUser?: HumanUser,
     }
     
     export async function checkMFAFactors(
    -  serviceUrl: string,
    +  serviceConfig: ServiceConfig,
       session: Session,
       loginSettings: LoginSettings | undefined,
       authMethods: AuthenticationMethodType[],
    @@ -179,10 +179,7 @@ export async function checkMFAFactors(
         session?.factors?.user?.id &&
         shouldEnforceMFA(session, loginSettings)
       ) {
    -    const userResponse = await getUserByID({
    -      serviceUrl,
    -      userId: session.factors?.user?.id,
    -    });
    +    const userResponse = await getUserByID({ serviceConfig, userId: session.factors?.user?.id });
     
         const humanUser = userResponse?.user?.type.case === "human" ? userResponse?.user.type.value : undefined;
     
    
  • apps/login/src/lib/zitadel.ts+279 212 modified
    @@ -45,15 +45,14 @@ async function cacheWrapper<T>(callback: Promise<T>) {
     }
     
     export async function getHostedLoginTranslation({
    -  serviceUrl,
    +  serviceConfig,
       organization,
       locale,
    -}: {
    -  serviceUrl: string;
    +}: WithServiceConfig<{
       organization?: string;
       locale?: string;
    -}) {
    -  const settingsService: Client<typeof SettingsService> = await createServiceForHost(SettingsService, serviceUrl);
    +}>) {
    +  const settingsService: Client<typeof SettingsService> = await createServiceForHost(SettingsService, serviceConfig);
     
       const callback = settingsService
         .getHostedLoginTranslation(
    @@ -78,8 +77,13 @@ export async function getHostedLoginTranslation({
       return useCache ? cacheWrapper(callback) : callback;
     }
     
    -export async function getBrandingSettings({ serviceUrl, organization }: { serviceUrl: string; organization?: string }) {
    -  const settingsService: Client<typeof SettingsService> = await createServiceForHost(SettingsService, serviceUrl);
    +export async function getBrandingSettings({
    +  serviceConfig,
    +  organization,
    +}: WithServiceConfig<{
    +  organization?: string;
    +}>) {
    +  const settingsService: Client<typeof SettingsService> = await createServiceForHost(SettingsService, serviceConfig);
     
       const callback = settingsService
         .getBrandingSettings({ ctx: makeReqCtx(organization) }, {})
    @@ -88,8 +92,13 @@ export async function getBrandingSettings({ serviceUrl, organization }: { servic
       return useCache ? cacheWrapper(callback) : callback;
     }
     
    -export async function getLoginSettings({ serviceUrl, organization }: { serviceUrl: string; organization?: string }) {
    -  const settingsService: Client<typeof SettingsService> = await createServiceForHost(SettingsService, serviceUrl);
    +export async function getLoginSettings({
    +  serviceConfig,
    +  organization,
    +}: WithServiceConfig<{
    +  organization?: string;
    +}>) {
    +  const settingsService: Client<typeof SettingsService> = await createServiceForHost(SettingsService, serviceConfig);
     
       const callback = settingsService
         .getLoginSettings({ ctx: makeReqCtx(organization) }, {})
    @@ -98,16 +107,16 @@ export async function getLoginSettings({ serviceUrl, organization }: { serviceUr
       return useCache ? cacheWrapper(callback) : callback;
     }
     
    -export async function getSecuritySettings({ serviceUrl }: { serviceUrl: string }) {
    -  const settingsService: Client<typeof SettingsService> = await createServiceForHost(SettingsService, serviceUrl);
    +export async function getSecuritySettings({ serviceConfig }: WithServiceConfig) {
    +  const settingsService: Client<typeof SettingsService> = await createServiceForHost(SettingsService, serviceConfig);
     
       const callback = settingsService.getSecuritySettings({}).then((resp) => (resp.settings ? resp.settings : undefined));
     
       return useCache ? cacheWrapper(callback) : callback;
     }
     
    -export async function getLockoutSettings({ serviceUrl, orgId }: { serviceUrl: string; orgId?: string }) {
    -  const settingsService: Client<typeof SettingsService> = await createServiceForHost(SettingsService, serviceUrl);
    +export async function getLockoutSettings({ serviceConfig, orgId }: WithServiceConfig<{ orgId?: string }>) {
    +  const settingsService: Client<typeof SettingsService> = await createServiceForHost(SettingsService, serviceConfig);
     
       const callback = settingsService
         .getLockoutSettings({ ctx: makeReqCtx(orgId) }, {})
    @@ -116,8 +125,8 @@ export async function getLockoutSettings({ serviceUrl, orgId }: { serviceUrl: st
       return useCache ? cacheWrapper(callback) : callback;
     }
     
    -export async function getPasswordExpirySettings({ serviceUrl, orgId }: { serviceUrl: string; orgId?: string }) {
    -  const settingsService: Client<typeof SettingsService> = await createServiceForHost(SettingsService, serviceUrl);
    +export async function getPasswordExpirySettings({ serviceConfig, orgId }: WithServiceConfig<{ orgId?: string }>) {
    +  const settingsService: Client<typeof SettingsService> = await createServiceForHost(SettingsService, serviceConfig);
     
       const callback = settingsService
         .getPasswordExpirySettings({ ctx: makeReqCtx(orgId) }, {})
    @@ -126,46 +135,45 @@ export async function getPasswordExpirySettings({ serviceUrl, orgId }: { service
       return useCache ? cacheWrapper(callback) : callback;
     }
     
    -export async function listIDPLinks({ serviceUrl, userId }: { serviceUrl: string; userId: string }) {
    -  const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceUrl);
    +export async function listIDPLinks({ serviceConfig, userId }: WithServiceConfig<{ userId: string }>) {
    +  const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceConfig);
     
       return userService.listIDPLinks({ userId }, {});
     }
     
    -export async function addOTPEmail({ serviceUrl, userId }: { serviceUrl: string; userId: string }) {
    -  const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceUrl);
    +export async function addOTPEmail({ serviceConfig, userId }: WithServiceConfig<{ userId: string }>) {
    +  const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceConfig);
     
       return userService.addOTPEmail({ userId }, {});
     }
     
    -export async function addOTPSMS({ serviceUrl, userId }: { serviceUrl: string; userId: string }) {
    -  const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceUrl);
    +export async function addOTPSMS({ serviceConfig, userId }: WithServiceConfig<{ userId: string }>) {
    +  const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceConfig);
     
       return userService.addOTPSMS({ userId }, {});
     }
     
    -export async function registerTOTP({ serviceUrl, userId }: { serviceUrl: string; userId: string }) {
    -  const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceUrl);
    +export async function registerTOTP({ serviceConfig, userId }: WithServiceConfig<{ userId: string }>) {
    +  const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceConfig);
     
       return userService.registerTOTP({ userId }, {});
     }
     
    -export async function getGeneralSettings({ serviceUrl }: { serviceUrl: string }) {
    -  const settingsService: Client<typeof SettingsService> = await createServiceForHost(SettingsService, serviceUrl);
    +export async function getGeneralSettings({ serviceConfig }: WithServiceConfig) {
    +  const settingsService: Client<typeof SettingsService> = await createServiceForHost(SettingsService, serviceConfig);
     
       const callback = settingsService.getGeneralSettings({}, {}).then((resp) => resp.supportedLanguages);
     
       return useCache ? cacheWrapper(callback) : callback;
     }
     
     export async function getLegalAndSupportSettings({
    -  serviceUrl,
    +  serviceConfig,
       organization,
    -}: {
    -  serviceUrl: string;
    +}: WithServiceConfig<{
       organization?: string;
    -}) {
    -  const settingsService: Client<typeof SettingsService> = await createServiceForHost(SettingsService, serviceUrl);
    +}>) {
    +  const settingsService: Client<typeof SettingsService> = await createServiceForHost(SettingsService, serviceConfig);
     
       const callback = settingsService
         .getLegalAndSupportSettings({ ctx: makeReqCtx(organization) }, {})
    @@ -175,13 +183,12 @@ export async function getLegalAndSupportSettings({
     }
     
     export async function getPasswordComplexitySettings({
    -  serviceUrl,
    +  serviceConfig,
       organization,
    -}: {
    -  serviceUrl: string;
    +}: WithServiceConfig<{
       organization?: string;
    -}) {
    -  const settingsService: Client<typeof SettingsService> = await createServiceForHost(SettingsService, serviceUrl);
    +}>) {
    +  const settingsService: Client<typeof SettingsService> = await createServiceForHost(SettingsService, serviceConfig);
     
       const callback = settingsService
         .getPasswordComplexitySettings({ ctx: makeReqCtx(organization) })
    @@ -191,37 +198,35 @@ export async function getPasswordComplexitySettings({
     }
     
     export async function createSessionFromChecks({
    -  serviceUrl,
    +  serviceConfig,
       checks,
       lifetime,
    -}: {
    -  serviceUrl: string;
    +}: WithServiceConfig<{
       checks: Checks;
       lifetime: Duration;
    -}) {
    -  const sessionService: Client<typeof SessionService> = await createServiceForHost(SessionService, serviceUrl);
    +}>) {
    +  const sessionService: Client<typeof SessionService> = await createServiceForHost(SessionService, serviceConfig);
     
       const userAgent = await getUserAgent();
     
       return sessionService.createSession({ checks, lifetime, userAgent }, {});
     }
     
     export async function createSessionForUserIdAndIdpIntent({
    -  serviceUrl,
    +  serviceConfig,
       userId,
       idpIntent,
       lifetime,
    -}: {
    -  serviceUrl: string;
    +}: WithServiceConfig<{
       userId: string;
       idpIntent: {
         idpIntentId?: string | undefined;
         idpIntentToken?: string | undefined;
       };
       lifetime: Duration;
    -}) {
    +}>) {
       console.log("Creating session for userId and IDP intent", { userId, idpIntent, lifetime });
    -  const sessionService: Client<typeof SessionService> = await createServiceForHost(SessionService, serviceUrl);
    +  const sessionService: Client<typeof SessionService> = await createServiceForHost(SessionService, serviceConfig);
     
       const userAgent = await getUserAgent();
     
    @@ -241,21 +246,20 @@ export async function createSessionForUserIdAndIdpIntent({
     }
     
     export async function setSession({
    -  serviceUrl,
    +  serviceConfig,
       sessionId,
       sessionToken,
       challenges,
       checks,
       lifetime,
    -}: {
    -  serviceUrl: string;
    +}: WithServiceConfig<{
       sessionId: string;
       sessionToken: string;
       challenges: RequestChallenges | undefined;
       checks?: Checks;
       lifetime: Duration;
    -}) {
    -  const sessionService: Client<typeof SessionService> = await createServiceForHost(SessionService, serviceUrl);
    +}>) {
    +  const sessionService: Client<typeof SessionService> = await createServiceForHost(SessionService, serviceConfig);
     
       return sessionService.setSession(
         {
    @@ -271,40 +275,37 @@ export async function setSession({
     }
     
     export async function getSession({
    -  serviceUrl,
    +  serviceConfig,
       sessionId,
       sessionToken,
    -}: {
    -  serviceUrl: string;
    +}: WithServiceConfig<{
       sessionId: string;
       sessionToken: string;
    -}) {
    -  const sessionService: Client<typeof SessionService> = await createServiceForHost(SessionService, serviceUrl);
    +}>) {
    +  const sessionService: Client<typeof SessionService> = await createServiceForHost(SessionService, serviceConfig);
     
       return sessionService.getSession({ sessionId, sessionToken }, {});
     }
     
     export async function deleteSession({
    -  serviceUrl,
    +  serviceConfig,
       sessionId,
       sessionToken,
    -}: {
    -  serviceUrl: string;
    +}: WithServiceConfig<{
       sessionId: string;
       sessionToken: string;
    -}) {
    -  const sessionService: Client<typeof SessionService> = await createServiceForHost(SessionService, serviceUrl);
    +}>) {
    +  const sessionService: Client<typeof SessionService> = await createServiceForHost(SessionService, serviceConfig);
     
       return sessionService.deleteSession({ sessionId, sessionToken }, {});
     }
     
    -type ListSessionsCommand = {
    -  serviceUrl: string;
    +type ListSessionsCommand = WithServiceConfig<{
       ids: string[];
    -};
    +}>;
     
    -export async function listSessions({ serviceUrl, ids }: ListSessionsCommand) {
    -  const sessionService: Client<typeof SessionService> = await createServiceForHost(SessionService, serviceUrl);
    +export async function listSessions({ serviceConfig, ids }: ListSessionsCommand) {
    +  const sessionService: Client<typeof SessionService> = await createServiceForHost(SessionService, serviceConfig);
     
       return sessionService.listSessions(
         {
    @@ -321,17 +322,16 @@ export async function listSessions({ serviceUrl, ids }: ListSessionsCommand) {
       );
     }
     
    -export type AddHumanUserData = {
    -  serviceUrl: string;
    +export type AddHumanUserData = WithServiceConfig<{
       firstName: string;
       lastName: string;
       email: string;
       password?: string;
       organization: string;
    -};
    +}>;
     
    -export async function addHumanUser({ serviceUrl, email, firstName, lastName, password, organization }: AddHumanUserData) {
    -  const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceUrl);
    +export async function addHumanUser({ serviceConfig, email, firstName, lastName, password, organization }: AddHumanUserData) {
    +  const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceConfig);
     
       let addHumanUserRequest: AddHumanUserRequest = create(AddHumanUserRequestSchema, {
         email: {
    @@ -360,67 +360,69 @@ export async function addHumanUser({ serviceUrl, email, firstName, lastName, pas
       return userService.addHumanUser(addHumanUserRequest);
     }
     
    -export async function addHuman({ serviceUrl, request }: { serviceUrl: string; request: AddHumanUserRequest }) {
    -  const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceUrl);
    +export async function addHuman({ serviceConfig, request }: WithServiceConfig<{ request: AddHumanUserRequest }>) {
    +  const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceConfig);
     
       return userService.addHumanUser(request);
     }
     
    -export async function updateHuman({ serviceUrl, request }: { serviceUrl: string; request: UpdateHumanUserRequest }) {
    -  const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceUrl);
    +export async function updateHuman({
    +  serviceConfig,
    +  request,
    +}: WithServiceConfig<{
    +  request: UpdateHumanUserRequest;
    +}>) {
    +  const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceConfig);
     
       return userService.updateHumanUser(request);
     }
     
     export async function verifyTOTPRegistration({
    -  serviceUrl,
    +  serviceConfig,
       code,
       userId,
    -}: {
    -  serviceUrl: string;
    +}: WithServiceConfig<{
       code: string;
       userId: string;
    -}) {
    -  const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceUrl);
    +}>) {
    +  const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceConfig);
     
       return userService.verifyTOTPRegistration({ code, userId }, {});
     }
     
    -export async function getUserByID({ serviceUrl, userId }: { serviceUrl: string; userId: string }) {
    -  const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceUrl);
    +export async function getUserByID({ serviceConfig, userId }: WithServiceConfig<{ userId: string }>) {
    +  const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceConfig);
     
       return userService.getUserByID({ userId }, {});
     }
     
    -export async function humanMFAInitSkipped({ serviceUrl, userId }: { serviceUrl: string; userId: string }) {
    -  const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceUrl);
    +export async function humanMFAInitSkipped({ serviceConfig, userId }: WithServiceConfig<{ userId: string }>) {
    +  const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceConfig);
     
       return userService.humanMFAInitSkipped({ userId }, {});
     }
     
     export async function verifyInviteCode({
    -  serviceUrl,
    +  serviceConfig,
       userId,
       verificationCode,
    -}: {
    -  serviceUrl: string;
    +}: WithServiceConfig<{
       userId: string;
       verificationCode: string;
    -}) {
    -  const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceUrl);
    +}>) {
    +  const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceConfig);
     
       return userService.verifyInviteCode({ userId, verificationCode }, {});
     }
     
     export async function sendEmailCode({
    -  serviceUrl,
    +  serviceConfig,
       userId,
       urlTemplate,
    -}: {
    -  serviceUrl: string;
    +}: WithServiceConfig<{
       userId: string;
       urlTemplate: string;
    -}) {
    +}>) {
       let medium = create(SendEmailCodeRequestSchema, { userId });
     
       medium = create(SendEmailCodeRequestSchema, {
    @@ -433,20 +435,19 @@ export async function sendEmailCode({
         },
       });
     
    -  const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceUrl);
    +  const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceConfig);
     
       return userService.sendEmailCode(medium, {});
     }
     
     export async function createInviteCode({
    -  serviceUrl,
    +  serviceConfig,
       urlTemplate,
       userId,
    -}: {
    -  serviceUrl: string;
    +}: WithServiceConfig<{
       urlTemplate: string;
       userId: string;
    -}) {
    +}>) {
       let medium = create(SendInviteCodeSchema, {
         applicationName: process.env.NEXT_PUBLIC_APPLICATION_NAME || "Zitadel Login",
       });
    @@ -456,7 +457,7 @@ export async function createInviteCode({
         urlTemplate,
       };
     
    -  const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceUrl);
    +  const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceConfig);
     
       return userService.createInviteCode(
         {
    @@ -470,16 +471,15 @@ export async function createInviteCode({
       );
     }
     
    -export type ListUsersCommand = {
    -  serviceUrl: string;
    +export type ListUsersCommand = WithServiceConfig<{
       loginName?: string;
       userName?: string;
       email?: string;
       phone?: string;
       organizationId?: string;
    -};
    +}>;
     
    -export async function listUsers({ serviceUrl, loginName, userName, phone, email, organizationId }: ListUsersCommand) {
    +export async function listUsers({ serviceConfig, loginName, userName, phone, email, organizationId }: ListUsersCommand) {
       const queries: SearchQuery[] = [];
     
       // either use loginName or userName, email, phone
    @@ -562,18 +562,17 @@ export async function listUsers({ serviceUrl, loginName, userName, phone, email,
         );
       }
     
    -  const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceUrl);
    +  const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceConfig);
     
       return userService.listUsers({ queries });
     }
     
    -export type SearchUsersCommand = {
    -  serviceUrl: string;
    +export type SearchUsersCommand = WithServiceConfig<{
       searchValue: string;
       loginSettings: LoginSettings;
       organizationId?: string;
       suffix?: string;
    -};
    +}>;
     
     const PhoneQuery = (searchValue: string) =>
       create(SearchQuerySchema, {
    @@ -612,7 +611,13 @@ const EmailQuery = (searchValue: string) =>
      * this is a dedicated search function to search for users from the loginname page
      * it searches users based on the loginName or userName and org suffix combination, and falls back to email and phone if no users are found
      *  */
    -export async function searchUsers({ serviceUrl, searchValue, loginSettings, organizationId, suffix }: SearchUsersCommand) {
    +export async function searchUsers({
    +  serviceConfig,
    +  searchValue,
    +  loginSettings,
    +  organizationId,
    +  suffix,
    +}: SearchUsersCommand) {
       const queries: SearchQuery[] = [];
     
       const t = await getTranslations("zitadel");
    @@ -640,7 +645,7 @@ export async function searchUsers({ serviceUrl, searchValue, loginSettings, orga
         );
       }
     
    -  const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceUrl);
    +  const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceConfig);
     
       const loginNameResult = await userService.listUsers({ queries });
     
    @@ -723,8 +728,8 @@ export async function searchUsers({ serviceUrl, searchValue, loginSettings, orga
       return { result: [] };
     }
     
    -export async function getDefaultOrg({ serviceUrl }: { serviceUrl: string }): Promise<Organization | null> {
    -  const orgService: Client<typeof OrganizationService> = await createServiceForHost(OrganizationService, serviceUrl);
    +export async function getDefaultOrg({ serviceConfig }: WithServiceConfig): Promise<Organization | null> {
    +  const orgService: Client<typeof OrganizationService> = await createServiceForHost(OrganizationService, serviceConfig);
     
       return orgService
         .listOrganizations(
    @@ -743,8 +748,8 @@ export async function getDefaultOrg({ serviceUrl }: { serviceUrl: string }): Pro
         .then((resp) => (resp?.result && resp.result[0] ? resp.result[0] : null));
     }
     
    -export async function getOrgsByDomain({ serviceUrl, domain }: { serviceUrl: string; domain: string }) {
    -  const orgService: Client<typeof OrganizationService> = await createServiceForHost(OrganizationService, serviceUrl);
    +export async function getOrgsByDomain({ serviceConfig, domain }: WithServiceConfig<{ domain: string }>) {
    +  const orgService: Client<typeof OrganizationService> = await createServiceForHost(OrganizationService, serviceConfig);
     
       return orgService.listOrganizations(
         {
    @@ -762,15 +767,15 @@ export async function getOrgsByDomain({ serviceUrl, domain }: { serviceUrl: stri
     }
     
     export async function startIdentityProviderFlow({
    -  serviceUrl,
    +  serviceConfig,
       idpId,
       urls,
    -}: {
    -  serviceUrl: string;
    +}: WithServiceConfig<{
       idpId: string;
       urls: RedirectURLsJson;
    -}): Promise<string | null> {
    -  const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceUrl);
    +}>): Promise<string | null> {
    +    // Use empty publicHost to avoid issues with redirect URIs pointing to the login UI instead of the zitadel API
    +    const userService: Client<typeof UserService> = await createServiceForHost(UserService, {...serviceConfig, publicHost: ''});
     
       return userService
         .startIdentityProviderIntent({
    @@ -823,17 +828,16 @@ export async function startIdentityProviderFlow({
     }
     
     export async function startLDAPIdentityProviderFlow({
    -  serviceUrl,
    +  serviceConfig,
       idpId,
       username,
       password,
    -}: {
    -  serviceUrl: string;
    +}: WithServiceConfig<{
       idpId: string;
       username: string;
       password: string;
    -}) {
    -  const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceUrl);
    +}>) {
    +  const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceConfig);
     
       return userService.startIdentityProviderIntent({
         idpId,
    @@ -847,32 +851,41 @@ export async function startLDAPIdentityProviderFlow({
       });
     }
     
    -export async function getAuthRequest({ serviceUrl, authRequestId }: { serviceUrl: string; authRequestId: string }) {
    -  const oidcService = await createServiceForHost(OIDCService, serviceUrl);
    +export async function getAuthRequest({
    +  serviceConfig,
    +  authRequestId,
    +}: WithServiceConfig<{
    +  authRequestId: string;
    +}>) {
    +  const oidcService = await createServiceForHost(OIDCService, serviceConfig);
     
       return oidcService.getAuthRequest({
         authRequestId,
       });
     }
     
    -export async function getDeviceAuthorizationRequest({ serviceUrl, userCode }: { serviceUrl: string; userCode: string }) {
    -  const oidcService = await createServiceForHost(OIDCService, serviceUrl);
    +export async function getDeviceAuthorizationRequest({
    +  serviceConfig,
    +  userCode,
    +}: WithServiceConfig<{
    +  userCode: string;
    +}>) {
    +  const oidcService = await createServiceForHost(OIDCService, serviceConfig);
     
       return oidcService.getDeviceAuthorizationRequest({
         userCode,
       });
     }
     
     export async function authorizeOrDenyDeviceAuthorization({
    -  serviceUrl,
    +  serviceConfig,
       deviceAuthorizationId,
       session,
    -}: {
    -  serviceUrl: string;
    +}: WithServiceConfig<{
       deviceAuthorizationId: string;
       session?: { sessionId: string; sessionToken: string };
    -}) {
    -  const oidcService = await createServiceForHost(OIDCService, serviceUrl);
    +}>) {
    +  const oidcService = await createServiceForHost(OIDCService, serviceConfig);
     
       return oidcService.authorizeOrDenyDeviceAuthorization({
         deviceAuthorizationId,
    @@ -888,36 +901,40 @@ export async function authorizeOrDenyDeviceAuthorization({
       });
     }
     
    -export async function createCallback({ serviceUrl, req }: { serviceUrl: string; req: CreateCallbackRequest }) {
    -  const oidcService = await createServiceForHost(OIDCService, serviceUrl);
    +export async function createCallback({ serviceConfig, req }: WithServiceConfig<{ req: CreateCallbackRequest }>) {
    +  const oidcService = await createServiceForHost(OIDCService, serviceConfig);
     
       return oidcService.createCallback(req);
     }
     
    -export async function getSAMLRequest({ serviceUrl, samlRequestId }: { serviceUrl: string; samlRequestId: string }) {
    -  const samlService = await createServiceForHost(SAMLService, serviceUrl);
    +export async function getSAMLRequest({
    +  serviceConfig,
    +  samlRequestId,
    +}: WithServiceConfig<{
    +  samlRequestId: string;
    +}>) {
    +  const samlService = await createServiceForHost(SAMLService, serviceConfig);
     
       return samlService.getSAMLRequest({
         samlRequestId,
       });
     }
     
    -export async function createResponse({ serviceUrl, req }: { serviceUrl: string; req: CreateResponseRequest }) {
    -  const samlService = await createServiceForHost(SAMLService, serviceUrl);
    +export async function createResponse({ serviceConfig, req }: WithServiceConfig<{ req: CreateResponseRequest }>) {
    +  const samlService = await createServiceForHost(SAMLService, serviceConfig);
     
       return samlService.createResponse(req);
     }
     
     export async function verifyEmail({
    -  serviceUrl,
    +  serviceConfig,
       userId,
       verificationCode,
    -}: {
    -  serviceUrl: string;
    +}: WithServiceConfig<{
       userId: string;
       verificationCode: string;
    -}) {
    -  const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceUrl);
    +}>) {
    +  const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceConfig);
     
       return userService.verifyEmail(
         {
    @@ -929,14 +946,13 @@ export async function verifyEmail({
     }
     
     export async function resendEmailCode({
    -  serviceUrl,
    +  serviceConfig,
       userId,
       urlTemplate,
    -}: {
    -  serviceUrl: string;
    +}: WithServiceConfig<{
       userId: string;
       urlTemplate: string;
    -}) {
    +}>) {
       let request: ResendEmailCodeRequest = create(ResendEmailCodeRequestSchema, {
         userId,
       });
    @@ -947,33 +963,42 @@ export async function resendEmailCode({
     
       request = { ...request, verification: { case: "sendCode", value: medium } };
     
    -  const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceUrl);
    +  const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceConfig);
     
       return userService.resendEmailCode(request, {});
     }
     
    -export async function retrieveIDPIntent({ serviceUrl, id, token }: { serviceUrl: string; id: string; token: string }) {
    -  const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceUrl);
    +export async function retrieveIDPIntent({
    +  serviceConfig,
    +  id,
    +  token,
    +}: WithServiceConfig<{
    +  id: string;
    +  token: string;
    +}>) {
    +  const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceConfig);
     
       return userService.retrieveIdentityProviderIntent({ idpIntentId: id, idpIntentToken: token }, {});
     }
     
    -export async function getIDPByID({ serviceUrl, id }: { serviceUrl: string; id: string }) {
    -  const idpService: Client<typeof IdentityProviderService> = await createServiceForHost(IdentityProviderService, serviceUrl);
    +export async function getIDPByID({ serviceConfig, id }: WithServiceConfig<{ id: string }>) {
    +  const idpService: Client<typeof IdentityProviderService> = await createServiceForHost(
    +    IdentityProviderService,
    +    serviceConfig,
    +  );
     
       return idpService.getIDPByID({ id }, {}).then((resp) => resp.idp);
     }
     
     export async function addIDPLink({
    -  serviceUrl,
    +  serviceConfig,
       idp,
       userId,
    -}: {
    -  serviceUrl: string;
    +}: WithServiceConfig<{
       idp: { id: string; userId: string; userName: string };
       userId: string;
    -}) {
    -  const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceUrl);
    +}>) {
    +  const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceConfig);
     
       return userService.addIDPLink(
         {
    @@ -989,14 +1014,13 @@ export async function addIDPLink({
     }
     
     export async function passwordReset({
    -  serviceUrl,
    +  serviceConfig,
       userId,
       urlTemplate,
    -}: {
    -  serviceUrl: string;
    +}: WithServiceConfig<{
       userId: string;
       urlTemplate?: string;
    -}) {
    +}>) {
       let medium = create(SendPasswordResetLinkSchema, {
         notificationType: NotificationType.Email,
       });
    @@ -1006,7 +1030,7 @@ export async function passwordReset({
         urlTemplate,
       };
     
    -  const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceUrl);
    +  const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceConfig);
     
       return userService.passwordReset(
         {
    @@ -1021,16 +1045,15 @@ export async function passwordReset({
     }
     
     export async function setUserPassword({
    -  serviceUrl,
    +  serviceConfig,
       userId,
       password,
       code,
    -}: {
    -  serviceUrl: string;
    +}: WithServiceConfig<{
       userId: string;
       password: string;
       code?: string;
    -}) {
    +}>) {
       let payload = create(SetPasswordRequestSchema, {
         userId,
         newPassword: {
    @@ -1048,7 +1071,7 @@ export async function setUserPassword({
         };
       }
     
    -  const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceUrl);
    +  const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceConfig);
     
       return userService.setPassword(payload, {}).catch((error) => {
         // throw error if failed precondition (ex. User is not yet initialized)
    @@ -1060,8 +1083,13 @@ export async function setUserPassword({
       });
     }
     
    -export async function setPassword({ serviceUrl, payload }: { serviceUrl: string; payload: SetPasswordRequest }) {
    -  const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceUrl);
    +export async function setPassword({
    +  serviceConfig,
    +  payload,
    +}: WithServiceConfig<{
    +  payload: SetPasswordRequest;
    +}>) {
    +  const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceConfig);
     
       return userService.setPassword(payload, {});
     }
    @@ -1072,8 +1100,13 @@ export async function setPassword({ serviceUrl, payload }: { serviceUrl: string;
      * @param userId the id of the user where the email should be set
      * @returns the newly set email
      */
    -export async function createPasskeyRegistrationLink({ serviceUrl, userId }: { serviceUrl: string; userId: string }) {
    -  const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceUrl);
    +export async function createPasskeyRegistrationLink({
    +  serviceConfig,
    +  userId,
    +}: WithServiceConfig<{
    +  userId: string;
    +}>) {
    +  const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceConfig);
     
       return userService.createPasskeyRegistrationLink({
         userId,
    @@ -1091,8 +1124,15 @@ export async function createPasskeyRegistrationLink({ serviceUrl, userId }: { se
      * @param domain the domain on which the factor is registered
      * @returns the newly set email
      */
    -export async function registerU2F({ serviceUrl, userId, domain }: { serviceUrl: string; userId: string; domain: string }) {
    -  const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceUrl);
    +export async function registerU2F({
    +  serviceConfig,
    +  userId,
    +  domain,
    +}: WithServiceConfig<{
    +  userId: string;
    +  domain: string;
    +}>) {
    +  const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceConfig);
     
       return userService.registerU2F({
         userId,
    @@ -1107,13 +1147,12 @@ export async function registerU2F({ serviceUrl, userId, domain }: { serviceUrl:
      * @returns the result of the verification
      */
     export async function verifyU2FRegistration({
    -  serviceUrl,
    +  serviceConfig,
       request,
    -}: {
    -  serviceUrl: string;
    +}: WithServiceConfig<{
       request: VerifyU2FRegistrationRequest;
    -}) {
    -  const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceUrl);
    +}>) {
    +  const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceConfig);
     
       return userService.verifyU2FRegistration(request, {});
     }
    @@ -1126,19 +1165,18 @@ export async function verifyU2FRegistration({
      * @returns the active identity providers
      */
     export async function getActiveIdentityProviders({
    -  serviceUrl,
    +  serviceConfig,
       orgId,
       linking_allowed,
    -}: {
    -  serviceUrl: string;
    +}: WithServiceConfig<{
       orgId?: string;
       linking_allowed?: boolean;
    -}) {
    +}>) {
       const props: any = { ctx: makeReqCtx(orgId) };
       if (linking_allowed) {
         props.linkingAllowed = linking_allowed;
       }
    -  const settingsService: Client<typeof SettingsService> = await createServiceForHost(SettingsService, serviceUrl);
    +  const settingsService: Client<typeof SettingsService> = await createServiceForHost(SettingsService, serviceConfig);
     
       return settingsService.getActiveIdentityProviders(props, {});
     }
    @@ -1150,13 +1188,12 @@ export async function getActiveIdentityProviders({
      * @returns the result of the verification
      */
     export async function verifyPasskeyRegistration({
    -  serviceUrl,
    +  serviceConfig,
       request,
    -}: {
    -  serviceUrl: string;
    +}: WithServiceConfig<{
       request: VerifyPasskeyRegistrationRequest;
    -}) {
    -  const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceUrl);
    +}>) {
    +  const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceConfig);
     
       return userService.verifyPasskeyRegistration(request, {});
     }
    @@ -1170,17 +1207,16 @@ export async function verifyPasskeyRegistration({
      * @returns the newly set email
      */
     export async function registerPasskey({
    -  serviceUrl,
    +  serviceConfig,
       userId,
       code,
       domain,
    -}: {
    -  serviceUrl: string;
    +}: WithServiceConfig<{
       userId: string;
       code: { id: string; code: string };
       domain: string;
    -}) {
    -  const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceUrl);
    +}>) {
    +  const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceConfig);
     
       return userService.registerPasskey({
         userId,
    @@ -1195,33 +1231,64 @@ export async function registerPasskey({
      * @param userId the id of the user where the email should be set
      * @returns the list of authentication method types
      */
    -export async function listAuthenticationMethodTypes({ serviceUrl, userId }: { serviceUrl: string; userId: string }) {
    -  const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceUrl);
    +export async function listAuthenticationMethodTypes({
    +  serviceConfig,
    +  userId,
    +}: WithServiceConfig<{
    +  userId: string;
    +}>) {
    +  const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceConfig);
     
       return userService.listAuthenticationMethodTypes({
         userId,
       });
     }
     
    -export function createServerTransport(token: string, baseUrl: string) {
    +export interface ServiceConfig {
    +  baseUrl: string;
    +  instanceHost?: string; // only for multi-tenant
    +  publicHost?: string; // only for multi-tenant
    +}
    +
    +/**
    + * Base type that all function parameters must extend to ensure serviceConfig is always required
    + */
    +export type WithServiceConfig<T = {}> = T & {
    +  serviceConfig: ServiceConfig;
    +};
    +
    +export function createServerTransport(token: string, serviceConfig: ServiceConfig) {
       return libCreateServerTransport(token, {
    -    baseUrl,
    -    interceptors: !process.env.CUSTOM_REQUEST_HEADERS
    -      ? undefined
    -      : [
    -          (next) => {
    -            return (req) => {
    -              process.env.CUSTOM_REQUEST_HEADERS!.split(",").forEach((header) => {
    -                const kv = header.indexOf(":");
    -                if (kv > 0) {
    -                  req.header.set(header.slice(0, kv).trim(), header.slice(kv + 1).trim());
    -                } else {
    -                  console.warn(`Skipping malformed header: ${header}`);
    +    baseUrl: serviceConfig.baseUrl,
    +    interceptors:
    +      !process.env.CUSTOM_REQUEST_HEADERS && !serviceConfig.instanceHost && !serviceConfig.publicHost
    +        ? undefined
    +        : [
    +            (next) => {
    +              return (req) => {
    +                // Apply headers from serviceConfig
    +                if (serviceConfig.instanceHost) {
    +                  req.header.set("x-zitadel-instance-host", serviceConfig.instanceHost);
                     }
    -              });
    -              return next(req);
    -            };
    -          },
    -        ],
    +                if (serviceConfig.publicHost) {
    +                  req.header.set("x-zitadel-public-host", serviceConfig.publicHost);
    +                }
    +
    +                // Apply headers from CUSTOM_REQUEST_HEADERS environment variable
    +                if (process.env.CUSTOM_REQUEST_HEADERS) {
    +                  process.env.CUSTOM_REQUEST_HEADERS.split(",").forEach((header) => {
    +                    const kv = header.indexOf(":");
    +                    if (kv > 0) {
    +                      req.header.set(header.slice(0, kv).trim(), header.slice(kv + 1).trim());
    +                    } else {
    +                      console.warn(`Skipping malformed header: ${header}`);
    +                    }
    +                  });
    +                }
    +
    +                return next(req);
    +              };
    +            },
    +          ],
       });
     }
    
  • apps/login/src/middleware.ts+20 44 modified
    @@ -3,28 +3,16 @@ import { SecuritySettings } from "@zitadel/proto/zitadel/settings/v2/security_se
     import { headers } from "next/headers";
     import { NextRequest, NextResponse } from "next/server";
     import { DEFAULT_CSP } from "../constants/csp";
    -import { getServiceUrlFromHeaders } from "./lib/service-url";
    +import { getServiceConfig } from "./lib/service-url";
     export const config = {
    -  matcher: [
    -    "/.well-known/:path*",
    -    "/oauth/:path*",
    -    "/oidc/:path*",
    -    "/idps/callback/:path*",
    -    "/saml/:path*",
    -    "/:path*",
    -  ],
    +  matcher: ["/.well-known/:path*", "/oauth/:path*", "/oidc/:path*", "/idps/callback/:path*", "/saml/:path*", "/:path*"],
     };
     
    -async function loadSecuritySettings(
    -  request: NextRequest,
    -): Promise<SecuritySettings | null> {
    +async function loadSecuritySettings(request: NextRequest): Promise<SecuritySettings | null> {
       const securityResponse = await fetch(`${request.nextUrl.origin}/security`);
     
       if (!securityResponse.ok) {
    -    console.error(
    -      "Failed to fetch security settings:",
    -      securityResponse.statusText,
    -    );
    +    console.error("Failed to fetch security settings:", securityResponse.statusText);
         return null;
       }
     
    @@ -49,40 +37,28 @@ export async function middleware(request: NextRequest) {
       }
     
       // Only run the rest of the logic for the original matcher paths
    -  const proxyPaths = [
    -    "/.well-known/",
    -    "/oauth/",
    -    "/oidc/",
    -    "/idps/callback/",
    -    "/saml/",
    -  ];
    -
    -  const isMatched = proxyPaths.some((prefix) =>
    -    request.nextUrl.pathname.startsWith(prefix),
    -  );
    -
    -  // escape proxy if the environment is setup for multitenancy
    -  if (
    -    !isMatched ||
    -    !process.env.ZITADEL_API_URL ||
    -    !process.env.ZITADEL_SERVICE_USER_TOKEN
    -  ) {
    -    // For all other routes, just add the header and continue
    +  const proxyPaths = ["/.well-known/", "/oauth/", "/oidc/", "/idps/callback/", "/saml/"];
    +
    +  const isMatched = proxyPaths.some((prefix) => request.nextUrl.pathname.startsWith(prefix));
    +
    +  // Only proxy in self-hosted mode
    +  if (!isMatched) {
    +    // For multi-tenant or non-proxied routes, just add the header and continue
         return NextResponse.next({
           request: { headers: requestHeaders },
         });
       }
     
       const _headers = await headers();
    -  const { serviceUrl } = getServiceUrlFromHeaders(_headers);
    +  const { serviceConfig } = getServiceConfig(_headers);
     
    -  const instanceHost = `${serviceUrl}`
    -    .replace("https://", "")
    -    .replace("http://", "");
    -
    -  // Add additional headers as before
    -  requestHeaders.set("x-zitadel-public-host", `${request.nextUrl.host}`);
    -  requestHeaders.set("x-zitadel-instance-host", instanceHost);
    +  // add additional headers if available
    +  if (serviceConfig.publicHost) {
    +    requestHeaders.set("x-zitadel-public-host", serviceConfig.publicHost);
    +  }
    +  if (serviceConfig.instanceHost) {
    +    requestHeaders.set("x-zitadel-instance-host", serviceConfig.instanceHost);
    +  }
     
       const responseHeaders = new Headers();
       responseHeaders.set("Access-Control-Allow-Origin", "*");
    @@ -98,7 +74,7 @@ export async function middleware(request: NextRequest) {
         responseHeaders.delete("X-Frame-Options");
       }
     
    -  request.nextUrl.href = `${serviceUrl}${request.nextUrl.pathname}${request.nextUrl.search}`;
    +  request.nextUrl.href = `${serviceConfig.baseUrl}${request.nextUrl.pathname}${request.nextUrl.search}`;
     
       return NextResponse.rewrite(request.nextUrl, {
         request: {
    
  • apps/login/tsconfig.json+12 2 modified
    @@ -28,6 +28,16 @@
           ]
         }
       },
    -  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
    -  "exclude": ["node_modules", "integration", "acceptance"]
    +  "include": [
    +    "next-env.d.ts",
    +    "**/*.ts",
    +    "**/*.tsx",
    +    ".next/types/**/*.ts",
    +    ".next/dev/types/**/*.ts"
    +  ],
    +  "exclude": [
    +    "node_modules",
    +    "integration",
    +    "acceptance"
    +  ]
     }
    
  • internal/api/oidc/auth_request.go+26 9 modified
    @@ -10,10 +10,11 @@ import (
     	"strings"
     	"time"
     
    +	"github.com/go-jose/go-jose/v4"
     	"github.com/zitadel/logging"
    +	"github.com/zitadel/oidc/v3/pkg/crypto"
     	"github.com/zitadel/oidc/v3/pkg/oidc"
     	"github.com/zitadel/oidc/v3/pkg/op"
    -	"golang.org/x/text/language"
     
     	"github.com/zitadel/zitadel/internal/api/authz"
     	http_utils "github.com/zitadel/zitadel/internal/api/http"
    @@ -33,6 +34,7 @@ const (
     	LoginPostLogoutRedirectParam = "post_logout_redirect"
     	LoginLogoutHintParam         = "logout_hint"
     	LoginUILocalesParam          = "ui_locales"
    +	LoginLogoutTokenParam        = "logout_token"
     	LoginPath                    = "/login"
     	LogoutPath                   = "/logout"
     	LogoutDonePath               = "/logout/done"
    @@ -298,7 +300,11 @@ func (o *OPStorage) TerminateSessionFromRequest(ctx context.Context, endSessionR
     		} else {
     			logoutURI = logoutURI.JoinPath(LogoutPath)
     		}
    -		return buildLoginV2LogoutURL(logoutURI, redirectURI, endSessionRequest.LogoutHint, endSessionRequest.UILocales), nil
    +		signer, _, err := GetSignerOnce(o.query.GetActiveSigningWebKey)(ctx)
    +		if err != nil {
    +			return "", err
    +		}
    +		return buildLoginV2LogoutURL(logoutURI, redirectURI, endSessionRequest.LogoutHint, endSessionRequest.UILocales, signer)
     	}
     
     	// V1:
    @@ -375,7 +381,13 @@ func (o *OPStorage) federatedLogout(ctx context.Context, sessionID string, postL
     	return login.ExternalLogoutPath(sessionID)
     }
     
    -func buildLoginV2LogoutURL(logoutURI *url.URL, redirectURI, logoutHint string, uiLocales []language.Tag) string {
    +type logoutTokenPayload struct {
    +	PostLogoutRedirectURI string       `json:"post_logout_redirect_uri,omitempty"`
    +	LogoutHint            string       `json:"logout_hint,omitempty"`
    +	UILocales             oidc.Locales `json:"ui_locales,omitempty"`
    +}
    +
    +func buildLoginV2LogoutURL(logoutURI *url.URL, redirectURI, logoutHint string, uiLocales oidc.Locales, signer jose.Signer) (string, error) {
     	if strings.HasSuffix(logoutURI.Path, "/") && len(logoutURI.Path) > 1 {
     		logoutURI.Path = strings.TrimSuffix(logoutURI.Path, "/")
     	}
    @@ -386,14 +398,19 @@ func buildLoginV2LogoutURL(logoutURI *url.URL, redirectURI, logoutHint string, u
     		q.Set(LoginLogoutHintParam, logoutHint)
     	}
     	if len(uiLocales) > 0 {
    -		locales := make([]string, len(uiLocales))
    -		for i, locale := range uiLocales {
    -			locales[i] = locale.String()
    -		}
    -		q.Set(LoginUILocalesParam, strings.Join(locales, " "))
    +		q.Set(LoginUILocalesParam, uiLocales.String())
    +	}
    +	logoutToken, err := crypto.Sign(&logoutTokenPayload{
    +		PostLogoutRedirectURI: redirectURI,
    +		LogoutHint:            logoutHint,
    +		UILocales:             uiLocales,
    +	}, signer)
    +	if err != nil {
    +		return "", err
     	}
    +	q.Set(LoginLogoutTokenParam, logoutToken)
     	logoutURI.RawQuery = q.Encode()
    -	return logoutURI.String()
    +	return logoutURI.String(), nil
     }
     
     // v2PostLogoutRedirectURI will take care that the post_logout_redirect_uri is correctly set for v2 logins.
    
  • internal/api/oidc/auth_request_test.go+43 1 modified
    @@ -1,9 +1,13 @@
     package oidc
     
     import (
    +	"crypto/rand"
    +	"crypto/rsa"
    +	"encoding/json"
     	"net/url"
     	"testing"
     
    +	"github.com/go-jose/go-jose/v4"
     	"github.com/stretchr/testify/assert"
     	"github.com/stretchr/testify/require"
     	"golang.org/x/text/language"
    @@ -18,6 +22,7 @@ func TestBuildLoginV2LogoutURL(t *testing.T) {
     		redirectURI    string
     		logoutHint     string
     		uiLocales      []language.Tag
    +		signer         jose.Signer
     		expectedParams map[string]string
     	}{
     		{
    @@ -26,6 +31,7 @@ func TestBuildLoginV2LogoutURL(t *testing.T) {
     			redirectURI:  "https://client/cb",
     			expectedParams: map[string]string{
     				"post_logout_redirect": "https://client/cb",
    +				"logout_token":         "", // presence checked separately
     			},
     		},
     		{
    @@ -36,6 +42,7 @@ func TestBuildLoginV2LogoutURL(t *testing.T) {
     			expectedParams: map[string]string{
     				"post_logout_redirect": "https://client/cb",
     				"logout_hint":          "user@example.com",
    +				"logout_token":         "", // presence checked separately
     			},
     		},
     		{
    @@ -46,6 +53,7 @@ func TestBuildLoginV2LogoutURL(t *testing.T) {
     			expectedParams: map[string]string{
     				"post_logout_redirect": "https://client/cb",
     				"ui_locales":           "en it",
    +				"logout_token":         "", // presence checked separately
     			},
     		},
     		{
    @@ -58,6 +66,7 @@ func TestBuildLoginV2LogoutURL(t *testing.T) {
     				"post_logout_redirect": "https://client/cb",
     				"logout_hint":          "logoutme",
     				"ui_locales":           "de-CH fr",
    +				"logout_token":         "", // presence checked separately
     			},
     		},
     		{
    @@ -66,6 +75,7 @@ func TestBuildLoginV2LogoutURL(t *testing.T) {
     			redirectURI:  "https://client/cb",
     			expectedParams: map[string]string{
     				"post_logout_redirect": "https://client/cb",
    +				"logout_token":         "", // presence checked separately
     			},
     		},
     	}
    @@ -79,9 +89,10 @@ func TestBuildLoginV2LogoutURL(t *testing.T) {
     			require.NoError(t, err)
     
     			// When
    -			got := buildLoginV2LogoutURL(logoutURI, tc.redirectURI, tc.logoutHint, tc.uiLocales)
    +			got, err := buildLoginV2LogoutURL(logoutURI, tc.redirectURI, tc.logoutHint, tc.uiLocales, signer)
     
     			// Then
    +			require.NoError(t, err)
     			gotURL, err := url.Parse(got)
     			require.NoError(t, err)
     			require.NotContains(t, gotURL.String(), "/logout/")
    @@ -91,8 +102,39 @@ func TestBuildLoginV2LogoutURL(t *testing.T) {
     			require.Len(t, q, len(tc.expectedParams))
     
     			for k, v := range tc.expectedParams {
    +				if k == LoginLogoutTokenParam {
    +					assertLogoutToken(t, q.Get(k), &logoutTokenPayload{
    +						PostLogoutRedirectURI: tc.redirectURI,
    +						LogoutHint:            tc.logoutHint,
    +						UILocales:             tc.uiLocales,
    +					})
    +					continue
    +				}
     				assert.Equal(t, v, q.Get(k))
     			}
     		})
     	}
     }
    +
    +func assertLogoutToken(t *testing.T, token string, payload *logoutTokenPayload) {
    +	signature, err := jose.ParseSigned(token, []jose.SignatureAlgorithm{jose.RS256})
    +	require.NoError(t, err)
    +	logoutToken := new(logoutTokenPayload)
    +	err = json.Unmarshal(signature.UnsafePayloadWithoutVerification(), logoutToken)
    +	require.NoError(t, err)
    +	assert.Equal(t, payload, logoutToken)
    +}
    +
    +var (
    +	privKey, _ = rsa.GenerateKey(rand.Reader, 2048)
    +	signer     = func() jose.Signer {
    +		signer, _ := jose.NewSigner(
    +			jose.SigningKey{
    +				Algorithm: jose.RS256,
    +				Key:       privKey,
    +			},
    +			(&jose.SignerOptions{}).WithType("JWT"),
    +		)
    +		return signer
    +	}()
    +)
    
  • internal/api/oidc/integration_test/auth_request_test.go+16 4 modified
    @@ -551,23 +551,24 @@ func TestOPStorage_TerminateSession_refresh_grant(t *testing.T) {
     	require.NoError(t, err)
     }
     
    -func buildLogoutURL(origin, logoutURLV2 string, redirectURI string, extraParams map[string]string) string {
    +func buildLogoutURL(origin, logoutURLV2, redirectURI string, extraParams map[string]string) *url.URL {
     	u, _ := url.Parse(origin + logoutURLV2 + redirectURI)
     	q := u.Query()
     	for k, v := range extraParams {
     		q.Set(k, v)
     	}
    +	q.Set("logout_token", "signed-logout-token") // placeholder
     	u.RawQuery = q.Encode()
     	// Append the redirect URI as a URL-escaped string
    -	return u.String()
    +	return u
     }
     
     func TestOPStorage_TerminateSession_empty_id_token_hint(t *testing.T) {
     	tests := []struct {
     		name          string
     		clientID      string
     		authRequestID func(t testing.TB, instance *integration.Instance, clientID, redirectURI string, scope ...string) string
    -		logoutURL     string
    +		logoutURL     *url.URL
     	}{
     		{
     			name: "login header",
    @@ -614,7 +615,18 @@ func TestOPStorage_TerminateSession_empty_id_token_hint(t *testing.T) {
     
     			postLogoutRedirect, err := rp.EndSession(CTX, provider, "", logoutRedirectURI, "state", "hint", oidc.ParseLocales([]string{"it-IT", "en-US"}))
     			require.NoError(t, err)
    -			assert.Equal(t, tt.logoutURL, postLogoutRedirect.String())
    +
    +			requiredQueries := tt.logoutURL.Query()
    +			for key, value := range requiredQueries {
    +				if key == "logout_token" {
    +					assert.NotEmpty(t, value[0], "logout_token must be present")
    +					continue
    +				}
    +				assert.Equal(t, value[0], postLogoutRedirect.Query().Get(key))
    +			}
    +			requiredURLWithoutQueries := *tt.logoutURL
    +			requiredURLWithoutQueries.RawQuery = ""
    +			assert.Equal(t, requiredURLWithoutQueries.String(), postLogoutRedirect.Scheme+"://"+postLogoutRedirect.Host+postLogoutRedirect.Path)
     
     			// userinfo must not fail until login UI terminated session
     			_, err = rp.Userinfo[*oidc.UserInfo](CTX, tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.Subject, provider)
    
  • packages/zitadel-client/src/node.ts+52 4 modified
    @@ -1,16 +1,31 @@
    -import { createGrpcTransport, GrpcTransportOptions } from "@connectrpc/connect-node";
    -import { importPKCS8, SignJWT } from "jose";
    +import {
    +  createGrpcTransport,
    +  GrpcTransportOptions,
    +} from "@connectrpc/connect-node";
    +import {
    +  createRemoteJWKSet,
    +  importPKCS8,
    +  jwtVerify,
    +  JWTPayload,
    +  SignJWT,
    +} from "jose";
     import { NewAuthorizationBearerInterceptor } from "./interceptors.js";
     
     /**
      * Create a server transport using grpc with the given token and configuration options.
      * @param token
      * @param opts
      */
    -export function createServerTransport(token: string, opts: GrpcTransportOptions) {
    +export function createServerTransport(
    +  token: string,
    +  opts: GrpcTransportOptions
    +) {
       return createGrpcTransport({
         ...opts,
    -    interceptors: [...(opts.interceptors || []), NewAuthorizationBearerInterceptor(token)],
    +    interceptors: [
    +      ...(opts.interceptors || []),
    +      NewAuthorizationBearerInterceptor(token),
    +    ],
       });
     }
     
    @@ -34,3 +49,36 @@ export async function newSystemToken({
         .setAudience(audience)
         .sign(await importPKCS8(key, "RS256"));
     }
    +
    +/**
    + * Verify a signed JWT with the given keys endpoint.
    + * @param token
    + * @param keysEndpoint
    + * @param options
    + */
    +export async function verifyJwt<T = JWTPayload>(
    +  token: string,
    +  keysEndpoint: string,
    +  options?: {
    +    issuer?: string;
    +    audience?: string;
    +    instanceHost?: string;
    +    publicHost?: string;
    +  },
    +): Promise<T & JWTPayload> {
    +   const headers: Record<string, string> = {};
    +   if (options?.instanceHost) {
    +     headers["x-zitadel-instance-host"] = options.instanceHost;
    +   }
    +   if (options?.publicHost) {
    +     headers["x-zitadel-public-host"] = options.publicHost;
    +   }
    +  const JWKS = createRemoteJWKSet(new URL(keysEndpoint), {headers: headers});
    +
    +  const { payload } = await jwtVerify(token, JWKS, {
    +    issuer: options?.issuer,
    +    audience: options?.audience,
    +  });
    +
    +  return payload as T & JWTPayload;
    +}
    
  • pnpm-lock.yaml+56 56 modified
    @@ -54,14 +54,14 @@ importers:
             specifier: ^2.29.4
             version: 2.30.1
           next:
    -        specifier: 15.4.0-canary.86
    -        version: 15.4.0-canary.86(@babel/core@7.28.4)(@playwright/test@1.55.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.92.1)
    +        specifier: 15.5.7
    +        version: 15.5.7(@babel/core@7.28.4)(@playwright/test@1.55.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.92.1)
           next-intl:
             specifier: ^3.25.1
    -        version: 3.26.5(next@15.4.0-canary.86(@babel/core@7.28.4)(@playwright/test@1.55.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.92.1))(react@19.1.0)
    +        version: 3.26.5(next@15.5.7(@babel/core@7.28.4)(@playwright/test@1.55.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.92.1))(react@19.1.0)
           next-themes:
             specifier: ^0.2.1
    -        version: 0.2.1(next@15.4.0-canary.86(@babel/core@7.28.4)(@playwright/test@1.55.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.92.1))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
    +        version: 0.2.1(next@15.5.7(@babel/core@7.28.4)(@playwright/test@1.55.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.92.1))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
           nice-grpc:
             specifier: 2.0.1
             version: 2.0.1
    @@ -166,8 +166,8 @@ importers:
             specifier: ^8.57.0
             version: 8.57.1
           eslint-config-next:
    -        specifier: 15.4.0-canary.86
    -        version: 15.4.0-canary.86(eslint@8.57.1)(typescript@5.9.2)
    +        specifier: 15.5.7
    +        version: 15.5.7(eslint@8.57.1)(typescript@5.9.2)
           eslint-config-prettier:
             specifier: ^9.1.0
             version: 9.1.2(eslint@8.57.1)
    @@ -3415,56 +3415,56 @@ packages:
         resolution: {integrity: sha512-kPTF5yemdmadP/+qMDcc3p10NkZKXHXGm2BCFvB192paCNxQrSJz+qb56SO+kvSn9exg+HvhGJ0gfIcVwPjzWw==}
         engines: {node: ^14.14.0 || >=16.0.0}
     
    -  '@next/env@15.4.0-canary.86':
    -    resolution: {integrity: sha512-WPrEvwqHnjeLx05ncJvqizbBJJFlQGRbxzOnL/pZWKzo19auM9x5Se87P27+E/D/d6jJS801l+thF85lfobAZQ==}
    +  '@next/env@15.5.7':
    +    resolution: {integrity: sha512-4h6Y2NyEkIEN7Z8YxkA27pq6zTkS09bUSYC0xjd0NpwFxjnIKeZEeH591o5WECSmjpUhLn3H2QLJcDye3Uzcvg==}
     
    -  '@next/eslint-plugin-next@15.4.0-canary.86':
    -    resolution: {integrity: sha512-cOlp6ajA1ptiBxiProcXaNAR88O5ck3IwGJr+A5SnNKU4iTUg4nP0K5lS4Mkage+LAMIQ8dImkLR53PpebXICA==}
    +  '@next/eslint-plugin-next@15.5.7':
    +    resolution: {integrity: sha512-DtRU2N7BkGr8r+pExfuWHwMEPX5SD57FeA6pxdgCHODo+b/UgIgjE+rgWKtJAbEbGhVZ2jtHn4g3wNhWFoNBQQ==}
     
    -  '@next/swc-darwin-arm64@15.4.0-canary.86':
    -    resolution: {integrity: sha512-1ofBmzjPkmoMdM+dXvybZ/Roq8HRo0sFzcwXk7/FJNOufuwyK+QKdSpLE7pHlPR7ZREqfEMj61ONO+gAK+zOJw==}
    +  '@next/swc-darwin-arm64@15.5.7':
    +    resolution: {integrity: sha512-IZwtxCEpI91HVU/rAUOOobWSZv4P2DeTtNaCdHqLcTJU4wdNXgAySvKa/qJCgR5m6KI8UsKDXtO2B31jcaw1Yw==}
         engines: {node: '>= 10'}
         cpu: [arm64]
         os: [darwin]
     
    -  '@next/swc-darwin-x64@15.4.0-canary.86':
    -    resolution: {integrity: sha512-WCKSrllvwzYi4TgrSdgxKSOF2nhieeaWWOeGucn0OXy50uOAamr0HwP5OaIBCx3oRar4w66gvs4IrdTdMedeJA==}
    +  '@next/swc-darwin-x64@15.5.7':
    +    resolution: {integrity: sha512-UP6CaDBcqaCBuiq/gfCEJw7sPEoX1aIjZHnBWN9v9qYHQdMKvCKcAVs4OX1vIjeE+tC5EIuwDTVIoXpUes29lg==}
         engines: {node: '>= 10'}
         cpu: [x64]
         os: [darwin]
     
    -  '@next/swc-linux-arm64-gnu@15.4.0-canary.86':
    -    resolution: {integrity: sha512-8qn7DJVNFjhEIDo2ts0YCsO7g+vJjPWh8Ur8lBK3XspeX0BPsF4s+YmgidrpzRXeIfoo2uYLkkXcy/57CVDblw==}
    +  '@next/swc-linux-arm64-gnu@15.5.7':
    +    resolution: {integrity: sha512-NCslw3GrNIw7OgmRBxHtdWFQYhexoUCq+0oS2ccjyYLtcn1SzGzeM54jpTFonIMUjNbHmpKpziXnpxhSWLcmBA==}
         engines: {node: '>= 10'}
         cpu: [arm64]
         os: [linux]
     
    -  '@next/swc-linux-arm64-musl@15.4.0-canary.86':
    -    resolution: {integrity: sha512-8MTn6N4Ja25neMLu2Bra1lqW9AWPqsYg0BVs5M/cxL0QkcN3mak/8LLX1vbzz7GigMGSA+NLwg+ol8lglfgIGA==}
    +  '@next/swc-linux-arm64-musl@15.5.7':
    +    resolution: {integrity: sha512-nfymt+SE5cvtTrG9u1wdoxBr9bVB7mtKTcj0ltRn6gkP/2Nu1zM5ei8rwP9qKQP0Y//umK+TtkKgNtfboBxRrw==}
         engines: {node: '>= 10'}
         cpu: [arm64]
         os: [linux]
     
    -  '@next/swc-linux-x64-gnu@15.4.0-canary.86':
    -    resolution: {integrity: sha512-hIhzDwWDQHnH0M0Pzaqs1c5fa4+LHiLLEBuPJQvhBxQfH+Eh86DWiWHDCaoNiURvdRPg6uCuF2MjwptrMplEkg==}
    +  '@next/swc-linux-x64-gnu@15.5.7':
    +    resolution: {integrity: sha512-hvXcZvCaaEbCZcVzcY7E1uXN9xWZfFvkNHwbe/n4OkRhFWrs1J1QV+4U1BN06tXLdaS4DazEGXwgqnu/VMcmqw==}
         engines: {node: '>= 10'}
         cpu: [x64]
         os: [linux]
     
    -  '@next/swc-linux-x64-musl@15.4.0-canary.86':
    -    resolution: {integrity: sha512-FG6SBuSeRWYMNu6tsfaZ4iDzv3BLxlpRncO2xvKKQPeUdDSQ0cehuHYnx8fRte8IOAJ3rlbRd6NXvrDarqu92Q==}
    +  '@next/swc-linux-x64-musl@15.5.7':
    +    resolution: {integrity: sha512-4IUO539b8FmF0odY6/SqANJdgwn1xs1GkPO5doZugwZ3ETF6JUdckk7RGmsfSf7ws8Qb2YB5It33mvNL/0acqA==}
         engines: {node: '>= 10'}
         cpu: [x64]
         os: [linux]
     
    -  '@next/swc-win32-arm64-msvc@15.4.0-canary.86':
    -    resolution: {integrity: sha512-3HvZo4VuyINrNYplRhvC8ILdKwi/vFDHOcTN/I4ru039TFpu2eO6VtXsLBdOdJjGslSSSBYkX+6yRrghihAZDA==}
    +  '@next/swc-win32-arm64-msvc@15.5.7':
    +    resolution: {integrity: sha512-CpJVTkYI3ZajQkC5vajM7/ApKJUOlm6uP4BknM3XKvJ7VXAvCqSjSLmM0LKdYzn6nBJVSjdclx8nYJSa3xlTgQ==}
         engines: {node: '>= 10'}
         cpu: [arm64]
         os: [win32]
     
    -  '@next/swc-win32-x64-msvc@15.4.0-canary.86':
    -    resolution: {integrity: sha512-UO9JzGGj7GhtSJFdI0Bl0dkIIBfgbhXLsgNVmq9Z/CsUsQB6J9RS/BMhsxfVwhO+RETk13nFpNutMAhAwcuD8w==}
    +  '@next/swc-win32-x64-msvc@15.5.7':
    +    resolution: {integrity: sha512-gMzgBX164I6DN+9/PGA+9dQiwmTkE4TloBNx8Kv9UiGARsr9Nba7IpcBRA1iTV9vwlYnrE3Uy6I7Aj6qLjQuqw==}
         engines: {node: '>= 10'}
         cpu: [x64]
         os: [win32]
    @@ -7869,8 +7869,8 @@ packages:
         engines: {node: '>=6.0'}
         hasBin: true
     
    -  eslint-config-next@15.4.0-canary.86:
    -    resolution: {integrity: sha512-nMQzamY2GWhvScnfkfOVeq38tCt/TfyJyHMIzVYarpfyRj286Jk8ZkpgzQT8JtyeQ39kxTDZNBrB4CrWODYg4g==}
    +  eslint-config-next@15.5.7:
    +    resolution: {integrity: sha512-nU/TRGHHeG81NeLW5DeQT5t6BDUqbpsNQTvef1ld/tqHT+/zTx60/TIhKnmPISTTe++DVo+DLxDmk4rnwHaZVw==}
         peerDependencies:
           eslint: ^7.23.0 || ^8.0.0 || ^9.0.0
           typescript: '>=3.3.1'
    @@ -10615,8 +10615,8 @@ packages:
           react: '*'
           react-dom: '*'
     
    -  next@15.4.0-canary.86:
    -    resolution: {integrity: sha512-lGeO0sOvPZ7oFIklqRA863YzRL1bW+kT/OqU3N6RBquHldiucZwnZKQceZdn6WcHEFmWIHzZV+SMG1JEK7hZLg==}
    +  next@15.5.7:
    +    resolution: {integrity: sha512-+t2/0jIJ48kUpGKkdlhgkv+zPTEOoXyr60qXe68eB/pl3CMJaLeIGjzp5D6Oqt25hCBiBTt8wEeeAzfJvUKnPQ==}
         engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0}
         hasBin: true
         peerDependencies:
    @@ -19427,34 +19427,34 @@ snapshots:
           read-pkg-up: 9.1.0
           semver: 7.7.2
     
    -  '@next/env@15.4.0-canary.86': {}
    +  '@next/env@15.5.7': {}
     
    -  '@next/eslint-plugin-next@15.4.0-canary.86':
    +  '@next/eslint-plugin-next@15.5.7':
         dependencies:
           fast-glob: 3.3.1
     
    -  '@next/swc-darwin-arm64@15.4.0-canary.86':
    +  '@next/swc-darwin-arm64@15.5.7':
         optional: true
     
    -  '@next/swc-darwin-x64@15.4.0-canary.86':
    +  '@next/swc-darwin-x64@15.5.7':
         optional: true
     
    -  '@next/swc-linux-arm64-gnu@15.4.0-canary.86':
    +  '@next/swc-linux-arm64-gnu@15.5.7':
         optional: true
     
    -  '@next/swc-linux-arm64-musl@15.4.0-canary.86':
    +  '@next/swc-linux-arm64-musl@15.5.7':
         optional: true
     
    -  '@next/swc-linux-x64-gnu@15.4.0-canary.86':
    +  '@next/swc-linux-x64-gnu@15.5.7':
         optional: true
     
    -  '@next/swc-linux-x64-musl@15.4.0-canary.86':
    +  '@next/swc-linux-x64-musl@15.5.7':
         optional: true
     
    -  '@next/swc-win32-arm64-msvc@15.4.0-canary.86':
    +  '@next/swc-win32-arm64-msvc@15.5.7':
         optional: true
     
    -  '@next/swc-win32-x64-msvc@15.4.0-canary.86':
    +  '@next/swc-win32-x64-msvc@15.5.7':
         optional: true
     
       '@ngtools/webpack@16.2.16(@angular/compiler-cli@16.2.12(@angular/compiler@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(typescript@5.1.6))(typescript@5.1.6)(webpack@5.94.0(@swc/core@1.13.5(@swc/helpers@0.5.17))(esbuild@0.18.17))':
    @@ -24513,9 +24513,9 @@ snapshots:
         optionalDependencies:
           source-map: 0.6.1
     
    -  eslint-config-next@15.4.0-canary.86(eslint@8.57.1)(typescript@5.9.2):
    +  eslint-config-next@15.5.7(eslint@8.57.1)(typescript@5.9.2):
         dependencies:
    -      '@next/eslint-plugin-next': 15.4.0-canary.86
    +      '@next/eslint-plugin-next': 15.5.7
           '@rushstack/eslint-patch': 1.12.0
           '@typescript-eslint/eslint-plugin': 8.43.0(@typescript-eslint/parser@8.43.0(eslint@8.57.1)(typescript@5.9.2))(eslint@8.57.1)(typescript@5.9.2)
           '@typescript-eslint/parser': 8.43.0(eslint@8.57.1)(typescript@5.9.2)
    @@ -28371,38 +28371,38 @@ snapshots:
       netmask@2.0.2:
         optional: true
     
    -  next-intl@3.26.5(next@15.4.0-canary.86(@babel/core@7.28.4)(@playwright/test@1.55.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.92.1))(react@19.1.0):
    +  next-intl@3.26.5(next@15.5.7(@babel/core@7.28.4)(@playwright/test@1.55.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.92.1))(react@19.1.0):
         dependencies:
           '@formatjs/intl-localematcher': 0.5.10
           negotiator: 1.0.0
    -      next: 15.4.0-canary.86(@babel/core@7.28.4)(@playwright/test@1.55.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.92.1)
    +      next: 15.5.7(@babel/core@7.28.4)(@playwright/test@1.55.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.92.1)
           react: 19.1.0
           use-intl: 3.26.5(react@19.1.0)
     
    -  next-themes@0.2.1(next@15.4.0-canary.86(@babel/core@7.28.4)(@playwright/test@1.55.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.92.1))(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
    +  next-themes@0.2.1(next@15.5.7(@babel/core@7.28.4)(@playwright/test@1.55.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.92.1))(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
         dependencies:
    -      next: 15.4.0-canary.86(@babel/core@7.28.4)(@playwright/test@1.55.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.92.1)
    +      next: 15.5.7(@babel/core@7.28.4)(@playwright/test@1.55.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.92.1)
           react: 19.1.0
           react-dom: 19.1.0(react@19.1.0)
     
    -  next@15.4.0-canary.86(@babel/core@7.28.4)(@playwright/test@1.55.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.92.1):
    +  next@15.5.7(@babel/core@7.28.4)(@playwright/test@1.55.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.92.1):
         dependencies:
    -      '@next/env': 15.4.0-canary.86
    +      '@next/env': 15.5.7
           '@swc/helpers': 0.5.15
           caniuse-lite: 1.0.30001741
           postcss: 8.4.31
           react: 19.1.0
           react-dom: 19.1.0(react@19.1.0)
           styled-jsx: 5.1.6(@babel/core@7.28.4)(react@19.1.0)
         optionalDependencies:
    -      '@next/swc-darwin-arm64': 15.4.0-canary.86
    -      '@next/swc-darwin-x64': 15.4.0-canary.86
    -      '@next/swc-linux-arm64-gnu': 15.4.0-canary.86
    -      '@next/swc-linux-arm64-musl': 15.4.0-canary.86
    -      '@next/swc-linux-x64-gnu': 15.4.0-canary.86
    -      '@next/swc-linux-x64-musl': 15.4.0-canary.86
    -      '@next/swc-win32-arm64-msvc': 15.4.0-canary.86
    -      '@next/swc-win32-x64-msvc': 15.4.0-canary.86
    +      '@next/swc-darwin-arm64': 15.5.7
    +      '@next/swc-darwin-x64': 15.5.7
    +      '@next/swc-linux-arm64-gnu': 15.5.7
    +      '@next/swc-linux-arm64-musl': 15.5.7
    +      '@next/swc-linux-x64-gnu': 15.5.7
    +      '@next/swc-linux-x64-musl': 15.5.7
    +      '@next/swc-win32-arm64-msvc': 15.5.7
    +      '@next/swc-win32-x64-msvc': 15.5.7
           '@playwright/test': 1.55.0
           sass: 1.92.1
           sharp: 0.34.3
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

4

News mentions

0

No linked articles in our index yet.