mirror of
https://github.com/andwati/zitadel.git
synced 2026-10-25 00:32:15 +00:00
fix(login): resolve unknownContext error on password page caused by inconsistent ignoreUnknownUsernames handling (#12512)
# Which Problems Are Solved The `/password` page (and related password pages) can show the `unknownContext` error even though the user was resolved correctly and login completes fine. Two causes: 1. `sendLoginname` used two different `ignoreUnknownUsernames` sources: session creation was gated on the **user's org** login settings, while the redirect's `loginName` masking and enumeration guards were gated on a **caller-supplied flag** reflecting the request context (instance or org scope). Whenever the two diverge — e.g. `ignoreUnknownUsernames` enabled on the instance but not on the user's org, or the reverse — the redirect carries a `loginName` that doesn't match the session cookie (or no session is created at all), so the `/password` page can't load the session and renders `unknownContext`, while password submission still succeeds via its user-search fallback. This affects every caller of `sendLoginname` — the manual `/loginname` form as well as the server-side `login_hint` resolution; it was most reproducible with `login_hint`, since that path resolves context settings at the instance level, maximizing the chance of divergence with an org-level policy. 2. The password pages tried to re-derive "was enumeration protection applied?" from login settings to suppress the error — but they resolve settings from a different context than `sendLoginname` (falling back to the default organization instead of the instance), so any org-level custom policy made the suppression misfire. More fundamentally, the alert was keyed on a failed session lookup, which is not an error state: under enumeration protection no session exists by design, and the forms recover without one (`sendPassword` falls back to user search; `/password/set` works via code + userId). # How the Problems Are Solved `sendLoginname` no longer accepts `ignoreUnknownUsernames` from the caller. It derives a single constant server-side from the request-context login settings it already fetches and uses it for every decision in the flow: session creation, `preventUserEnumeration`, the multiple-users branch, the INITIAL-user check, the organization reset, the auth-method guards, and the IdP redirect after domain discovery. Redirect params use `session?.factors?.user?.loginName ?? command.loginName`: when a session exists, its loginName goes in the URL so the next page always finds the session cookie; when protection applies, no session (and no session cookie) is created and the raw input is echoed, keeping known and unknown users fully indistinguishable — including the previously observable `Set-Cookie` difference. `UsernameForm` and `resolveLoginHint` stop forwarding the flag, which also removes the redundant `getLoginSettings` call in `resolveLoginHint`. The password pages no longer consult login settings or the session lookup for the `unknownContext` error and instead warn only when required input is actually missing (matching the existing pattern on the passkey/mfa/u2f pages): `/password` and `/password/change` when no `loginName` searchParam is present, `/password/set` when neither `loginName` nor `userId` is present. A missing session alone no longer triggers the error. Verified against a live instance in all four settings combinations (uniform on/off, both divergence directions) including a known-vs-unknown `login_hint` comparison producing byte-identical responses under protection; full unit suite passes. --------- Co-authored-by: Livio Spring <[email protected]>
This commit is contained in:
co-authored by
Livio Spring
parent
9070b537d2
commit
464d1cb655
@@ -5,7 +5,7 @@ import { Translated } from "@/components/translated";
|
||||
import { UserAvatar } from "@/components/user-avatar";
|
||||
import { getServiceConfig } from "@/lib/service-url";
|
||||
import { loadMostRecentSession } from "@/lib/session";
|
||||
import { getBrandingSettings, getLoginSettings, getPasswordComplexitySettings } from "@/lib/zitadel";
|
||||
import { getBrandingSettings, getPasswordComplexitySettings } from "@/lib/zitadel";
|
||||
import { Metadata } from "next";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { headers } from "next/headers";
|
||||
@@ -39,11 +39,6 @@ export default async function Page(props: { searchParams: Promise<Record<string
|
||||
organization: sessionFactors?.factors?.user?.organizationId,
|
||||
});
|
||||
|
||||
const loginSettings = await getLoginSettings({
|
||||
serviceConfig,
|
||||
organization: sessionFactors?.factors?.user?.organizationId,
|
||||
});
|
||||
|
||||
return (
|
||||
<DynamicTheme branding={branding}>
|
||||
<div className="flex flex-col space-y-4">
|
||||
@@ -54,8 +49,10 @@ export default async function Page(props: { searchParams: Promise<Record<string
|
||||
<Translated i18nKey="change.description" namespace="password" />
|
||||
</p>
|
||||
|
||||
{/* show error only if usernames should be shown to be unknown */}
|
||||
{(!sessionFactors || !loginName) && !loginSettings?.ignoreUnknownUsernames && (
|
||||
{/* Only warn when there is no loginName to continue with; a failed session
|
||||
lookup is reported by the form gate below (failedLoading), and under
|
||||
enumeration protection no session may exist by design. */}
|
||||
{!loginName && (
|
||||
<div className="py-4">
|
||||
<Alert>
|
||||
<Translated i18nKey="unknownContext" namespace="error" />
|
||||
|
||||
@@ -73,8 +73,11 @@ export default async function Page(props: { searchParams: Promise<Record<string
|
||||
</div>
|
||||
|
||||
<div className="w-full">
|
||||
{/* show error only if usernames should be shown to be unknown */}
|
||||
{(!sessionFactors || !loginName) && !loginSettings?.ignoreUnknownUsernames && (
|
||||
{/* Only warn when there is no loginName to continue with (e.g. a direct visit
|
||||
without searchParams). A failed session lookup alone is not an error: the
|
||||
form still works via the user-search fallback in sendPassword, and under
|
||||
enumeration protection no session exists by design. */}
|
||||
{!loginName && (
|
||||
<div className="py-4">
|
||||
<Alert>
|
||||
<Translated i18nKey="unknownContext" namespace="error" />
|
||||
|
||||
@@ -108,8 +108,10 @@ export default async function Page(props: { searchParams: Promise<Record<string
|
||||
<Translated i18nKey="set.description" namespace="password" />
|
||||
</p>
|
||||
|
||||
{/* show error only if usernames should be shown to be unknown */}
|
||||
{loginName && !session && !loginSettings?.ignoreUnknownUsernames && (
|
||||
{/* Only warn when there is neither a loginName nor a userId to continue with.
|
||||
A missing session is expected here: the set/reset flow works via code +
|
||||
userId, and under enumeration protection no session exists by design. */}
|
||||
{!loginName && !userId && (
|
||||
<div className="py-4">
|
||||
<Alert>
|
||||
<Translated i18nKey="unknownContext" namespace="error" />
|
||||
|
||||
@@ -68,7 +68,6 @@ export function UsernameForm({
|
||||
defaultOrganization,
|
||||
requestId,
|
||||
suffix,
|
||||
ignoreUnknownUsernames: loginSettings?.ignoreUnknownUsernames,
|
||||
});
|
||||
|
||||
handleServerActionResponse(res, router, setSamlData, setError);
|
||||
@@ -79,7 +78,7 @@ export function UsernameForm({
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[defaultOrganization, requestId, suffix, loginSettings, router, t],
|
||||
[defaultOrganization, requestId, suffix, router, t],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -498,7 +498,7 @@ describe("handleOIDCFlowInitiation — org-scoped session filtering", () => {
|
||||
expect(location).not.toContain("submit=true");
|
||||
});
|
||||
|
||||
test("should forward ignoreUnknownUsernames from login settings when resolving loginHint", async () => {
|
||||
test("should resolve loginHint without forwarding enumeration settings (derived server-side by sendLoginname)", async () => {
|
||||
mockGetAuthRequest.mockResolvedValue({
|
||||
authRequest: {
|
||||
id: "abc123",
|
||||
@@ -509,9 +509,9 @@ describe("handleOIDCFlowInitiation — org-scoped session filtering", () => {
|
||||
},
|
||||
});
|
||||
|
||||
// Org has enumeration protection enabled: sendLoginname must receive the
|
||||
// flag so an unknown hint still redirects to the (fake) /password step.
|
||||
mockGetLoginSettings.mockResolvedValue({ ignoreUnknownUsernames: true });
|
||||
// Enumeration protection is derived inside sendLoginname from the request-context
|
||||
// login settings; resolveLoginHint only passes the hint and the org context, and
|
||||
// an unknown hint still redirects to the (fake) /password step.
|
||||
mockSendLoginname.mockResolvedValue({
|
||||
redirect: "/password?loginName=unknown%40example.com",
|
||||
});
|
||||
@@ -521,7 +521,11 @@ describe("handleOIDCFlowInitiation — org-scoped session filtering", () => {
|
||||
expect(mockSendLoginname).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
loginName: "[email protected]",
|
||||
ignoreUnknownUsernames: true,
|
||||
}),
|
||||
);
|
||||
expect(mockSendLoginname).not.toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
ignoreUnknownUsernames: expect.anything(),
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
createResponse,
|
||||
getActiveIdentityProviders,
|
||||
getAuthRequest,
|
||||
getLoginSettings,
|
||||
getOrgsByDomain,
|
||||
getSAMLRequest,
|
||||
getSecuritySettings,
|
||||
@@ -117,13 +116,11 @@ const gotoLoginname = ({
|
||||
* error), in which case callers fall back to the prefilled /loginname screen.
|
||||
*/
|
||||
const resolveLoginHint = async ({
|
||||
serviceConfig,
|
||||
request,
|
||||
requestId,
|
||||
loginHint,
|
||||
organization,
|
||||
}: {
|
||||
serviceConfig: ServiceConfig;
|
||||
request: NextRequest;
|
||||
requestId: string;
|
||||
loginHint?: string;
|
||||
@@ -134,17 +131,14 @@ const resolveLoginHint = async ({
|
||||
}
|
||||
|
||||
try {
|
||||
// Mirror the client-side loginname form: forward ignoreUnknownUsernames so
|
||||
// an unknown hint redirects to the (fake) /password step like a known one,
|
||||
// instead of falling back to /loginname and disclosing that the user does
|
||||
// not exist. getLoginSettings is TTL-cached, so this adds no extra RPC.
|
||||
const loginSettings = await getLoginSettings({ serviceConfig, organization: organization || undefined });
|
||||
|
||||
// sendLoginname derives enumeration protection (ignoreUnknownUsernames) from the
|
||||
// same organization context server-side, so an unknown hint redirects to the
|
||||
// (fake) /password step like a known one instead of falling back to /loginname
|
||||
// and disclosing that the user does not exist.
|
||||
const res = await sendLoginname({
|
||||
loginName: loginHint,
|
||||
requestId,
|
||||
organization: organization || undefined,
|
||||
ignoreUnknownUsernames: loginSettings?.ignoreUnknownUsernames,
|
||||
});
|
||||
|
||||
if (res && "redirect" in res && res.redirect) {
|
||||
@@ -343,7 +337,6 @@ export async function handleOIDCFlowInitiation(params: FlowInitiationParams): Pr
|
||||
});
|
||||
} else if (authRequest.prompt.includes(Prompt.LOGIN)) {
|
||||
const hintResponse = await resolveLoginHint({
|
||||
serviceConfig,
|
||||
request,
|
||||
requestId,
|
||||
loginHint: authRequest.loginHint,
|
||||
@@ -414,7 +407,6 @@ export async function handleOIDCFlowInitiation(params: FlowInitiationParams): Pr
|
||||
if (!selectedSession || !selectedSession.id) {
|
||||
// login_hint matches no session: resolve it straight to the next step.
|
||||
const hintResponse = await resolveLoginHint({
|
||||
serviceConfig,
|
||||
request,
|
||||
requestId,
|
||||
loginHint: authRequest.loginHint,
|
||||
@@ -499,7 +491,6 @@ export async function handleOIDCFlowInitiation(params: FlowInitiationParams): Pr
|
||||
} else {
|
||||
// No session: resolve a login_hint straight to the next step if we can.
|
||||
const hintResponse = await resolveLoginHint({
|
||||
serviceConfig,
|
||||
request,
|
||||
requestId,
|
||||
loginHint: authRequest?.loginHint,
|
||||
|
||||
@@ -537,7 +537,6 @@ describe("sendLoginname", () => {
|
||||
loginName: "[email protected]",
|
||||
requestId: "req123",
|
||||
organization: "org123",
|
||||
ignoreUnknownUsernames: true,
|
||||
});
|
||||
|
||||
expect(result).toBeDefined();
|
||||
@@ -874,7 +873,6 @@ describe("sendLoginname", () => {
|
||||
|
||||
const result = await sendLoginname({
|
||||
loginName: "[email protected]",
|
||||
ignoreUnknownUsernames: true,
|
||||
});
|
||||
|
||||
expect(result).not.toEqual({ error: "errors.userNotFound" });
|
||||
@@ -899,7 +897,6 @@ describe("sendLoginname", () => {
|
||||
|
||||
const result = await sendLoginname({
|
||||
loginName: "[email protected]",
|
||||
ignoreUnknownUsernames: true,
|
||||
});
|
||||
|
||||
expect(result).not.toEqual({ error: "errors.userNotFound" });
|
||||
@@ -989,7 +986,6 @@ describe("sendLoginname", () => {
|
||||
|
||||
const result = await sendLoginname({
|
||||
loginName: "[email protected]",
|
||||
ignoreUnknownUsernames: true,
|
||||
});
|
||||
|
||||
expect(result).not.toEqual({ error: "errors.moreThanOneUserFound" });
|
||||
@@ -1020,7 +1016,7 @@ describe("sendLoginname", () => {
|
||||
rawMessage: "Errors.User.NotActive (SESSION-Gj4ko)",
|
||||
});
|
||||
|
||||
const result = await sendLoginname({ loginName: "user1", ignoreUnknownUsernames: true });
|
||||
const result = await sendLoginname({ loginName: "user1" });
|
||||
|
||||
expect(result).toEqual({ redirect: "/password?loginName=user1" });
|
||||
// With ignoreUnknownUsernames: true, we skip session creation, so this mock is NOT called
|
||||
@@ -1046,7 +1042,7 @@ describe("sendLoginname", () => {
|
||||
authMethodTypes: [AuthenticationMethodType.PASSWORD],
|
||||
});
|
||||
|
||||
const result = await sendLoginname({ loginName: "user1", ignoreUnknownUsernames: true });
|
||||
const result = await sendLoginname({ loginName: "user1" });
|
||||
|
||||
expect(result).toEqual({ redirect: "/password?loginName=user1" });
|
||||
expect(mockCreateSessionAndUpdateCookie).not.toHaveBeenCalled();
|
||||
@@ -1086,7 +1082,6 @@ describe("sendLoginname", () => {
|
||||
|
||||
const result = await sendLoginname({
|
||||
loginName: "[email protected]",
|
||||
ignoreUnknownUsernames: true,
|
||||
});
|
||||
|
||||
expect(result).not.toEqual({ error: "errors.localAuthenticationNotAllowed" });
|
||||
@@ -1126,7 +1121,6 @@ describe("sendLoginname", () => {
|
||||
|
||||
const result = await sendLoginname({
|
||||
loginName: "[email protected]",
|
||||
ignoreUnknownUsernames: true,
|
||||
});
|
||||
|
||||
expect(result).not.toEqual({ error: "errors.passkeysNotAllowed" });
|
||||
@@ -1239,7 +1233,6 @@ describe("sendLoginname", () => {
|
||||
// INPUT login name is just "user"
|
||||
const result = await sendLoginname({
|
||||
loginName: "user",
|
||||
ignoreUnknownUsernames: true,
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty("redirect");
|
||||
@@ -1281,7 +1274,6 @@ describe("sendLoginname", () => {
|
||||
|
||||
const result = await sendLoginname({
|
||||
loginName: "user",
|
||||
ignoreUnknownUsernames: true,
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty("redirect");
|
||||
@@ -1323,7 +1315,6 @@ describe("sendLoginname", () => {
|
||||
|
||||
const result = await sendLoginname({
|
||||
loginName: "user",
|
||||
ignoreUnknownUsernames: true,
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty("redirect");
|
||||
@@ -1371,7 +1362,6 @@ describe("sendLoginname", () => {
|
||||
const result = await sendLoginname({
|
||||
loginName: "input-name",
|
||||
organization: "context-org", // Context has ignore=true
|
||||
ignoreUnknownUsernames: true,
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty("redirect");
|
||||
@@ -1419,7 +1409,6 @@ describe("sendLoginname", () => {
|
||||
const result = await sendLoginname({
|
||||
loginName: "input-name",
|
||||
organization: "context-org", // Context has ignore=false
|
||||
ignoreUnknownUsernames: false,
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty("redirect");
|
||||
|
||||
@@ -36,7 +36,6 @@ export type SendLoginnameCommand = {
|
||||
organization?: string;
|
||||
defaultOrganization?: string;
|
||||
suffix?: string;
|
||||
ignoreUnknownUsernames?: boolean;
|
||||
};
|
||||
|
||||
const ORG_SUFFIX_REGEX = /(?<=@)(.+)/;
|
||||
@@ -53,6 +52,13 @@ export async function sendLoginname(command: SendLoginnameCommand) {
|
||||
return { error: t("errors.couldNotGetLoginSettings") };
|
||||
}
|
||||
|
||||
// Single source of truth for enumeration protection, derived server-side from the
|
||||
// request-context login settings (sendLoginname is a public server action, so a
|
||||
// client-supplied flag must not be trusted). It gates session creation and the
|
||||
// loginName exposed in redirect URLs, keeping known and unknown users
|
||||
// indistinguishable while protection applies.
|
||||
const ignoreUnknownUsernames = !!loginSettingsByContext.ignoreUnknownUsernames;
|
||||
|
||||
let searchUsersRequest: SearchUsersCommand = {
|
||||
serviceConfig,
|
||||
searchValue: command.loginName,
|
||||
@@ -89,7 +95,7 @@ export async function sendLoginname(command: SendLoginnameCommand) {
|
||||
}
|
||||
|
||||
const preventUserEnumeration = (organization: string | undefined) => {
|
||||
if (command.ignoreUnknownUsernames) {
|
||||
if (ignoreUnknownUsernames) {
|
||||
logger.debug("ignoreUnknownUsernames is true, redirecting to password");
|
||||
const paramsPasswordDefault = new URLSearchParams({
|
||||
loginName: command.loginName,
|
||||
@@ -235,7 +241,7 @@ export async function sendLoginname(command: SendLoginnameCommand) {
|
||||
|
||||
if (users.length > 1) {
|
||||
logger.debug("multiple users found, returning error");
|
||||
if (loginSettingsByContext?.ignoreUnknownUsernames) {
|
||||
if (ignoreUnknownUsernames) {
|
||||
return preventUserEnumeration(command.organization);
|
||||
}
|
||||
return { error: t("errors.moreThanOneUserFound") };
|
||||
@@ -265,8 +271,11 @@ export async function sendLoginname(command: SendLoginnameCommand) {
|
||||
}
|
||||
}
|
||||
|
||||
// Only create a session (and its cookie) when enumeration protection does not
|
||||
// apply: with protection on, known and unknown users must be indistinguishable,
|
||||
// and the /password page must not be able to tell the difference either.
|
||||
let session;
|
||||
if (!userLoginSettings?.ignoreUnknownUsernames) {
|
||||
if (!ignoreUnknownUsernames) {
|
||||
const checks = create(ChecksSchema, {
|
||||
user: { search: { case: "userId", value: userId } },
|
||||
});
|
||||
@@ -292,9 +301,17 @@ export async function sendLoginname(command: SendLoginnameCommand) {
|
||||
return { error: t("errors.couldNotCreateSession") };
|
||||
}
|
||||
|
||||
// LoginName to expose in redirect URLs: while enumeration protection applies,
|
||||
// echo the raw input so known and unknown users stay indistinguishable; otherwise
|
||||
// use the session's loginName so the next page can match the session cookie
|
||||
// (falling back to the user's preferred login name).
|
||||
const redirectLoginName = ignoreUnknownUsernames
|
||||
? command.loginName
|
||||
: (session?.factors?.user?.loginName ?? user.preferredLoginName);
|
||||
|
||||
// TODO: check if handling of userstate INITIAL is needed
|
||||
if (user.state === UserState.INITIAL) {
|
||||
if (userLoginSettings?.ignoreUnknownUsernames) {
|
||||
if (ignoreUnknownUsernames) {
|
||||
return preventUserEnumeration(command.organization);
|
||||
}
|
||||
return { error: t("errors.initialUserNotSupported") };
|
||||
@@ -303,7 +320,7 @@ export async function sendLoginname(command: SendLoginnameCommand) {
|
||||
// Resolve organization from command or session
|
||||
let organization = command.organization ?? session?.factors?.user?.organizationId ?? user.details?.resourceOwner;
|
||||
|
||||
if (userLoginSettings?.ignoreUnknownUsernames) {
|
||||
if (ignoreUnknownUsernames) {
|
||||
organization = command.organization;
|
||||
if (!organization && ORG_SUFFIX_REGEX.test(command.loginName)) {
|
||||
const matched = ORG_SUFFIX_REGEX.exec(command.loginName);
|
||||
@@ -384,7 +401,7 @@ export async function sendLoginname(command: SendLoginnameCommand) {
|
||||
return idpResp;
|
||||
}
|
||||
|
||||
if (command.ignoreUnknownUsernames) {
|
||||
if (ignoreUnknownUsernames) {
|
||||
return preventUserEnumeration(command.organization);
|
||||
}
|
||||
|
||||
@@ -395,9 +412,7 @@ export async function sendLoginname(command: SendLoginnameCommand) {
|
||||
|
||||
{
|
||||
const paramsPassword = new URLSearchParams({
|
||||
loginName: command.ignoreUnknownUsernames
|
||||
? command.loginName
|
||||
: (session?.factors?.user?.loginName ?? user.preferredLoginName),
|
||||
loginName: redirectLoginName,
|
||||
});
|
||||
|
||||
if (organization) {
|
||||
@@ -415,7 +430,7 @@ export async function sendLoginname(command: SendLoginnameCommand) {
|
||||
|
||||
case AuthenticationMethodType.PASSKEY: // AuthenticationMethodType.AUTHENTICATION_METHOD_TYPE_PASSKEY
|
||||
if (userLoginSettings?.passkeysType === PasskeysType.NOT_ALLOWED || !userLoginSettings?.allowLocalAuthentication) {
|
||||
if (command.ignoreUnknownUsernames) {
|
||||
if (ignoreUnknownUsernames) {
|
||||
return preventUserEnumeration(command.organization);
|
||||
}
|
||||
return {
|
||||
@@ -425,9 +440,7 @@ export async function sendLoginname(command: SendLoginnameCommand) {
|
||||
|
||||
{
|
||||
const paramsPasskey = new URLSearchParams({
|
||||
loginName: command.ignoreUnknownUsernames
|
||||
? command.loginName
|
||||
: (session?.factors?.user?.loginName ?? user.preferredLoginName),
|
||||
loginName: redirectLoginName,
|
||||
});
|
||||
if (command.requestId) {
|
||||
paramsPasskey.append("requestId", command.requestId);
|
||||
@@ -458,9 +471,7 @@ export async function sendLoginname(command: SendLoginnameCommand) {
|
||||
userLoginSettings?.allowLocalAuthentication
|
||||
) {
|
||||
const passkeyParams = new URLSearchParams({
|
||||
loginName: command.ignoreUnknownUsernames
|
||||
? command.loginName
|
||||
: (session?.factors?.user?.loginName ?? user.preferredLoginName),
|
||||
loginName: redirectLoginName,
|
||||
altPassword: `${methods.authMethodTypes.includes(AuthenticationMethodType.PASSWORD) && userLoginSettings?.allowLocalAuthentication}`, // show alternative password option only if allowed
|
||||
});
|
||||
|
||||
@@ -478,7 +489,7 @@ export async function sendLoginname(command: SendLoginnameCommand) {
|
||||
} else if (methods.authMethodTypes.includes(AuthenticationMethodType.PASSWORD)) {
|
||||
// Check if password authentication is allowed
|
||||
if (!userLoginSettings?.allowLocalAuthentication) {
|
||||
if (command.ignoreUnknownUsernames) {
|
||||
if (ignoreUnknownUsernames) {
|
||||
return preventUserEnumeration(command.organization);
|
||||
}
|
||||
return {
|
||||
@@ -488,9 +499,7 @@ export async function sendLoginname(command: SendLoginnameCommand) {
|
||||
|
||||
// user has no passkey setup and login settings allow passwords
|
||||
const paramsPasswordDefault = new URLSearchParams({
|
||||
loginName: command.ignoreUnknownUsernames
|
||||
? command.loginName
|
||||
: (session?.factors?.user?.loginName ?? user.preferredLoginName),
|
||||
loginName: redirectLoginName,
|
||||
});
|
||||
|
||||
if (command.requestId) {
|
||||
@@ -545,6 +554,7 @@ export async function sendLoginname(command: SendLoginnameCommand) {
|
||||
logger.debug("No single org found for discovery");
|
||||
}
|
||||
}
|
||||
|
||||
// When a user is not found, try to redirect to an external IdP if:
|
||||
// - local authentication is disabled, OR
|
||||
// - domain discovery resolved an organization (regardless of allowRegister)
|
||||
@@ -553,7 +563,7 @@ export async function sendLoginname(command: SendLoginnameCommand) {
|
||||
// Fixes: https://github.com/zitadel/zitadel/issues/12021
|
||||
// Fixes: https://github.com/zitadel/zitadel/issues/12023
|
||||
|
||||
if ((!effectiveLoginSettings?.allowLocalAuthentication || discoveredOrganization) && !command.ignoreUnknownUsernames) {
|
||||
if ((!effectiveLoginSettings?.allowLocalAuthentication || discoveredOrganization) && !ignoreUnknownUsernames) {
|
||||
const resp = await redirectUserToIDP(undefined, discoveredOrganization);
|
||||
if (resp) {
|
||||
logger.debug("Redirecting to IDP", { organization: discoveredOrganization });
|
||||
|
||||
@@ -11,7 +11,7 @@ try {
|
||||
cacheConfig = JSON.parse(process.env.API_CACHE_CONFIG);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to parse API_CACHE_CONFIG", e);
|
||||
logger.error("Failed to parse API_CACHE_CONFIG", { error: e });
|
||||
}
|
||||
|
||||
/** Cache TTL: defaults to longMinutes (1 hour). Security settings rarely change. */
|
||||
|
||||
Reference in New Issue
Block a user