Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/force-enterprise-sso-task.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@clerk/shared': minor
'@clerk/clerk-js': minor
'@clerk/ui': minor
'@clerk/localizations': minor
---

Introduce the `force-enterprise-sso` session task. When an organization requires its members to authenticate through its enterprise SSO connection, `<SignIn />` and `<SignUp />` now show a step that sends the user through that connection before the organization can be used. Switching to such an organization with `setActive` also routes to the task instead of leaving the session pending with nothing on screen. Moving from one session task to the next also keeps the flow's destination, so finishing the last task no longer returns to an earlier task's page and spins.
76 changes: 76 additions & 0 deletions packages/clerk-js/src/core/__tests__/clerk.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -644,6 +644,82 @@ describe('Clerk singleton', () => {
});
});

describe('when switching organization leaves the session pending', () => {
const pendingTask = { key: 'force-enterprise-sso' } as const;
const createSession = () => {
const session: any = {
id: '1',
remove: vi.fn(),
status: 'active',
user: {},
getToken: vi.fn(),
lastActiveToken: { getRawString: () => 'mocked-token' },
tasks: null,
currentTask: undefined,
};
const becomePending = () => {
session.status = 'pending';
session.tasks = [pendingTask];
session.currentTask = pendingTask;
return Promise.resolve();
};
session.touch = vi.fn(becomePending);
session.__internal_touch = vi.fn(becomePending);
return session;
};

it('navigates to the task on the sign-in page instead of the redirect url', async () => {
const mockSession = createSession();
mockClientFetch.mockReturnValue(Promise.resolve({ signedInSessions: [mockSession] }));

const sut = new Clerk(productionPublishableKey);
sut.navigate = vi.fn();
await sut.load({ signInUrl: '/sign-in' });
await sut.setActive({ organization: 'org_id', redirectUrl: '/dashboard' });

expect(sut.navigate).toHaveBeenCalledTimes(1);
expect(vi.mocked(sut.navigate).mock.calls[0][0]).toContain('/sign-in#/tasks/force-enterprise-sso');
});

it('navigates to the task even without a redirect url', async () => {
const mockSession = createSession();
mockClientFetch.mockReturnValue(Promise.resolve({ signedInSessions: [mockSession] }));

const sut = new Clerk(productionPublishableKey);
sut.navigate = vi.fn();
await sut.load({ signInUrl: '/sign-in' });
await sut.setActive({ organization: 'org_id' });

expect(vi.mocked(sut.navigate).mock.calls[0][0]).toContain('/sign-in#/tasks/force-enterprise-sso');
});

it('navigates to the `taskUrls` option for the task', async () => {
const mockSession = createSession();
mockClientFetch.mockReturnValue(Promise.resolve({ signedInSessions: [mockSession] }));

const sut = new Clerk(productionPublishableKey);
sut.navigate = vi.fn();
await sut.load({ taskUrls: { 'force-enterprise-sso': '/sso' } });
await sut.setActive({ organization: 'org_id' });

expect(sut.navigate).toHaveBeenCalledWith('/sso');
});

it('leaves navigation to the `navigate` callback', async () => {
const mockSession = createSession();
mockClientFetch.mockReturnValue(Promise.resolve({ signedInSessions: [mockSession] }));
const navigate = vi.fn();

const sut = new Clerk(productionPublishableKey);
sut.navigate = vi.fn();
await sut.load({ signInUrl: '/sign-in' });
await sut.setActive({ organization: 'org_id', navigate });

expect(navigate).toHaveBeenCalled();
expect(sut.navigate).not.toHaveBeenCalled();
});
});

