fix(login): use constructUrl for implicit IdP success/failure URLs (#11389)

Closes #11386

# Which Problems Are Solved

This PR fixes an issue where the `successUrl` and `failureUrl` for
implicit IdP flows (initiated via OIDC `idp🆔...` scopes) were being
constructed using `request.nextUrl.origin`. This failed to account for:
- `x-zitadel-forward-host` headers (essential for proper public URL
resolution).
- `NEXT_PUBLIC_BASE_PATH` configuration.

# How the Problems Are Solved

The fix replaces the manual string concatenation with the shared
`constructUrl` helper, ensuring consistent and correct URL generation.

Co-authored-by: Livio Spring <[email protected]>
This commit is contained in:
Max Peintner
2026-01-23 05:55:59 +00:00
committed by GitHub
co-authored by Livio Spring
parent 1fc6f048a9
commit a10d13e2ec
+27 -27
View File
@@ -1,3 +1,5 @@
import { idpTypeToSlug } from "@/lib/idp";
import { sendLoginname, SendLoginnameCommand } from "@/lib/server/loginname";
import { constructUrl } from "@/lib/service-url";
import { findValidSession } from "@/lib/session";
import {
@@ -8,11 +10,9 @@ import {
getOrgsByDomain,
getSAMLRequest,
getSecuritySettings,
startIdentityProviderFlow,
ServiceConfig,
startIdentityProviderFlow,
} from "@/lib/zitadel";
import { sendLoginname, SendLoginnameCommand } from "@/lib/server/loginname";
import { idpTypeToSlug } from "@/lib/idp";
import { create } from "@zitadel/client";
import { Prompt } from "@zitadel/proto/zitadel/oidc/v2/authorization_pb";
import { CreateCallbackRequestSchema, SessionSchema } from "@zitadel/proto/zitadel/oidc/v2/oidc_service_pb";
@@ -61,8 +61,7 @@ export interface FlowInitiationParams {
export async function handleOIDCFlowInitiation(params: FlowInitiationParams): Promise<NextResponse> {
const { serviceConfig, requestId, sessions, sessionCookies, request } = params;
const { authRequest } = await getAuthRequest({ serviceConfig, authRequestId: requestId.replace("oidc_", ""),
});
const { authRequest } = await getAuthRequest({ serviceConfig, authRequestId: requestId.replace("oidc_", "") });
let organization = "";
let suffix = "";
@@ -84,8 +83,7 @@ export async function handleOIDCFlowInitiation(params: FlowInitiationParams): Pr
console.log("Extracted org domain:", orgDomain);
if (orgDomain) {
const orgs = await getOrgsByDomain({ serviceConfig, domain: orgDomain,
});
const orgs = await getOrgsByDomain({ serviceConfig, domain: orgDomain });
if (orgs.result && orgs.result.length === 1) {
organization = orgs.result[0].id ?? "";
@@ -99,7 +97,9 @@ export async function handleOIDCFlowInitiation(params: FlowInitiationParams): Pr
const matched = IDP_SCOPE_REGEX.exec(idpScope);
idpId = matched?.[1] ?? "";
const identityProviders = await getActiveIdentityProviders({ serviceConfig, orgId: organization ? organization : undefined,
const identityProviders = await getActiveIdentityProviders({
serviceConfig,
orgId: organization ? organization : undefined,
}).then((resp) => {
return resp.identityProviders;
});
@@ -107,7 +107,6 @@ export async function handleOIDCFlowInitiation(params: FlowInitiationParams): Pr
const idp = identityProviders.find((idp) => idp.id === idpId);
if (idp) {
const origin = request.nextUrl.origin;
const identityProviderType = identityProviders[0].type;
if (identityProviderType === IdentityProviderType.LDAP) {
@@ -132,10 +131,12 @@ export async function handleOIDCFlowInitiation(params: FlowInitiationParams): Pr
params.set("organization", organization);
}
let url: string | null = await startIdentityProviderFlow({ serviceConfig, idpId,
let url: string | null = await startIdentityProviderFlow({
serviceConfig,
idpId,
urls: {
successUrl: `${origin}/idp/${provider}/process?` + new URLSearchParams(params),
failureUrl: `${origin}/idp/${provider}/failure?` + new URLSearchParams(params),
successUrl: constructUrl(request, `/idp/${provider}/process?${params.toString()}`).toString(),
failureUrl: constructUrl(request, `/idp/${provider}/failure?${params.toString()}`).toString(),
},
});
@@ -208,11 +209,9 @@ export async function handleOIDCFlowInitiation(params: FlowInitiationParams): Pr
}
return NextResponse.redirect(loginNameUrl);
} else if (authRequest.prompt.includes(Prompt.NONE)) {
const securitySettings = await getSecuritySettings({ serviceConfig, });
const securitySettings = await getSecuritySettings({ serviceConfig });
const selectedSession = await findValidSession({ serviceConfig, sessions,
authRequest,
});
const selectedSession = await findValidSession({ serviceConfig, sessions, authRequest });
const noSessionResponse = NextResponse.json({ error: "No active session found" }, { status: 400 });
@@ -240,7 +239,9 @@ export async function handleOIDCFlowInitiation(params: FlowInitiationParams): Pr
sessionToken: cookie.token,
};
const { callbackUrl } = await createCallback({ serviceConfig, req: create(CreateCallbackRequestSchema, {
const { callbackUrl } = await createCallback({
serviceConfig,
req: create(CreateCallbackRequestSchema, {
authRequestId: requestId.replace("oidc_", ""),
callbackKind: {
case: "session",
@@ -262,9 +263,7 @@ export async function handleOIDCFlowInitiation(params: FlowInitiationParams): Pr
return callbackResponse;
} else {
let selectedSession = await findValidSession({ serviceConfig, sessions,
authRequest,
});
let selectedSession = await findValidSession({ serviceConfig, sessions, authRequest });
if (!selectedSession || !selectedSession.id) {
return gotoAccounts({
@@ -290,7 +289,9 @@ export async function handleOIDCFlowInitiation(params: FlowInitiationParams): Pr
};
try {
const { callbackUrl } = await createCallback({ serviceConfig, req: create(CreateCallbackRequestSchema, {
const { callbackUrl } = await createCallback({
serviceConfig,
req: create(CreateCallbackRequestSchema, {
authRequestId: requestId.replace("oidc_", ""),
callbackKind: {
case: "session",
@@ -344,8 +345,7 @@ export async function handleOIDCFlowInitiation(params: FlowInitiationParams): Pr
export async function handleSAMLFlowInitiation(params: FlowInitiationParams): Promise<NextResponse> {
const { serviceConfig, requestId, sessions, sessionCookies, request } = params;
const { samlRequest } = await getSAMLRequest({ serviceConfig, samlRequestId: requestId.replace("saml_", ""),
});
const { samlRequest } = await getSAMLRequest({ serviceConfig, samlRequestId: requestId.replace("saml_", "") });
if (!samlRequest) {
return NextResponse.json({ error: "No samlRequest found" }, { status: 400 });
@@ -359,9 +359,7 @@ export async function handleSAMLFlowInitiation(params: FlowInitiationParams): Pr
}
// Try to find a valid session
let selectedSession = await findValidSession({ serviceConfig, sessions,
samlRequest,
});
let selectedSession = await findValidSession({ serviceConfig, sessions, samlRequest });
// Early return: No valid session found - show account selection
if (!selectedSession || !selectedSession.id) {
@@ -389,7 +387,9 @@ export async function handleSAMLFlowInitiation(params: FlowInitiationParams): Pr
};
try {
const { url, binding } = await createResponse({ serviceConfig, req: create(CreateResponseRequestSchema, {
const { url, binding } = await createResponse({
serviceConfig,
req: create(CreateResponseRequestSchema, {
samlRequestId: requestId.replace("saml_", ""),
responseKind: {
case: "session",