fix(login): preserve org domain suffix through account chooser navigation (#12304)

Closes #12024

# Which Problems Are Solved

When the login flow is started with an
`urn:zitadel:iam:org:domain:primary:{domain}` scope, the resolved domain
suffix is lost if the user navigates through the account chooser (e.g.
clicking "Use another account"). This causes the login to fail with
"User not found" because the organization can no longer be resolved.

Additionally, the `hideLoginNameSuffix` branding setting was never
respected by the login app.

# How the Problems Are Solved

- Forward the `orgDomain` parameter through `gotoAccounts()` and the
`/accounts` page, so it survives navigation to `/loginname` — the same
way `organization` and `requestId` are already forwarded.
- Respect `BrandingSettings.hideLoginNameSuffix`: when enabled, the
`@domain.com` suffix is hidden from the input UI but still used
internally for user search.

---------

Co-authored-by: Livio Spring <9405495+livio-a@users.noreply.github.com>
This commit is contained in:
Max Peintner
2026-07-03 16:25:20 +02:00
committed by GitHub
co-authored by Livio Spring
parent 6b629c2290
commit 1beabed8cf
4 changed files with 37 additions and 19 deletions
@@ -43,6 +43,7 @@ export default async function Page(props: { searchParams: Promise<Record<string
const requestId = searchParams?.requestId;
const organization = searchParams?.organization;
const orgDomain = searchParams?.orgDomain;
const _headers = await headers();
const { serviceConfig } = getServiceConfig(_headers);
@@ -69,6 +70,10 @@ export default async function Page(props: { searchParams: Promise<Record<string
params.append("organization", organization);
}
if (orgDomain) {
params.append("orgDomain", orgDomain);
}
return (
<DynamicTheme branding={branding}>
<div className="flex flex-col space-y-4">
@@ -20,7 +20,7 @@ export default async function Page(props: { searchParams: Promise<Record<string
const loginName = searchParams?.loginName;
const requestId = searchParams?.requestId;
const organization = searchParams?.organization;
const suffix = searchParams?.suffix;
const orgDomain = searchParams?.orgDomain;
const submit: boolean = searchParams?.submit === "true";
const _headers = await headers();
@@ -64,7 +64,8 @@ export default async function Page(props: { searchParams: Promise<Record<string
organization={organization} // stick to "organization" as we still want to do user discovery based on the searchParams not the default organization, later the organization is determined by the found user
defaultOrganization={defaultOrganization}
loginSettings={loginSettings}
suffix={suffix}
suffix={orgDomain}
hideSuffix={branding?.hideLoginNameSuffix}
submit={submit}
allowRegister={!!loginSettings?.allowRegister}
></UsernameForm>
+3 -1
View File
@@ -26,6 +26,7 @@ type Props = {
organization?: string;
defaultOrganization?: string;
suffix?: string;
hideSuffix?: boolean;
submit: boolean;
allowRegister: boolean;
};
@@ -36,6 +37,7 @@ export function UsernameForm({
organization,
defaultOrganization,
suffix,
hideSuffix,
loginSettings,
submit,
allowRegister,
@@ -111,7 +113,7 @@ export function UsernameForm({
{...register("loginName", { required: t("required.loginName") })}
label={inputLabel}
data-testid="username-text-input"
suffix={suffix}
suffix={hideSuffix ? undefined : suffix}
/>
{allowRegister && (
<button
+26 -16
View File
@@ -57,10 +57,12 @@ const gotoAccounts = ({
request,
requestId,
organization,
orgDomain,
}: {
request: NextRequest;
requestId: string;
organization?: string;
orgDomain?: string;
}): NextResponse<unknown> => {
const accountsUrl = constructUrl(request, "/accounts");
@@ -70,6 +72,9 @@ const gotoAccounts = ({
if (organization) {
accountsUrl.searchParams.set("organization", organization);
}
if (orgDomain) {
accountsUrl.searchParams.set("orgDomain", orgDomain);
}
return NextResponse.redirect(accountsUrl);
};
@@ -79,13 +84,13 @@ const gotoLoginname = ({
requestId,
loginHint,
organization,
suffix,
orgDomain,
}: {
request: NextRequest;
requestId: string;
loginHint?: string;
organization?: string;
suffix?: string;
orgDomain?: string;
}): NextResponse<unknown> => {
const loginNameUrl = constructUrl(request, "/loginname");
loginNameUrl.searchParams.set("requestId", requestId);
@@ -97,8 +102,8 @@ const gotoLoginname = ({
if (organization) {
loginNameUrl.searchParams.set("organization", organization);
}
if (suffix) {
loginNameUrl.searchParams.set("suffix", suffix);
if (orgDomain) {
loginNameUrl.searchParams.set("orgDomain", orgDomain);
}
return NextResponse.redirect(loginNameUrl);
@@ -140,7 +145,7 @@ export async function handleOIDCFlowInitiation(params: FlowInitiationParams): Pr
}
let organization = "";
let suffix = "";
let orgDomain = "";
let idpId = "";
if (authRequest?.scope) {
@@ -155,15 +160,15 @@ export async function handleOIDCFlowInitiation(params: FlowInitiationParams): Pr
if (orgDomainScope) {
const matched = ORG_DOMAIN_SCOPE_REGEX.exec(orgDomainScope);
const orgDomain = matched?.[1] ?? "";
const scopeDomain = matched?.[1] ?? "";
logger.info("Extracted org domain:", { orgDomain });
if (orgDomain) {
const orgs = await getOrgsByDomain({ serviceConfig, domain: orgDomain });
logger.info("Extracted org domain:", { orgDomain: scopeDomain });
if (scopeDomain) {
const orgs = await getOrgsByDomain({ serviceConfig, domain: scopeDomain });
if (orgs.result && orgs.result.length === 1) {
organization = orgs.result[0].id ?? "";
suffix = orgDomain;
orgDomain = scopeDomain;
}
}
}
@@ -275,13 +280,14 @@ export async function handleOIDCFlowInitiation(params: FlowInitiationParams): Pr
requestId,
loginHint: authRequest.loginHint,
organization,
suffix,
orgDomain,
});
}
return gotoAccounts({
request,
requestId,
organization,
orgDomain,
});
} else if (authRequest.prompt.includes(Prompt.LOGIN)) {
if (authRequest.loginHint) {
@@ -315,8 +321,8 @@ export async function handleOIDCFlowInitiation(params: FlowInitiationParams): Pr
if (organization) {
loginNameUrl.searchParams.set("organization", organization);
}
if (suffix) {
loginNameUrl.searchParams.set("suffix", suffix);
if (orgDomain) {
loginNameUrl.searchParams.set("orgDomain", orgDomain);
}
return NextResponse.redirect(loginNameUrl);
} else if (authRequest.prompt.includes(Prompt.NONE)) {
@@ -378,13 +384,14 @@ export async function handleOIDCFlowInitiation(params: FlowInitiationParams): Pr
requestId,
loginHint: authRequest.loginHint,
organization,
suffix,
orgDomain,
});
}
return gotoAccounts({
request,
requestId,
organization,
orgDomain,
});
}
@@ -395,6 +402,7 @@ export async function handleOIDCFlowInitiation(params: FlowInitiationParams): Pr
request,
requestId,
organization,
orgDomain,
});
}
@@ -425,6 +433,7 @@ export async function handleOIDCFlowInitiation(params: FlowInitiationParams): Pr
return gotoAccounts({
request,
organization,
orgDomain,
requestId,
});
}
@@ -434,6 +443,7 @@ export async function handleOIDCFlowInitiation(params: FlowInitiationParams): Pr
request,
requestId,
organization,
orgDomain,
});
}
}
@@ -450,8 +460,8 @@ export async function handleOIDCFlowInitiation(params: FlowInitiationParams): Pr
loginNameUrl.searchParams.set("organization", organization);
}
if (suffix) {
loginNameUrl.searchParams.set("suffix", suffix);
if (orgDomain) {
loginNameUrl.searchParams.set("orgDomain", orgDomain);
}
return NextResponse.redirect(loginNameUrl);