diff --git a/apps/login/src/app/(login)/register/page.tsx b/apps/login/src/app/(login)/register/page.tsx index 2a74acfe8..34647532a 100644 --- a/apps/login/src/app/(login)/register/page.tsx +++ b/apps/login/src/app/(login)/register/page.tsx @@ -55,7 +55,23 @@ export default async function Page(props: { searchParams: Promise +
+

+ +

+ + + +
+
+ + ); + } + + if (!loginSettings?.allowRegister && (!loginSettings.allowExternalIdp || identityProviders.length === 0)) { return (
diff --git a/apps/login/src/lib/server/loginname.test.ts b/apps/login/src/lib/server/loginname.test.ts index b401cc713..70b0cbb16 100644 --- a/apps/login/src/lib/server/loginname.test.ts +++ b/apps/login/src/lib/server/loginname.test.ts @@ -276,7 +276,7 @@ describe("sendLoginname", () => { }); mockListIDPLinks.mockResolvedValue({ result: [] }); mockGetActiveIdentityProviders.mockResolvedValue({ - identityProviders: [{ id: "org-idp-123", type: 0 }], + identityProviders: [{ id: "org-idp-123", type: 0, options: { isAutoCreation: true } }], }); mockIdpTypeToSlug.mockReturnValue("google"); mockStartIdentityProviderFlow.mockResolvedValue({ url: "https://org-idp.example.com/auth" }); @@ -469,7 +469,7 @@ describe("sendLoginname", () => { allowLocalAuthentication: false, }); mockGetActiveIdentityProviders.mockResolvedValue({ - identityProviders: [{ id: "idp123", type: "OIDC" }], + identityProviders: [{ id: "idp123", type: "OIDC", options: { isAutoCreation: true } }], }); mockStartIdentityProviderFlow.mockResolvedValue({ url: "https://idp.example.com/auth" }); @@ -503,6 +503,7 @@ describe("sendLoginname", () => { test("should redirect to password when ignoreUnknownUsernames is true", async () => { mockGetLoginSettings.mockResolvedValue({ ignoreUnknownUsernames: true, + allowLocalAuthentication: true, }); const result = await sendLoginname({ @@ -532,6 +533,39 @@ describe("sendLoginname", () => { expect(result).toEqual({ error: "errors.userNotFound" }); }); + test("should redirect to IDP when allowRegister: false, allowLocalAuthentication: false and a single active IDP exists", async () => { + mockGetLoginSettings.mockResolvedValue({ + allowRegister: false, + allowLocalAuthentication: false, + }); + mockGetActiveIdentityProviders.mockResolvedValue({ + identityProviders: [{ id: "idp123", type: "OIDC", options: { isAutoCreation: true } }], + }); + mockStartIdentityProviderFlow.mockResolvedValue({ url: "https://idp.example.com/auth" }); + + const result = await sendLoginname({ + loginName: "user@example.com", + }); + + expect(result).toEqual({ redirect: "https://idp.example.com/auth" }); + }); + + test("should not redirect to IDP when single active IDP does not allow creation", async () => { + mockGetLoginSettings.mockResolvedValue({ + allowRegister: false, + allowLocalAuthentication: false, + }); + mockGetActiveIdentityProviders.mockResolvedValue({ + identityProviders: [{ id: "idp123", type: "OIDC", options: { isAutoCreation: false, isCreationAllowed: false } }], + }); + + const result = await sendLoginname({ + loginName: "user@example.com", + }); + + expect(result).toEqual({ error: "errors.userNotFound" }); + }); + test("should discover organization from domain suffix when user not found without org context", async () => { // Mock login settings for instance level (no org context) mockGetLoginSettings @@ -592,7 +626,7 @@ describe("sendLoginname", () => { }); mockGetActiveIdentityProviders.mockResolvedValue({ - identityProviders: [{ id: "idp123", type: "OIDC" }], + identityProviders: [{ id: "idp123", type: "OIDC", options: { isAutoCreation: true } }], }); mockStartIdentityProviderFlow.mockResolvedValue({ url: "https://idp.example.com/auth?org=discovered-org-456" }); diff --git a/apps/login/src/lib/server/loginname.ts b/apps/login/src/lib/server/loginname.ts index 2802e4a35..e31aaf1ec 100644 --- a/apps/login/src/lib/server/loginname.ts +++ b/apps/login/src/lib/server/loginname.ts @@ -119,7 +119,7 @@ export async function sendLoginname(command: SendLoginnameCommand) { // If no IDP links exist for the user (or no userId provided), try to get active IDPs from the organization if (identityProviders.length === 0) { const activeIdps = await getActiveIdentityProviders({ serviceConfig, orgId: organization }).then((resp) => { - return resp.identityProviders; + return resp.identityProviders.filter((idp) => idp.options?.isAutoCreation || idp.options?.isCreationAllowed); }); // If exactly one active IDP exists in the organization, redirect to it @@ -525,8 +525,9 @@ export async function sendLoginname(command: SendLoginnameCommand) { } } - // user not found, check if register is enabled on instance / organization context - if (effectiveLoginSettings?.allowRegister && !effectiveLoginSettings?.allowLocalAuthentication) { + // user not found, check if IDPs are available when local auth is not allowed + if (!effectiveLoginSettings?.allowLocalAuthentication) { + console.log("redirecting to IDP (register allowed, password not allowed)"); logger.debug("redirecting to IDP (register allowed, password not allowed)"); const resp = await redirectUserToIDP(undefined, discoveredOrganization); if (resp) {