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/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 }); + }); }, ); 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 }); + }); }, ); 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, pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy