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
+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 { UserState } from "@zitadel/proto/zitadel/user/v2/user_pb";
import { getServiceUrlFromHeaders } from "../service-url";
import { getServiceConfig } from "../service-url";
import {
getActiveIdentityProviders,
getIDPByID,
@@ -22,7 +22,7 @@ import {
startIdentityProviderFlow,
} from "../zitadel";
import { createSessionAndUpdateCookie } from "./cookie";
import { getOriginalHost } from "./host";
import { getPublicHost } from "./host";
import { IDPLink } from "@zitadel/proto/zitadel/user/v2/idp_pb";
export type SendLoginnameCommand = {
@@ -36,21 +36,18 @@ const ORG_SUFFIX_REGEX = /(?<=@)(.+)/;
export async function sendLoginname(command: SendLoginnameCommand) {
const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
const { serviceConfig } = getServiceConfig(_headers);
const t = await getTranslations("loginname");
const loginSettingsByContext = await getLoginSettings({
serviceUrl,
organization: command.organization,
});
const loginSettingsByContext = await getLoginSettings({ serviceConfig, organization: command.organization });
if (!loginSettingsByContext) {
return { error: t("errors.couldNotGetLoginSettings") };
}
let searchUsersRequest: SearchUsersCommand = {
serviceUrl,
serviceConfig,
searchValue: command.loginName,
organizationId: command.organization,
loginSettings: loginSettingsByContext,
@@ -88,28 +85,22 @@ export async function sendLoginname(command: SendLoginnameCommand) {
// If userId is provided, check for user-specific IDP links first
let identityProviders: IDPLink[] = [];
if (userId) {
identityProviders = await listIDPLinks({
serviceUrl,
userId,
}).then((resp) => {
identityProviders = await listIDPLinks({ serviceConfig, userId }).then((resp) => {
return resp.result;
});
}
// If no IDP links exist for the user (or no userId provided), try to get active IDPs from the organization
if (identityProviders.length === 0) {
const activeIdps = await getActiveIdentityProviders({
serviceUrl,
orgId: organization,
}).then((resp) => {
const activeIdps = await getActiveIdentityProviders({ serviceConfig, orgId: organization }).then((resp) => {
return resp.identityProviders;
});
// If exactly one active IDP exists in the organization, redirect to it
if (activeIdps.length === 1) {
const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
const host = await getOriginalHost();
const { serviceConfig } = getServiceConfig(_headers);
const host = getPublicHost(_headers);
const identityProviderType = activeIdps[0].type;
const provider = idpTypeToSlug(identityProviderType);
@@ -131,7 +122,7 @@ export async function sendLoginname(command: SendLoginnameCommand) {
const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? "";
const url = await startIdentityProviderFlow({
serviceUrl,
serviceConfig,
idpId: activeIdps[0].id,
urls: {
successUrl:
@@ -153,15 +144,12 @@ export async function sendLoginname(command: SendLoginnameCommand) {
if (identityProviders.length === 1) {
const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
const host = await getOriginalHost();
const { serviceConfig } = getServiceConfig(_headers);
const host = getPublicHost(_headers);
const identityProviderId = identityProviders[0].idpId;
const idp = await getIDPByID({
serviceUrl,
id: identityProviderId,
});
const idp = await getIDPByID({ serviceConfig, id: identityProviderId });
const idpType = idp?.type;
@@ -189,7 +177,7 @@ export async function sendLoginname(command: SendLoginnameCommand) {
const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? "";
const url = await startIdentityProviderFlow({
serviceUrl,
serviceConfig,
idpId: idp.id,
urls: {
successUrl:
@@ -216,10 +204,7 @@ export async function sendLoginname(command: SendLoginnameCommand) {
const user = users[0];
const userId = users[0].userId;
const userLoginSettings = await getLoginSettings({
serviceUrl,
organization: user.details?.resourceOwner,
});
const userLoginSettings = await getLoginSettings({ serviceConfig, organization: user.details?.resourceOwner });
// compare with the concatenated suffix when set
const concatLoginname = command.suffix ? `${command.loginName}@${command.suffix}` : command.loginName;
@@ -273,10 +258,7 @@ export async function sendLoginname(command: SendLoginnameCommand) {
// Resolve organization from command or session
const organization = command.organization ?? session.factors?.user?.organizationId;
const methods = await listAuthenticationMethodTypes({
serviceUrl,
userId: session.factors?.user?.id,
});
const methods = await listAuthenticationMethodTypes({ serviceConfig, userId: session.factors?.user?.id });
// always resend invite if user has no auth method set
if (!methods.authMethodTypes || !methods.authMethodTypes.length) {
@@ -418,18 +400,12 @@ export async function sendLoginname(command: SendLoginnameCommand) {
const suffix = matched?.[1] ?? "";
// this just returns orgs where the suffix is set as primary domain
const orgs = await getOrgsByDomain({
serviceUrl,
domain: suffix,
});
const orgs = await getOrgsByDomain({ serviceConfig, domain: suffix });
const orgToCheckForDiscovery = orgs.result && orgs.result.length === 1 ? orgs.result[0].id : undefined;
if (orgToCheckForDiscovery) {
const orgLoginSettings = await getLoginSettings({
serviceUrl,
organization: orgToCheckForDiscovery,
});
const orgLoginSettings = await getLoginSettings({ serviceConfig, organization: orgToCheckForDiscovery });
if (orgLoginSettings?.allowDomainDiscovery) {
console.log("org discovery successful, using org:", orgToCheckForDiscovery);