describe('with force organization selection enabled', () => {
const mockSession = {
id: '1',
Expand Down
23 changes: 19 additions & 4 deletions packages/clerk-js/src/core/clerk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ import type {
ResumeAfterProtectCheckParams,
SDKMetadata,
SessionResource,
SessionTask,
SessionTouchParams,
SetActiveParams,
SignedInSessionResource,
Expand Down Expand Up @@ -2011,7 +2012,7 @@ export class Clerk implements ClerkInterface {
await onBeforeSetActive(newSession === null ? 'sign-out' : undefined);
}

const taskUrl =
let taskUrl =
newSession?.status === 'pending' &&
newSession?.currentTask &&
this.#options.taskUrls?.[newSession?.currentTask.key];
Expand Down Expand Up @@ -2061,6 +2062,16 @@ export class Clerk implements ClerkInterface {
newSession = this.#getSessionFromClient(newSession?.id, updatedClient);
}

if (
!taskUrl &&
!setActiveNavigate &&
shouldSwitchOrganization &&
newSession?.status === 'pending' &&
newSession.currentTask
) {
taskUrl = this.#buildTaskUrl(newSession.currentTask);
}

// getToken syncs __session and __client_uat to cookies using events.TokenUpdate dispatched event.
try {
const token = await newSession?.getToken();
Expand All @@ -2085,7 +2096,7 @@ export class Clerk implements ClerkInterface {
// automatic reloading when reloading shouldn't be happening.
const tracker = createBeforeUnloadTracker(this.#options.standardBrowser);

if (shouldNavigate) {
if (shouldNavigate || taskUrl) {
await tracker.track(async () => {
if (!this.client) {
// Typescript is not happy because since thinks this.client might have changed to undefined because the function is asynchronous.
Expand Down Expand Up @@ -2421,15 +2432,19 @@ export class Clerk implements ClerkInterface {
return '';
}

const customTaskUrl = this.#options.taskUrls?.[currentTask.key];
return this.#buildTaskUrl(currentTask, options);
}

#buildTaskUrl(task: SessionTask, options?: TasksRedirectOptions): string {
const customTaskUrl = this.#options.taskUrls?.[task.key];
if (customTaskUrl) {
return customTaskUrl;
}

return buildURL(
{
base: this.buildSignInUrl(options),
hashPath: getTaskEndpoint(currentTask),
hashPath: getTaskEndpoint(task),
},
{
stringify: true,
Expand Down
10 changes: 10 additions & 0 deletions packages/localizations/src/ar-SA.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1992,6 +1992,16 @@ export const arSA: LocalizationResource = {
actionText: 'تم تسجيل الدخول كـ {{identifier}}',
},
},
taskForceEnterpriseSso: {
connectionUnavailable: undefined,
formButtonPrimary: undefined,
signOut: {
actionLink: undefined,
actionText: undefined,
},
subtitle: undefined,
title: undefined,
},
taskResetPassword: {
formButtonPrimary: undefined,
signOut: {
Expand Down
10 changes: 10 additions & 0 deletions packages/localizations/src/be-BY.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2001,6 +2001,16 @@ export const beBY: LocalizationResource = {
actionText: 'Увайшлі як {{identifier}}',
},
},
taskForceEnterpriseSso: {
connectionUnavailable: undefined,
formButtonPrimary: undefined,
signOut: {
actionLink: undefined,
actionText: undefined,
},
subtitle: undefined,
title: undefined,
},
taskResetPassword: {
formButtonPrimary: undefined,
signOut: {
Expand Down
10 changes: 10 additions & 0 deletions packages/localizations/src/bg-BG.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1997,6 +1997,16 @@ export const bgBG: LocalizationResource = {
actionText: 'Влязъл като {{identifier}}',
},
},
taskForceEnterpriseSso: {
connectionUnavailable: undefined,
formButtonPrimary: undefined,
signOut: {
actionLink: undefined,
actionText: undefined,
},
subtitle: undefined,
title: undefined,
},
taskResetPassword: {
formButtonPrimary: undefined,
signOut: {
Expand Down
10 changes: 10 additions & 0 deletions packages/localizations/src/bn-IN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2007,6 +2007,16 @@ export const bnIN: LocalizationResource = {
actionText: '{{identifier}} হিসাবে সাইন ইন করা হয়েছে',
},
},
taskForceEnterpriseSso: {
connectionUnavailable: undefined,
formButtonPrimary: undefined,
signOut: {
actionLink: undefined,
actionText: undefined,
},
subtitle: undefined,
title: undefined,
},
taskResetPassword: {
formButtonPrimary: 'পাসওয়ার্ড রিসেট করুন',
signOut: {
Expand Down
10 changes: 10 additions & 0 deletions packages/localizations/src/ca-ES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2005,6 +2005,16 @@ export const caES: LocalizationResource = {
actionText: 'Sessió iniciada com a {{identifier}}',
},
},
taskForceEnterpriseSso: {
connectionUnavailable: undefined,
formButtonPrimary: undefined,
signOut: {
actionLink: undefined,
actionText: undefined,
},
subtitle: undefined,
title: undefined,
},
taskResetPassword: {
formButtonPrimary: undefined,
signOut: {
Expand Down
10 changes: 10 additions & 0 deletions packages/localizations/src/cs-CZ.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2007,6 +2007,16 @@ export const csCZ: LocalizationResource = {
actionText: 'Přihlášen jako {{identifier}}',
},
},
taskForceEnterpriseSso: {
connectionUnavailable: undefined,
formButtonPrimary: undefined,
signOut: {
actionLink: undefined,
actionText: undefined,
},
subtitle: undefined,
title: undefined,
},
taskResetPassword: {
formButtonPrimary: undefined,
signOut: {
Expand Down
10 changes: 10 additions & 0 deletions packages/localizations/src/da-DK.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1993,6 +1993,16 @@ export const daDK: LocalizationResource = {
actionText: 'Logget ind som {{identifier}}',
},
},
taskForceEnterpriseSso: {
connectionUnavailable: undefined,
formButtonPrimary: undefined,
signOut: {
actionLink: undefined,
actionText: undefined,
},
subtitle: undefined,
title: undefined,
},
taskResetPassword: {
formButtonPrimary: undefined,
signOut: {
Expand Down
10 changes: 10 additions & 0 deletions packages/localizations/src/de-DE.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2014,6 +2014,16 @@ export const deDE: LocalizationResource = {
actionText: 'Angemeldet als {{identifier}}',
},
},
taskForceEnterpriseSso: {
connectionUnavailable: undefined,
formButtonPrimary: undefined,
signOut: {
actionLink: undefined,
actionText: undefined,
},
subtitle: undefined,
title: undefined,
},
taskResetPassword: {
formButtonPrimary: undefined,
signOut: {
Expand Down
10 changes: 10 additions & 0 deletions packages/localizations/src/el-GR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2007,6 +2007,16 @@ export const elGR: LocalizationResource = {
actionText: 'Συνδεδεμένος ως {{identifier}}',
},
},
taskForceEnterpriseSso: {
connectionUnavailable: undefined,
formButtonPrimary: undefined,
signOut: {
actionLink: undefined,
actionText: undefined,
},
subtitle: undefined,
title: undefined,
},
taskResetPassword: {
formButtonPrimary: 'Επαναφορά κωδικού πρόσβασης',
signOut: {
Expand Down
10 changes: 10 additions & 0 deletions packages/localizations/src/en-GB.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1997,6 +1997,16 @@ export const enGB: LocalizationResource = {
actionText: 'Signed in as {{identifier}}',
},
},
taskForceEnterpriseSso: {
connectionUnavailable: undefined,
formButtonPrimary: undefined,
signOut: {
actionLink: undefined,
actionText: undefined,
},
subtitle: undefined,
title: undefined,
},
taskResetPassword: {
formButtonPrimary: undefined,
signOut: {
Expand Down
10 changes: 10 additions & 0 deletions packages/localizations/src/en-US.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2044,6 +2044,16 @@ export const enUS: LocalizationResource = {
actionText: 'Signed in as {{identifier}}',
},
},
taskForceEnterpriseSso: {
connectionUnavailable: "Your organization's SSO connection isn't available. Contact your administrator.",
formButtonPrimary: 'Continue with {{connectionName}}',
signOut: {
actionLink: 'Sign out',
actionText: 'Signed in as {{identifier}}',
},
subtitle: '{{organizationName}} requires you to sign in with your organization account to continue.',
title: 'Continue with SSO',
},
taskResetPassword: {
formButtonPrimary: 'Reset Password',
signOut: {
Expand Down
10 changes: 10 additions & 0 deletions packages/localizations/src/es-CR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2004,6 +2004,16 @@ export const esCR: LocalizationResource = {
actionText: 'Conectado como {{identifier}}',
},
},
taskForceEnterpriseSso: {
connectionUnavailable: undefined,
formButtonPrimary: undefined,
signOut: {
actionLink: undefined,
actionText: undefined,
},
subtitle: undefined,
title: undefined,
},
taskResetPassword: {
formButtonPrimary: undefined,
signOut: {
Expand Down
10 changes: 10 additions & 0 deletions packages/localizations/src/es-ES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2005,6 +2005,16 @@ export const esES: LocalizationResource = {
actionText: 'Sesión iniciada como {{identifier}}',
},
},
taskForceEnterpriseSso: {
connectionUnavailable: undefined,
formButtonPrimary: undefined,
signOut: {
actionLink: undefined,
actionText: undefined,
},
subtitle: undefined,
title: undefined,
},
taskResetPassword: {
formButtonPrimary: undefined,
signOut: {
Expand Down
10 changes: 10 additions & 0 deletions packages/localizations/src/es-MX.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2005,6 +2005,16 @@ export const esMX: LocalizationResource = {
actionText: 'Registrado como {{identifier}}',
},
},
taskForceEnterpriseSso: {
connectionUnavailable: undefined,
formButtonPrimary: undefined,
signOut: {
actionLink: undefined,
actionText: undefined,
},
subtitle: undefined,
title: undefined,
},
taskResetPassword: {
formButtonPrimary: undefined,
signOut: {
Expand Down
Loading
Loading