diff --git a/apps/login/src/lib/server/flow-initiation.test.ts b/apps/login/src/lib/server/flow-initiation.test.ts index ff7f25ee6..e7f9fd246 100644 --- a/apps/login/src/lib/server/flow-initiation.test.ts +++ b/apps/login/src/lib/server/flow-initiation.test.ts @@ -355,3 +355,103 @@ describe("handleOIDCFlowInitiation — org-scoped session filtering", () => { expect(location).toContain("/loginname"); }); }); + +describe("handleOIDCFlowInitiation — Prompt.LOGIN + loginHint requestId prefix", () => { + let mockGetAuthRequest: ReturnType; + let mockConstructUrl: ReturnType; + let mockSendLoginname: ReturnType; + + const existingSession = { + id: "session-1", + factors: { user: { id: "user1", organizationId: "org1", loginName: "user@example.com" } }, + }; + + beforeEach(async () => { + vi.clearAllMocks(); + vi.unstubAllEnvs(); + + const zitadel = await import("@/lib/zitadel"); + const serviceUrl = await import("@/lib/service-url"); + const authUtils = await import("@/lib/auth-utils"); + const loginname = await import("@/lib/server/loginname"); + + mockGetAuthRequest = vi.mocked(zitadel.getAuthRequest); + mockConstructUrl = vi.mocked(serviceUrl.constructUrl); + mockSendLoginname = vi.mocked(loginname.sendLoginname); + vi.mocked(authUtils.getValidLocaleFromUILocales).mockReturnValue(null); + + mockConstructUrl.mockImplementation((_req: any, path: string) => { + return new URL(`https://example.com${path}`); + }); + }); + + afterEach(() => { + vi.unstubAllEnvs(); + }); + + test("should pass requestId with oidc_ prefix to sendLoginname (not authRequest.id)", async () => { + const { Prompt } = await import("@zitadel/proto/zitadel/oidc/v2/authorization_pb"); + + mockGetAuthRequest.mockResolvedValue({ + authRequest: { + id: "abc123", + uiLocales: [], + scope: [], + prompt: [Prompt.LOGIN], + loginHint: "user@example.com", + }, + }); + + mockSendLoginname.mockResolvedValue({ + redirect: "/password?loginName=user%40example.com", + }); + + await handleOIDCFlowInitiation( + makeBaseParams({ + requestId: "oidc_abc123", + sessions: [existingSession] as any, + sessionCookies: [{ id: "session-1", token: "tok" }], + }), + ); + + expect(mockSendLoginname).toHaveBeenCalledWith( + expect.objectContaining({ + loginName: "user@example.com", + requestId: "oidc_abc123", + }), + ); + }); + + test("should NOT pass raw authRequest.id without oidc_ prefix to sendLoginname", async () => { + const { Prompt } = await import("@zitadel/proto/zitadel/oidc/v2/authorization_pb"); + + mockGetAuthRequest.mockResolvedValue({ + authRequest: { + id: "abc123", + uiLocales: [], + scope: [], + prompt: [Prompt.LOGIN], + loginHint: "user@example.com", + }, + }); + + mockSendLoginname.mockResolvedValue({ + redirect: "/password?loginName=user%40example.com", + }); + + await handleOIDCFlowInitiation( + makeBaseParams({ + requestId: "oidc_abc123", + sessions: [existingSession] as any, + sessionCookies: [{ id: "session-1", token: "tok" }], + }), + ); + + // Ensure the raw authRequest.id is NOT what's passed + expect(mockSendLoginname).not.toHaveBeenCalledWith( + expect.objectContaining({ + requestId: "abc123", + }), + ); + }); +}); diff --git a/apps/login/src/lib/server/flow-initiation.ts b/apps/login/src/lib/server/flow-initiation.ts index 1333897e9..70f46892d 100644 --- a/apps/login/src/lib/server/flow-initiation.ts +++ b/apps/login/src/lib/server/flow-initiation.ts @@ -187,9 +187,7 @@ export async function handleOIDCFlowInitiation(params: FlowInitiationParams): Pr if (identityProviderType === IdentityProviderType.LDAP) { const ldapUrl = constructUrl(request, "/ldap"); - if (authRequest.id) { - ldapUrl.searchParams.set("requestId", `oidc_${authRequest.id}`); - } + ldapUrl.searchParams.set("requestId", requestId); if (organization) { ldapUrl.searchParams.set("organization", organization); } @@ -274,7 +272,7 @@ export async function handleOIDCFlowInitiation(params: FlowInitiationParams): Pr if (eligibleSessions.length === 0) { return gotoLoginname({ request, - requestId: `oidc_${authRequest.id}`, + requestId, loginHint: authRequest.loginHint, organization, suffix, @@ -282,7 +280,7 @@ export async function handleOIDCFlowInitiation(params: FlowInitiationParams): Pr } return gotoAccounts({ request, - requestId: `oidc_${authRequest.id}`, + requestId, organization, }); } else if (authRequest.prompt.includes(Prompt.LOGIN)) { @@ -290,7 +288,7 @@ export async function handleOIDCFlowInitiation(params: FlowInitiationParams): Pr try { let command: SendLoginnameCommand = { loginName: authRequest.loginHint, - requestId: authRequest.id, + requestId: requestId, }; if (organization) { @@ -377,7 +375,7 @@ export async function handleOIDCFlowInitiation(params: FlowInitiationParams): Pr if (eligibleSessions.length === 0) { return gotoLoginname({ request, - requestId: `oidc_${authRequest.id}`, + requestId, loginHint: authRequest.loginHint, organization, suffix, @@ -385,7 +383,7 @@ export async function handleOIDCFlowInitiation(params: FlowInitiationParams): Pr } return gotoAccounts({ request, - requestId: `oidc_${authRequest.id}`, + requestId, organization, }); } @@ -395,7 +393,7 @@ export async function handleOIDCFlowInitiation(params: FlowInitiationParams): Pr if (!cookie || !cookie.id || !cookie.token) { return gotoAccounts({ request, - requestId: `oidc_${authRequest.id}`, + requestId, organization, }); }