VYPR
High severityNVD Advisory· Published Dec 9, 2025· Updated Dec 10, 2025

ZITADEL Vulnerable to Account Takeover via DOM-Based XSS in Zitadel V2 Login

CVE-2025-67495

Description

ZITADEL is an open-source identity infrastructure tool. Versions 4.0.0-rc.1 through 4.7.0 are vulnerable to DOM-Based XSS through the Zitadel V2 logout endpoint. The /logout endpoint insecurely routes to a value that is supplied in the post_logout_redirect GET parameter. As a result, unauthenticated remote attacker can execute malicious JS code on Zitadel users’ browsers. To carry out an attack, multiple user sessions need to be active in the same browser, however, account takeover is mitigated when using Multi-Factor Authentication (MFA) or Passwordless authentication. This issue is fixed 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.