Skip to content

Commit 32a4952

Browse files
authored
fix(clerk-js): Force redirect to sso-callback if force-an-org is enabled (#6271)
1 parent 02a00ba commit 32a4952

File tree

5 files changed

+113
-12
lines changed

5 files changed

+113
-12
lines changed

.changeset/public-hats-relax.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@clerk/clerk-js': patch
3+
---
4+
5+
Force redirect to SSO callback route when force-an-org is enabled, ensuring task display and organization selection

integration/tests/session-tasks-sign-in.test.ts

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,22 @@
1+
import { createClerkClient } from '@clerk/backend';
12
import { expect, test } from '@playwright/test';
23

34
import { appConfigs } from '../presets';
5+
import { instanceKeys } from '../presets/envs';
46
import type { FakeUser } from '../testUtils';
57
import { createTestUtils, testAgainstRunningApps } from '../testUtils';
6-
import type { FakeOrganization } from '../testUtils/organizationsService';
8+
import { createUserService } from '../testUtils/usersService';
79

810
testAgainstRunningApps({ withEnv: [appConfigs.envs.withSessionTasks] })(
911
'session tasks after sign-in flow @nextjs',
1012
({ app }) => {
1113
test.describe.configure({ mode: 'serial' });
1214

1315
let fakeUser: FakeUser;
14-
let fakeOrganization: FakeOrganization;
1516

1617
test.beforeAll(async () => {
1718
const u = createTestUtils({ app });
1819
fakeUser = u.services.users.createFakeUser();
19-
fakeOrganization = u.services.organizations.createFakeOrganization();
2020
await u.services.users.createBapiUser(fakeUser);
2121
});
2222

@@ -27,7 +27,7 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withSessionTasks] })(
2727
await app.teardown();
2828
});
2929

30-
test('navigate to task on after sign-in', async ({ page, context }) => {
30+
test('with email and password, navigate to task on after sign-in', async ({ page, context }) => {
3131
const u = createTestUtils({ app, page, context });
3232

3333
// Performs sign-in
@@ -43,11 +43,49 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withSessionTasks] })(
4343
expect(page.url()).toContain('tasks');
4444

4545
// Resolves task
46+
const fakeOrganization = u.services.organizations.createFakeOrganization();
4647
await u.po.sessionTask.resolveForceOrganizationSelectionTask(fakeOrganization);
4748
await u.po.expect.toHaveResolvedTask();
4849

4950
// Navigates to after sign-in
5051
await u.page.waitForAppUrl('/');
5152
});
53+
54+
test('with sso, navigate to task on after sign-in', async ({ page, context }) => {
55+
const u = createTestUtils({ app, page, context });
56+
57+
// Create a clerkClient for the OAuth provider instance
58+
const client = createClerkClient({
59+
secretKey: instanceKeys.get('oauth-provider').sk,
60+
publishableKey: instanceKeys.get('oauth-provider').pk,
61+
});
62+
const users = createUserService(client);
63+
fakeUser = users.createFakeUser({
64+
withUsername: true,
65+
});
66+
// Create the user on the OAuth provider instance so we do not need to sign up twice
67+
await users.createBapiUser(fakeUser);
68+
69+
// Performs sign-in with SSO
70+
await u.po.signIn.goTo();
71+
await u.page.getByRole('button', { name: 'E2E OAuth Provider' }).click();
72+
await u.page.getByText('Sign in to oauth-provider').waitFor();
73+
await u.po.signIn.setIdentifier(fakeUser.email);
74+
await u.po.signIn.continue();
75+
await u.po.signIn.enterTestOtpCode();
76+
77+
// Resolves task
78+
const fakeOrganization = u.services.organizations.createFakeOrganization();
79+
await u.po.sessionTask.resolveForceOrganizationSelectionTask(fakeOrganization);
80+
await u.po.expect.toHaveResolvedTask();
81+
82+
// Navigates to after sign-in
83+
await u.page.waitForAppUrl('/');
84+
85+
// Delete the user on the OAuth provider instance
86+
await fakeUser.deleteIfExists();
87+
// Delete the user on the app instance.
88+
await u.services.users.deleteIfExists({ email: fakeUser.email });
89+
});
5290
},
5391
);

integration/tests/session-tasks-sign-up.test.ts

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
11
import { expect, test } from '@playwright/test';
22

3+
import { createClerkClient } from '@clerk/backend';
34
import { appConfigs } from '../presets';
5+
import { instanceKeys } from '../presets/envs';
46
import type { FakeUser } from '../testUtils';
57
import { createTestUtils, testAgainstRunningApps } from '../testUtils';
6-
import type { FakeOrganization } from '../testUtils/organizationsService';
8+
import { createUserService } from '../testUtils/usersService';
79

810
testAgainstRunningApps({ withEnv: [appConfigs.envs.withSessionTasks] })(
911
'session tasks after sign-up flow @nextjs',
1012
({ app }) => {
1113
test.describe.configure({ mode: 'serial' });
1214

1315
let fakeUser: FakeUser;
14-
let fakeOrganization: FakeOrganization;
1516

1617
test.beforeAll(() => {
1718
const u = createTestUtils({ app });
@@ -20,7 +21,6 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withSessionTasks] })(
2021
withPhoneNumber: true,
2122
withUsername: true,
2223
});
23-
fakeOrganization = u.services.organizations.createFakeOrganization();
2424
});
2525

