From 14874d65466960b4a7eaa6594af0a82e5b1dc48a Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Fri, 26 Jun 2026 10:01:09 +0200 Subject: [PATCH] fix(login): Prevent IDP auto-creation failure when name fields are missing (#11070) # Which Problems Are Solved When using IDP auto-creation, the addHuman() call would fail if the IDP didn't provide required profile fields (givenName or familyName), resulting in a poor user experience. # How the Problems Are Solved Added validation before auto-creation to check if required profile fields are present. If givenName or familyName is missing, users are now redirected to the complete-registration page where they can manually provide the missing information. - Added profile field validation in CASE 4 (auto-creation) of processIDPCallback - Redirect to /idp/{provider}/complete-registration when required fields are missing - Pre-fill any available user data in the registration form --------- Co-authored-by: David Skewis Co-authored-by: Florian Forster --- apps/login/src/lib/server/idp-intent.test.ts | 115 +++++++++++++++++++ apps/login/src/lib/server/idp-intent.ts | 36 +++++- 2 files changed, 149 insertions(+), 2 deletions(-) diff --git a/apps/login/src/lib/server/idp-intent.test.ts b/apps/login/src/lib/server/idp-intent.test.ts index 90de236a0..9f4822a01 100644 --- a/apps/login/src/lib/server/idp-intent.test.ts +++ b/apps/login/src/lib/server/idp-intent.test.ts @@ -736,6 +736,121 @@ describe("processIDPCallback", () => { expect(result.error).toBe("Session error"); }); + + test("should redirect to complete registration when givenName is missing", async () => { + mockRetrieveIDPIntent.mockResolvedValue({ + ...defaultIntent, + userId: undefined, + addHumanUser: { + ...defaultIntent.addHumanUser, + profile: { + givenName: "", + familyName: "User", + displayName: "User", + }, + }, + }); + + const result = await processIDPCallback(defaultParams); + + expect(mockAddHuman).not.toHaveBeenCalled(); + expect(result.redirect).toContain("/idp/google/complete-registration"); + expect(result.redirect).toContain("id=intent123"); + expect(result.redirect).toContain("token=token123"); + expect(result.redirect).toContain("organization=org123"); + expect(result.redirect).toContain("familyName=User"); + }); + + test("should redirect to complete registration when familyName is missing", async () => { + mockRetrieveIDPIntent.mockResolvedValue({ + ...defaultIntent, + userId: undefined, + addHumanUser: { + ...defaultIntent.addHumanUser, + profile: { + givenName: "Test", + familyName: "", + displayName: "Test", + }, + }, + }); + + const result = await processIDPCallback(defaultParams); + + expect(mockAddHuman).not.toHaveBeenCalled(); + expect(result.redirect).toContain("/idp/google/complete-registration"); + expect(result.redirect).toContain("id=intent123"); + expect(result.redirect).toContain("token=token123"); + expect(result.redirect).toContain("organization=org123"); + expect(result.redirect).toContain("givenName=Test"); + }); + + test("should redirect to complete registration when both givenName and familyName are missing", async () => { + mockRetrieveIDPIntent.mockResolvedValue({ + ...defaultIntent, + userId: undefined, + addHumanUser: { + ...defaultIntent.addHumanUser, + profile: { + givenName: "", + familyName: "", + displayName: "", + }, + }, + }); + + const result = await processIDPCallback(defaultParams); + + expect(mockAddHuman).not.toHaveBeenCalled(); + expect(result.redirect).toContain("/idp/google/complete-registration"); + expect(result.redirect).toContain("id=intent123"); + expect(result.redirect).toContain("token=token123"); + expect(result.redirect).toContain("organization=org123"); + }); + + test("should redirect to complete registration when profile is undefined", async () => { + mockRetrieveIDPIntent.mockResolvedValue({ + ...defaultIntent, + userId: undefined, + addHumanUser: { + ...defaultIntent.addHumanUser, + profile: undefined, + }, + }); + + const result = await processIDPCallback(defaultParams); + + expect(mockAddHuman).not.toHaveBeenCalled(); + expect(result.redirect).toContain("/idp/google/complete-registration"); + expect(result.redirect).toContain("id=intent123"); + expect(result.redirect).toContain("token=token123"); + }); + + test("should use fallback organization when required fields missing and no org provided", async () => { + mockRetrieveIDPIntent.mockResolvedValue({ + ...defaultIntent, + userId: undefined, + addHumanUser: { + ...defaultIntent.addHumanUser, + profile: { + givenName: "", + familyName: "User", + displayName: "User", + }, + }, + }); + mockGetDefaultOrg.mockResolvedValue({ id: "default-org" }); + + const result = await processIDPCallback({ + ...defaultParams, + organization: undefined, + }); + + expect(mockGetDefaultOrg).toHaveBeenCalled(); + expect(mockAddHuman).not.toHaveBeenCalled(); + expect(result.redirect).toContain("/idp/google/complete-registration"); + expect(result.redirect).toContain("organization=default-org"); + }); }); describe("CASE 5: Manual user creation allowed", () => { diff --git a/apps/login/src/lib/server/idp-intent.ts b/apps/login/src/lib/server/idp-intent.ts index 763914c2b..f1ef5178f 100644 --- a/apps/login/src/lib/server/idp-intent.ts +++ b/apps/login/src/lib/server/idp-intent.ts @@ -452,7 +452,7 @@ async function handleAutoLinking(ctx: IDPHandlerContext): Promise { const { options, intent, serviceConfig, buildRedirectParams, t } = ctx; - const { addHumanUser } = intent; + const { addHumanUser, idpInformation } = intent; const { organization, provider } = ctx.params; if (options?.isAutoCreation && addHumanUser) { @@ -468,6 +468,32 @@ async function handleAutoCreation(ctx: IDPHandlerContext): Promise