Merge commit from fork

* fix: Improve host header handling and deployment mode detection

* ZITADEL_CLOUD env

* refactor service config

* service config

* cleanup

* cleanup

* util for original and public host

* instanceHost, publicHost

* cleanup constructUrl

* cleanup middleware

* add x-zitadel-forward-host as fallback for current cloud setup

* fix build

* fix: lint

* fix: update test mocks

* fix: provide basepath to unit tests

* host handling

* unit tests

* imports

* fix: getPublicHost instead of getInstanceHost

* unit tests

* update nextjs, implement jwt verify for logout_token

* pass logout information in jwt to login v2

* handle hosts in jwks url

* point idp callbacks to the api and not the login UI

* logout_hint out of logout token, typecheck

* rm unused imports

* fix oidc integration test

---------

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