2626
test.afterAll(async () => {
@@ -45,11 +45,49 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withSessionTasks] })(
4545
expect(page.url()).toContain('tasks');
4646

4747
// Resolves task
48+
const fakeOrganization = u.services.organizations.createFakeOrganization();
4849
await u.po.sessionTask.resolveForceOrganizationSelectionTask(fakeOrganization);
4950
await u.po.expect.toHaveResolvedTask();
5051

5152
// Navigates to after sign-up
5253
await u.page.waitForAppUrl('/');
5354
});
55+
56+
test('with sso, navigate to task on after sign-up', async ({ page, context }) => {
57+
const u = createTestUtils({ app, page, context });
58+
59+
// Create a clerkClient for the OAuth provider instance
60+
const client = createClerkClient({
61+
secretKey: instanceKeys.get('oauth-provider').sk,
62+
publishableKey: instanceKeys.get('oauth-provider').pk,
63+
});
64+
const users = createUserService(client);
65+
fakeUser = users.createFakeUser({
66+
withUsername: true,
67+
});
68+
// Create the user on the OAuth provider instance so we do not need to sign up twice
69+
await users.createBapiUser(fakeUser);
70+
71+
// Performs sign-up (transfer flow with sign-in) with SSO
72+
await u.po.signIn.goTo();
73+
await u.page.getByRole('button', { name: 'E2E OAuth Provider' }).click();
74+
await u.page.getByText('Sign in to oauth-provider').waitFor();
75+
await u.po.signIn.setIdentifier(fakeUser.email);
76+
await u.po.signIn.continue();
77+
await u.po.signIn.enterTestOtpCode();
78+
79+
// Resolves task
80+
const fakeOrganization = u.services.organizations.createFakeOrganization();
81+
await u.po.sessionTask.resolveForceOrganizationSelectionTask(fakeOrganization);
82+
await u.po.expect.toHaveResolvedTask();
83+
84+
// Navigates to after sign-up
85+
await u.page.waitForAppUrl('/');
86+
87+
// Delete the user on the OAuth provider instance
88+
await fakeUser.deleteIfExists();
89+
// Delete the user on the app instance.
90+
await u.services.users.deleteIfExists({ email: fakeUser.email });
91+
});
5492
},
5593
);

packages/clerk-js/src/core/resources/SignIn.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -233,20 +233,30 @@ export class SignIn extends BaseResource implements SignInResource {
233233
): Promise<void> => {
234234
const { strategy, redirectUrl, redirectUrlComplete, identifier, oidcPrompt, continueSignIn } = params || {};
235235

236+
const redirectUrlWithAuthToken = SignIn.clerk.buildUrlWithAuth(redirectUrl);
237+
238+
// When force organization selection is enabled, redirect to SSO callback route.
239+
// This ensures organization selection tasks are displayed after sign-in,
240+
// rather than redirecting to potentially unprotected pages while the session is pending.
241+
const actionCompleteRedirectUrl = SignIn.clerk.__unstable__environment?.organizationSettings
242+
.forceOrganizationSelection
243+
? redirectUrlWithAuthToken
244+
: redirectUrlComplete;
245+
236246
if (!this.id || !continueSignIn) {
237247
await this.create({
238248
strategy,
239249
identifier,
240-
redirectUrl: SignIn.clerk.buildUrlWithAuth(redirectUrl),
241-
actionCompleteRedirectUrl: redirectUrlComplete,
250+
redirectUrl: redirectUrlWithAuthToken,
251+
actionCompleteRedirectUrl,
242252
});
243253
}
244254

245255
if (strategy === 'saml' || strategy === 'enterprise_sso') {
246256
await this.prepareFirstFactor({
247257
strategy,
248258
redirectUrl: SignIn.clerk.buildUrlWithAuth(redirectUrl),
249-
actionCompleteRedirectUrl: redirectUrlComplete,
259+
actionCompleteRedirectUrl,
250260
oidcPrompt,
251261
});
252262
}

packages/clerk-js/src/core/resources/SignUp.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -288,11 +288,21 @@ export class SignUp extends BaseResource implements SignUpResource {
288288
oidcPrompt,
289289
} = params;
290290

291+
const redirectUrlWithAuthToken = SignUp.clerk.buildUrlWithAuth(redirectUrl);
292+
293+
// When force organization selection is enabled, redirect to SSO callback route.
294+
// This ensures organization selection tasks are displayed after sign-up,
295+
// rather than redirecting to potentially unprotected pages while the session is pending.
296+
const actionCompleteRedirectUrl = SignUp.clerk.__unstable__environment?.organizationSettings
297+
.forceOrganizationSelection
298+
? redirectUrlWithAuthToken
299+
: redirectUrlComplete;
300+
291301
const authenticateFn = () => {
292302
const authParams = {
293303
strategy,
294-
redirectUrl: SignUp.clerk.buildUrlWithAuth(redirectUrl),
295-
actionCompleteRedirectUrl: redirectUrlComplete,
304+
redirectUrl: redirectUrlWithAuthToken,
305+
actionCompleteRedirectUrl,
296306
unsafeMetadata,
297307
emailAddress,
298308
legalAccepted,

0 commit comments

Comments
 (0)