From 865d4ab2e9eb2408fec0c0b9310255e1dfccc22c Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Tue, 8 Jul 2025 21:19:53 -0300 Subject: [PATCH 1/3] Set `redirectUrl` as `actionCompleteRedirectUrl` if force-an-org is enabled --- .changeset/public-hats-relax.md | 5 +++++ packages/clerk-js/src/core/resources/SignIn.ts | 16 +++++++++++++--- packages/clerk-js/src/core/resources/SignUp.ts | 14 ++++++++++++-- 3 files changed, 30 insertions(+), 5 deletions(-) create mode 100644 .changeset/public-hats-relax.md diff --git a/.changeset/public-hats-relax.md b/.changeset/public-hats-relax.md new file mode 100644 index 00000000000..ea9a45b655b --- /dev/null +++ b/.changeset/public-hats-relax.md @@ -0,0 +1,5 @@ +--- +'@clerk/clerk-js': patch +--- + +Force redirect to SSO callback route when force-an-org is enabled, ensuring task display and organization selection diff --git a/packages/clerk-js/src/core/resources/SignIn.ts b/packages/clerk-js/src/core/resources/SignIn.ts index e53e5bd07f2..3feeea5ddf7 100644 --- a/packages/clerk-js/src/core/resources/SignIn.ts +++ b/packages/clerk-js/src/core/resources/SignIn.ts @@ -233,12 +233,22 @@ export class SignIn extends BaseResource implements SignInResource { ): Promise => { const { strategy, redirectUrl, redirectUrlComplete, identifier, oidcPrompt, continueSignIn } = params || {}; + const redirectUrlWithAuthToken = SignIn.clerk.buildUrlWithAuth(redirectUrl); + + // When force organization selection is enabled, redirect to SSO callback route. + // This ensures organization selection tasks are displayed after sign-in, + // rather than redirecting to potentially unprotected pages while the session is pending. + const actionCompleteRedirectUrl = SignIn.clerk.__unstable__environment?.organizationSettings + .forceOrganizationSelection + ? redirectUrlWithAuthToken + : redirectUrlComplete; + if (!this.id || !continueSignIn) { await this.create({ strategy, identifier, - redirectUrl: SignIn.clerk.buildUrlWithAuth(redirectUrl), - actionCompleteRedirectUrl: redirectUrlComplete, + redirectUrl: redirectUrlWithAuthToken, + actionCompleteRedirectUrl, }); } @@ -246,7 +256,7 @@ export class SignIn extends BaseResource implements SignInResource { await this.prepareFirstFactor({ strategy, redirectUrl: SignIn.clerk.buildUrlWithAuth(redirectUrl), - actionCompleteRedirectUrl: redirectUrlComplete, + actionCompleteRedirectUrl, oidcPrompt, }); } diff --git a/packages/clerk-js/src/core/resources/SignUp.ts b/packages/clerk-js/src/core/resources/SignUp.ts index db934a95930..1023cd8a47d 100644 --- a/packages/clerk-js/src/core/resources/SignUp.ts +++ b/packages/clerk-js/src/core/resources/SignUp.ts @@ -288,11 +288,21 @@ export class SignUp extends BaseResource implements SignUpResource { oidcPrompt, } = params; + const redirectUrlWithAuthToken = SignUp.clerk.buildUrlWithAuth(redirectUrl); + + // When force organization selection is enabled, redirect to SSO callback route. + // This ensures organization selection tasks are displayed after sign-up, + // rather than redirecting to potentially unprotected pages while the session is pending. + const actionCompleteRedirectUrl = SignUp.clerk.__unstable__environment?.organizationSettings + .forceOrganizationSelection + ? redirectUrlWithAuthToken + : redirectUrlComplete; + const authenticateFn = () => { const authParams = { strategy, - redirectUrl: SignUp.clerk.buildUrlWithAuth(redirectUrl), - actionCompleteRedirectUrl: redirectUrlComplete, + redirectUrl: redirectUrlWithAuthToken, + actionCompleteRedirectUrl, unsafeMetadata, emailAddress, legalAccepted, From 9d4bb1e6748177d31912d3134b4482d134c79cb0 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Wed, 9 Jul 2025 14:46:00 -0300 Subject: [PATCH 2/3] Add E2E test for sign-in with SSO --- .../tests/session-tasks-sign-in.test.ts | 46 +++++++++++++++++-- 1 file changed, 42 insertions(+), 4 deletions(-) diff --git a/integration/tests/session-tasks-sign-in.test.ts b/integration/tests/session-tasks-sign-in.test.ts index 8d2c7dc79b6..0e4cbcbf2f5 100644 --- a/integration/tests/session-tasks-sign-in.test.ts +++ b/integration/tests/session-tasks-sign-in.test.ts @@ -1,9 +1,11 @@ +import { createClerkClient } from '@clerk/backend'; import { expect, test } from '@playwright/test'; import { appConfigs } from '../presets'; +import { instanceKeys } from '../presets/envs'; import type { FakeUser } from '../testUtils'; import { createTestUtils, testAgainstRunningApps } from '../testUtils'; -import type { FakeOrganization } from '../testUtils/organizationsService'; +import { createUserService } from '../testUtils/usersService'; testAgainstRunningApps({ withEnv: [appConfigs.envs.withSessionTasks] })( 'session tasks after sign-in flow @nextjs', @@ -11,12 +13,10 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withSessionTasks] })( test.describe.configure({ mode: 'serial' }); let fakeUser: FakeUser; - let fakeOrganization: FakeOrganization; test.beforeAll(async () => { const u = createTestUtils({ app }); fakeUser = u.services.users.createFakeUser(); - fakeOrganization = u.services.organizations.createFakeOrganization(); await u.services.users.createBapiUser(fakeUser); }); @@ -27,7 +27,7 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withSessionTasks] })( await app.teardown(); }); - test('navigate to task on after sign-in', async ({ page, context }) => { + test('with email and password, navigate to task on after sign-in', async ({ page, context }) => { const u = createTestUtils({ app, page, context }); // Performs sign-in @@ -43,11 +43,49 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withSessionTasks] })( expect(page.url()).toContain('tasks'); // Resolves task + const fakeOrganization = u.services.organizations.createFakeOrganization(); await u.po.sessionTask.resolveForceOrganizationSelectionTask(fakeOrganization); await u.po.expect.toHaveResolvedTask(); // Navigates to after sign-in await u.page.waitForAppUrl('/'); }); + + test('with sso, navigate to task on after sign-in', async ({ page, context }) => { + const u = createTestUtils({ app, page, context }); + + // Create a clerkClient for the OAuth provider instance + const client = createClerkClient({ + secretKey: instanceKeys.get('oauth-provider').sk, + publishableKey: instanceKeys.get('oauth-provider').pk, + }); + const users = createUserService(client); + fakeUser = users.createFakeUser({ + withUsername: true, + }); + // Create the user on the OAuth provider instance so we do not need to sign up twice + await users.createBapiUser(fakeUser); + + // Performs sign-in with SSO + await u.po.signIn.goTo(); + await u.page.getByRole('button', { name: 'E2E OAuth Provider' }).click(); + await u.page.getByText('Sign in to oauth-provider').waitFor(); + await u.po.signIn.setIdentifier(fakeUser.email); + await u.po.signIn.continue(); + await u.po.signIn.enterTestOtpCode(); + + // Resolves task + const fakeOrganization = u.services.organizations.createFakeOrganization(); + await u.po.sessionTask.resolveForceOrganizationSelectionTask(fakeOrganization); + await u.po.expect.toHaveResolvedTask(); + + // Navigates to after sign-in + await u.page.waitForAppUrl('/'); + + // Delete the user on the OAuth provider instance + await fakeUser.deleteIfExists(); + // Delete the user on the app instance. + await u.services.users.deleteIfExists({ email: fakeUser.email }); + }); }, ); From 6a47417c001d730eadea14bf7f8ba361758ef3dc Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Wed, 9 Jul 2025 14:56:15 -0300 Subject: [PATCH 3/3] Add E2E test for sign-up with SSO --- .../tests/session-tasks-sign-up.test.ts | 44 +++++++++++++++++-- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/integration/tests/session-tasks-sign-up.test.ts b/integration/tests/session-tasks-sign-up.test.ts index 680d949bc89..66283daf0aa 100644 --- a/integration/tests/session-tasks-sign-up.test.ts +++ b/integration/tests/session-tasks-sign-up.test.ts @@ -1,9 +1,11 @@ import { expect, test } from '@playwright/test'; +import { createClerkClient } from '@clerk/backend'; import { appConfigs } from '../presets'; +import { instanceKeys } from '../presets/envs'; import type { FakeUser } from '../testUtils'; import { createTestUtils, testAgainstRunningApps } from '../testUtils'; -import type { FakeOrganization } from '../testUtils/organizationsService'; +import { createUserService } from '../testUtils/usersService'; testAgainstRunningApps({ withEnv: [appConfigs.envs.withSessionTasks] })( 'session tasks after sign-up flow @nextjs', @@ -11,7 +13,6 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withSessionTasks] })( test.describe.configure({ mode: 'serial' }); let fakeUser: FakeUser; - let fakeOrganization: FakeOrganization; test.beforeAll(() => { const u = createTestUtils({ app }); @@ -20,7 +21,6 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withSessionTasks] })( withPhoneNumber: true, withUsername: true, }); - fakeOrganization = u.services.organizations.createFakeOrganization(); }); test.afterAll(async () => { @@ -45,11 +45,49 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withSessionTasks] })( expect(page.url()).toContain('tasks'); // Resolves task + const fakeOrganization = u.services.organizations.createFakeOrganization(); await u.po.sessionTask.resolveForceOrganizationSelectionTask(fakeOrganization); await u.po.expect.toHaveResolvedTask(); // Navigates to after sign-up await u.page.waitForAppUrl('/'); }); + + test('with sso, navigate to task on after sign-up', async ({ page, context }) => { + const u = createTestUtils({ app, page, context }); + + // Create a clerkClient for the OAuth provider instance + const client = createClerkClient({ + secretKey: instanceKeys.get('oauth-provider').sk, + publishableKey: instanceKeys.get('oauth-provider').pk, + }); + const users = createUserService(client); + fakeUser = users.createFakeUser({ + withUsername: true, + }); + // Create the user on the OAuth provider instance so we do not need to sign up twice + await users.createBapiUser(fakeUser); + + // Performs sign-up (transfer flow with sign-in) with SSO + await u.po.signIn.goTo(); + await u.page.getByRole('button', { name: 'E2E OAuth Provider' }).click(); + await u.page.getByText('Sign in to oauth-provider').waitFor(); + await u.po.signIn.setIdentifier(fakeUser.email); + await u.po.signIn.continue(); + await u.po.signIn.enterTestOtpCode(); + + // Resolves task + const fakeOrganization = u.services.organizations.createFakeOrganization(); + await u.po.sessionTask.resolveForceOrganizationSelectionTask(fakeOrganization); + await u.po.expect.toHaveResolvedTask(); + + // Navigates to after sign-up + await u.page.waitForAppUrl('/'); + + // Delete the user on the OAuth provider instance + await fakeUser.deleteIfExists(); + // Delete the user on the app instance. + await u.services.users.deleteIfExists({ email: fakeUser.email }); + }); }, );