diff --git a/.changeset/force-enterprise-sso-task.md b/.changeset/force-enterprise-sso-task.md
new file mode 100644
index 00000000000..5c80ba9c5c3
--- /dev/null
+++ b/.changeset/force-enterprise-sso-task.md
@@ -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, `` and `` 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.
diff --git a/packages/clerk-js/src/core/__tests__/clerk.test.ts b/packages/clerk-js/src/core/__tests__/clerk.test.ts
index 314c28a7ccf..af70cb5a013 100644
--- a/packages/clerk-js/src/core/__tests__/clerk.test.ts
+++ b/packages/clerk-js/src/core/__tests__/clerk.test.ts
@@ -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',
diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts
index d124caf535e..2a3080dd6b1 100644
--- a/packages/clerk-js/src/core/clerk.ts
+++ b/packages/clerk-js/src/core/clerk.ts
@@ -120,6 +120,7 @@ import type {
ResumeAfterProtectCheckParams,
SDKMetadata,
SessionResource,
+ SessionTask,
SessionTouchParams,
SetActiveParams,
SignedInSessionResource,
@@ -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];
@@ -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();
@@ -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.
@@ -2421,7 +2432,11 @@ 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;
}
@@ -2429,7 +2444,7 @@ export class Clerk implements ClerkInterface {
return buildURL(
{
base: this.buildSignInUrl(options),
- hashPath: getTaskEndpoint(currentTask),
+ hashPath: getTaskEndpoint(task),
},
{
stringify: true,
diff --git a/packages/localizations/src/ar-SA.ts b/packages/localizations/src/ar-SA.ts
index cc7882eab1a..89de42d2233 100644
--- a/packages/localizations/src/ar-SA.ts
+++ b/packages/localizations/src/ar-SA.ts
@@ -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: {
diff --git a/packages/localizations/src/be-BY.ts b/packages/localizations/src/be-BY.ts
index c95417dc549..a46f977584d 100644
--- a/packages/localizations/src/be-BY.ts
+++ b/packages/localizations/src/be-BY.ts
@@ -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: {
diff --git a/packages/localizations/src/bg-BG.ts b/packages/localizations/src/bg-BG.ts
index a2e222547eb..2b2088f002b 100644
--- a/packages/localizations/src/bg-BG.ts
+++ b/packages/localizations/src/bg-BG.ts
@@ -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: {
diff --git a/packages/localizations/src/bn-IN.ts b/packages/localizations/src/bn-IN.ts
index 2cb0e9dec42..0a0da8c2d5a 100644
--- a/packages/localizations/src/bn-IN.ts
+++ b/packages/localizations/src/bn-IN.ts
@@ -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: {
diff --git a/packages/localizations/src/ca-ES.ts b/packages/localizations/src/ca-ES.ts
index fd6720cff57..25445c47f3d 100644
--- a/packages/localizations/src/ca-ES.ts
+++ b/packages/localizations/src/ca-ES.ts
@@ -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: {
diff --git a/packages/localizations/src/cs-CZ.ts b/packages/localizations/src/cs-CZ.ts
index e025627f5bc..2696de8b726 100644
--- a/packages/localizations/src/cs-CZ.ts
+++ b/packages/localizations/src/cs-CZ.ts
@@ -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: {
diff --git a/packages/localizations/src/da-DK.ts b/packages/localizations/src/da-DK.ts
index 6e0d81a20b2..27e1e89b1ce 100644
--- a/packages/localizations/src/da-DK.ts
+++ b/packages/localizations/src/da-DK.ts
@@ -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: {
diff --git a/packages/localizations/src/de-DE.ts b/packages/localizations/src/de-DE.ts
index 4bc781d2353..14b9fd29ce8 100644
--- a/packages/localizations/src/de-DE.ts
+++ b/packages/localizations/src/de-DE.ts
@@ -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: {
diff --git a/packages/localizations/src/el-GR.ts b/packages/localizations/src/el-GR.ts
index 8bf87a2b91f..9aa3089c025 100644
--- a/packages/localizations/src/el-GR.ts
+++ b/packages/localizations/src/el-GR.ts
@@ -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: {
diff --git a/packages/localizations/src/en-GB.ts b/packages/localizations/src/en-GB.ts
index 0dfddf53585..1f3f0064b82 100644
--- a/packages/localizations/src/en-GB.ts
+++ b/packages/localizations/src/en-GB.ts
@@ -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: {
diff --git a/packages/localizations/src/en-US.ts b/packages/localizations/src/en-US.ts
index 7e862ea758e..1a7b1754830 100644
--- a/packages/localizations/src/en-US.ts
+++ b/packages/localizations/src/en-US.ts
@@ -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: {
diff --git a/packages/localizations/src/es-CR.ts b/packages/localizations/src/es-CR.ts
index 0db7fbb5555..8d85feaad49 100644
--- a/packages/localizations/src/es-CR.ts
+++ b/packages/localizations/src/es-CR.ts
@@ -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: {
diff --git a/packages/localizations/src/es-ES.ts b/packages/localizations/src/es-ES.ts
index c8f7d6d7b89..ed62f497c5f 100644
--- a/packages/localizations/src/es-ES.ts
+++ b/packages/localizations/src/es-ES.ts
@@ -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: {
diff --git a/packages/localizations/src/es-MX.ts b/packages/localizations/src/es-MX.ts
index 713bc34b6c9..660fcee4d8e 100644
--- a/packages/localizations/src/es-MX.ts
+++ b/packages/localizations/src/es-MX.ts
@@ -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: {
diff --git a/packages/localizations/src/es-UY.ts b/packages/localizations/src/es-UY.ts
index 3e72c1ee734..929b72508a9 100644
--- a/packages/localizations/src/es-UY.ts
+++ b/packages/localizations/src/es-UY.ts
@@ -2004,6 +2004,16 @@ export const esUY: LocalizationResource = {
actionText: 'Logueado como {{identifier}}',
},
},
+ taskForceEnterpriseSso: {
+ connectionUnavailable: undefined,
+ formButtonPrimary: undefined,
+ signOut: {
+ actionLink: undefined,
+ actionText: undefined,
+ },
+ subtitle: undefined,
+ title: undefined,
+ },
taskResetPassword: {
formButtonPrimary: undefined,
signOut: {
diff --git a/packages/localizations/src/fa-IR.ts b/packages/localizations/src/fa-IR.ts
index a255fe5baf8..29292f743bd 100644
--- a/packages/localizations/src/fa-IR.ts
+++ b/packages/localizations/src/fa-IR.ts
@@ -2008,6 +2008,16 @@ export const faIR: LocalizationResource = {
actionText: 'میخواهید خارج شوید؟',
},
},
+ taskForceEnterpriseSso: {
+ connectionUnavailable: undefined,
+ formButtonPrimary: undefined,
+ signOut: {
+ actionLink: undefined,
+ actionText: undefined,
+ },
+ subtitle: undefined,
+ title: undefined,
+ },
taskResetPassword: {
formButtonPrimary: undefined,
signOut: {
diff --git a/packages/localizations/src/fi-FI.ts b/packages/localizations/src/fi-FI.ts
index 7f2d2139608..68f808431df 100644
--- a/packages/localizations/src/fi-FI.ts
+++ b/packages/localizations/src/fi-FI.ts
@@ -2009,6 +2009,16 @@ export const fiFI: LocalizationResource = {
actionText: 'Kirjautuneena käyttäjänä {{identifier}}',
},
},
+ taskForceEnterpriseSso: {
+ connectionUnavailable: undefined,
+ formButtonPrimary: undefined,
+ signOut: {
+ actionLink: undefined,
+ actionText: undefined,
+ },
+ subtitle: undefined,
+ title: undefined,
+ },
taskResetPassword: {
formButtonPrimary: 'Nollaa salasana',
signOut: {
diff --git a/packages/localizations/src/fr-FR.ts b/packages/localizations/src/fr-FR.ts
index 0c7d92c392e..eaeefc01d64 100644
--- a/packages/localizations/src/fr-FR.ts
+++ b/packages/localizations/src/fr-FR.ts
@@ -2013,6 +2013,16 @@ export const frFR: LocalizationResource = {
actionText: 'Connecté en tant que {{identifier}}',
},
},
+ taskForceEnterpriseSso: {
+ connectionUnavailable: undefined,
+ formButtonPrimary: undefined,
+ signOut: {
+ actionLink: undefined,
+ actionText: undefined,
+ },
+ subtitle: undefined,
+ title: undefined,
+ },
taskResetPassword: {
formButtonPrimary: undefined,
signOut: {
diff --git a/packages/localizations/src/he-IL.ts b/packages/localizations/src/he-IL.ts
index a27db252446..6eb0e94e53f 100644
--- a/packages/localizations/src/he-IL.ts
+++ b/packages/localizations/src/he-IL.ts
@@ -1986,6 +1986,16 @@ export const heIL: LocalizationResource = {
actionText: 'מחובר כ-{{identifier}}',
},
},
+ taskForceEnterpriseSso: {
+ connectionUnavailable: undefined,
+ formButtonPrimary: undefined,
+ signOut: {
+ actionLink: undefined,
+ actionText: undefined,
+ },
+ subtitle: undefined,
+ title: undefined,
+ },
taskResetPassword: {
formButtonPrimary: undefined,
signOut: {
diff --git a/packages/localizations/src/hi-IN.ts b/packages/localizations/src/hi-IN.ts
index 78251d7e485..7223517c132 100644
--- a/packages/localizations/src/hi-IN.ts
+++ b/packages/localizations/src/hi-IN.ts
@@ -2008,6 +2008,16 @@ export const hiIN: LocalizationResource = {
actionText: '{{identifier}} के रूप में साइन इन किया गया',
},
},
+ taskForceEnterpriseSso: {
+ connectionUnavailable: undefined,
+ formButtonPrimary: undefined,
+ signOut: {
+ actionLink: undefined,
+ actionText: undefined,
+ },
+ subtitle: undefined,
+ title: undefined,
+ },
taskResetPassword: {
formButtonPrimary: 'पासवर्ड रीसेट करें',
signOut: {
diff --git a/packages/localizations/src/hr-HR.ts b/packages/localizations/src/hr-HR.ts
index d0976af81e5..552269cf9f4 100644
--- a/packages/localizations/src/hr-HR.ts
+++ b/packages/localizations/src/hr-HR.ts
@@ -2010,6 +2010,16 @@ export const hrHR: LocalizationResource = {
actionText: 'Prijavljen kao {{identifier}}',
},
},
+ taskForceEnterpriseSso: {
+ connectionUnavailable: undefined,
+ formButtonPrimary: undefined,
+ signOut: {
+ actionLink: undefined,
+ actionText: undefined,
+ },
+ subtitle: undefined,
+ title: undefined,
+ },
taskResetPassword: {
formButtonPrimary: 'Resetiraj lozinku',
signOut: {
diff --git a/packages/localizations/src/hu-HU.ts b/packages/localizations/src/hu-HU.ts
index eb5592c2763..9c4c949fd26 100644
--- a/packages/localizations/src/hu-HU.ts
+++ b/packages/localizations/src/hu-HU.ts
@@ -2012,6 +2012,16 @@ export const huHU: LocalizationResource = {
actionText: 'Bejelentkezve: {{identifier}}',
},
},
+ taskForceEnterpriseSso: {
+ connectionUnavailable: undefined,
+ formButtonPrimary: undefined,
+ signOut: {
+ actionLink: undefined,
+ actionText: undefined,
+ },
+ subtitle: undefined,
+ title: undefined,
+ },
taskResetPassword: {
formButtonPrimary: 'Jelszó visszaállítása',
signOut: {
diff --git a/packages/localizations/src/id-ID.ts b/packages/localizations/src/id-ID.ts
index e17e483497b..d8c3ab7b3c8 100644
--- a/packages/localizations/src/id-ID.ts
+++ b/packages/localizations/src/id-ID.ts
@@ -2003,6 +2003,16 @@ export const idID: LocalizationResource = {
actionText: 'Masuk sebagai {{identifier}}',
},
},
+ taskForceEnterpriseSso: {
+ connectionUnavailable: undefined,
+ formButtonPrimary: undefined,
+ signOut: {
+ actionLink: undefined,
+ actionText: undefined,
+ },
+ subtitle: undefined,
+ title: undefined,
+ },
taskResetPassword: {
formButtonPrimary: undefined,
signOut: {
diff --git a/packages/localizations/src/is-IS.ts b/packages/localizations/src/is-IS.ts
index f468b8838d9..421b51c7bc4 100644
--- a/packages/localizations/src/is-IS.ts
+++ b/packages/localizations/src/is-IS.ts
@@ -2010,6 +2010,16 @@ export const isIS: LocalizationResource = {
actionText: 'Skráður inn sem {{identifier}}',
},
},
+ taskForceEnterpriseSso: {
+ connectionUnavailable: undefined,
+ formButtonPrimary: undefined,
+ signOut: {
+ actionLink: undefined,
+ actionText: undefined,
+ },
+ subtitle: undefined,
+ title: undefined,
+ },
taskResetPassword: {
formButtonPrimary: 'Endurstilla lykilorð',
signOut: {
diff --git a/packages/localizations/src/it-IT.ts b/packages/localizations/src/it-IT.ts
index ac1f77d29c5..f5b22542a61 100644
--- a/packages/localizations/src/it-IT.ts
+++ b/packages/localizations/src/it-IT.ts
@@ -2005,6 +2005,16 @@ export const itIT: LocalizationResource = {
actionText: 'Accesso effettuato come {{identifier}}',
},
},
+ taskForceEnterpriseSso: {
+ connectionUnavailable: undefined,
+ formButtonPrimary: undefined,
+ signOut: {
+ actionLink: undefined,
+ actionText: undefined,
+ },
+ subtitle: undefined,
+ title: undefined,
+ },
taskResetPassword: {
formButtonPrimary: undefined,
signOut: {
diff --git a/packages/localizations/src/ja-JP.ts b/packages/localizations/src/ja-JP.ts
index 1fcea5854dc..6bcfea3978e 100644
--- a/packages/localizations/src/ja-JP.ts
+++ b/packages/localizations/src/ja-JP.ts
@@ -2008,6 +2008,16 @@ export const jaJP: LocalizationResource = {
actionText: '{{identifier}} としてサインイン中',
},
},
+ taskForceEnterpriseSso: {
+ connectionUnavailable: undefined,
+ formButtonPrimary: undefined,
+ signOut: {
+ actionLink: undefined,
+ actionText: undefined,
+ },
+ subtitle: undefined,
+ title: undefined,
+ },
taskResetPassword: {
formButtonPrimary: 'パスワードをリセット',
signOut: {
diff --git a/packages/localizations/src/kk-KZ.ts b/packages/localizations/src/kk-KZ.ts
index 70735f967e9..e7ffc757b1a 100644
--- a/packages/localizations/src/kk-KZ.ts
+++ b/packages/localizations/src/kk-KZ.ts
@@ -1988,6 +1988,16 @@ export const kkKZ: LocalizationResource = {
actionText: '{{identifier}} ретінде кірді',
},
},
+ taskForceEnterpriseSso: {
+ connectionUnavailable: undefined,
+ formButtonPrimary: undefined,
+ signOut: {
+ actionLink: undefined,
+ actionText: undefined,
+ },
+ subtitle: undefined,
+ title: undefined,
+ },
taskResetPassword: {
formButtonPrimary: undefined,
signOut: {
diff --git a/packages/localizations/src/ko-KR.ts b/packages/localizations/src/ko-KR.ts
index 78896999f98..d1756f27722 100644
--- a/packages/localizations/src/ko-KR.ts
+++ b/packages/localizations/src/ko-KR.ts
@@ -1991,6 +1991,16 @@ export const koKR: LocalizationResource = {
actionText: '{{identifier}}로 로그인됨',
},
},
+ taskForceEnterpriseSso: {
+ connectionUnavailable: undefined,
+ formButtonPrimary: undefined,
+ signOut: {
+ actionLink: undefined,
+ actionText: undefined,
+ },
+ subtitle: undefined,
+ title: undefined,
+ },
taskResetPassword: {
formButtonPrimary: '비밀번호 재설정',
signOut: {
diff --git a/packages/localizations/src/mn-MN.ts b/packages/localizations/src/mn-MN.ts
index c1e2bed88b7..605e5415a7a 100644
--- a/packages/localizations/src/mn-MN.ts
+++ b/packages/localizations/src/mn-MN.ts
@@ -1997,6 +1997,16 @@ export const mnMN: LocalizationResource = {
actionText: '{{identifier}} гэж нэвтэрсэн',
},
},
+ taskForceEnterpriseSso: {
+ connectionUnavailable: undefined,
+ formButtonPrimary: undefined,
+ signOut: {
+ actionLink: undefined,
+ actionText: undefined,
+ },
+ subtitle: undefined,
+ title: undefined,
+ },
taskResetPassword: {
formButtonPrimary: undefined,
signOut: {
diff --git a/packages/localizations/src/ms-MY.ts b/packages/localizations/src/ms-MY.ts
index 016bc8c0743..2f803803006 100644
--- a/packages/localizations/src/ms-MY.ts
+++ b/packages/localizations/src/ms-MY.ts
@@ -2013,6 +2013,16 @@ export const msMY: LocalizationResource = {
actionText: 'Log masuk sebagai {{identifier}}',
},
},
+ taskForceEnterpriseSso: {
+ connectionUnavailable: undefined,
+ formButtonPrimary: undefined,
+ signOut: {
+ actionLink: undefined,
+ actionText: undefined,
+ },
+ subtitle: undefined,
+ title: undefined,
+ },
taskResetPassword: {
formButtonPrimary: 'Tetapkan Semula Kata Laluan',
signOut: {
diff --git a/packages/localizations/src/nb-NO.ts b/packages/localizations/src/nb-NO.ts
index 384b98fdd7b..167632cd568 100644
--- a/packages/localizations/src/nb-NO.ts
+++ b/packages/localizations/src/nb-NO.ts
@@ -2010,6 +2010,16 @@ export const nbNO: LocalizationResource = {
actionText: 'Innlogget som {{identifier}}',
},
},
+ taskForceEnterpriseSso: {
+ connectionUnavailable: undefined,
+ formButtonPrimary: undefined,
+ signOut: {
+ actionLink: undefined,
+ actionText: undefined,
+ },
+ subtitle: undefined,
+ title: undefined,
+ },
taskResetPassword: {
formButtonPrimary: 'Tilbakestill passord',
signOut: {
diff --git a/packages/localizations/src/nl-BE.ts b/packages/localizations/src/nl-BE.ts
index 1bf581ef397..11c0693ec41 100644
--- a/packages/localizations/src/nl-BE.ts
+++ b/packages/localizations/src/nl-BE.ts
@@ -1997,6 +1997,16 @@ export const nlBE: LocalizationResource = {
actionText: 'Ingelogd als {{identifier}}',
},
},
+ taskForceEnterpriseSso: {
+ connectionUnavailable: undefined,
+ formButtonPrimary: undefined,
+ signOut: {
+ actionLink: undefined,
+ actionText: undefined,
+ },
+ subtitle: undefined,
+ title: undefined,
+ },
taskResetPassword: {
formButtonPrimary: undefined,
signOut: {
diff --git a/packages/localizations/src/nl-NL.ts b/packages/localizations/src/nl-NL.ts
index 2349d69abe6..ab8f8419a3f 100644
--- a/packages/localizations/src/nl-NL.ts
+++ b/packages/localizations/src/nl-NL.ts
@@ -1997,6 +1997,16 @@ export const nlNL: LocalizationResource = {
actionText: 'Ingelogd als {{identifier}}',
},
},
+ taskForceEnterpriseSso: {
+ connectionUnavailable: undefined,
+ formButtonPrimary: undefined,
+ signOut: {
+ actionLink: undefined,
+ actionText: undefined,
+ },
+ subtitle: undefined,
+ title: undefined,
+ },
taskResetPassword: {
formButtonPrimary: undefined,
signOut: {
diff --git a/packages/localizations/src/pl-PL.ts b/packages/localizations/src/pl-PL.ts
index c6f75646e70..9b99ad87d88 100644
--- a/packages/localizations/src/pl-PL.ts
+++ b/packages/localizations/src/pl-PL.ts
@@ -2001,6 +2001,16 @@ export const plPL: LocalizationResource = {
actionText: 'Zalogowano jako {{identifier}}',
},
},
+ taskForceEnterpriseSso: {
+ connectionUnavailable: undefined,
+ formButtonPrimary: undefined,
+ signOut: {
+ actionLink: undefined,
+ actionText: undefined,
+ },
+ subtitle: undefined,
+ title: undefined,
+ },
taskResetPassword: {
formButtonPrimary: 'Zresetuj hasło',
signOut: {
diff --git a/packages/localizations/src/pt-BR.ts b/packages/localizations/src/pt-BR.ts
index 7138b2f1e6f..c5c74c5f1ea 100644
--- a/packages/localizations/src/pt-BR.ts
+++ b/packages/localizations/src/pt-BR.ts
@@ -2010,6 +2010,16 @@ export const ptBR: LocalizationResource = {
actionText: 'Conectado como {{identifier}}',
},
},
+ taskForceEnterpriseSso: {
+ connectionUnavailable: undefined,
+ formButtonPrimary: undefined,
+ signOut: {
+ actionLink: undefined,
+ actionText: undefined,
+ },
+ subtitle: undefined,
+ title: undefined,
+ },
taskResetPassword: {
formButtonPrimary: 'Resetar Senha',
signOut: {
diff --git a/packages/localizations/src/pt-PT.ts b/packages/localizations/src/pt-PT.ts
index f0659aa1e63..55dd8ed0915 100644
--- a/packages/localizations/src/pt-PT.ts
+++ b/packages/localizations/src/pt-PT.ts
@@ -2007,6 +2007,16 @@ export const ptPT: LocalizationResource = {
actionText: 'Sessão iniciada como {{identifier}}',
},
},
+ taskForceEnterpriseSso: {
+ connectionUnavailable: undefined,
+ formButtonPrimary: undefined,
+ signOut: {
+ actionLink: undefined,
+ actionText: undefined,
+ },
+ subtitle: undefined,
+ title: undefined,
+ },
taskResetPassword: {
formButtonPrimary: 'Repor palavra-passe',
signOut: {
diff --git a/packages/localizations/src/ro-RO.ts b/packages/localizations/src/ro-RO.ts
index e9000c9055a..fcf104d8d80 100644
--- a/packages/localizations/src/ro-RO.ts
+++ b/packages/localizations/src/ro-RO.ts
@@ -2011,6 +2011,16 @@ export const roRO: LocalizationResource = {
actionText: 'Autentificat ca {{identifier}}',
},
},
+ taskForceEnterpriseSso: {
+ connectionUnavailable: undefined,
+ formButtonPrimary: undefined,
+ signOut: {
+ actionLink: undefined,
+ actionText: undefined,
+ },
+ subtitle: undefined,
+ title: undefined,
+ },
taskResetPassword: {
formButtonPrimary: undefined,
signOut: {
diff --git a/packages/localizations/src/ru-RU.ts b/packages/localizations/src/ru-RU.ts
index 73ba47d5a48..dae29920696 100644
--- a/packages/localizations/src/ru-RU.ts
+++ b/packages/localizations/src/ru-RU.ts
@@ -2008,6 +2008,16 @@ export const ruRU: LocalizationResource = {
actionText: 'Вошли как {{identifier}}',
},
},
+ taskForceEnterpriseSso: {
+ connectionUnavailable: undefined,
+ formButtonPrimary: undefined,
+ signOut: {
+ actionLink: undefined,
+ actionText: undefined,
+ },
+ subtitle: undefined,
+ title: undefined,
+ },
taskResetPassword: {
formButtonPrimary: undefined,
signOut: {
diff --git a/packages/localizations/src/sk-SK.ts b/packages/localizations/src/sk-SK.ts
index 025e874ff30..2c36cd3452f 100644
--- a/packages/localizations/src/sk-SK.ts
+++ b/packages/localizations/src/sk-SK.ts
@@ -2001,6 +2001,16 @@ export const skSK: LocalizationResource = {
actionText: 'Prihlásený ako {{identifier}}',
},
},
+ taskForceEnterpriseSso: {
+ connectionUnavailable: undefined,
+ formButtonPrimary: undefined,
+ signOut: {
+ actionLink: undefined,
+ actionText: undefined,
+ },
+ subtitle: undefined,
+ title: undefined,
+ },
taskResetPassword: {
formButtonPrimary: undefined,
signOut: {
diff --git a/packages/localizations/src/sr-RS.ts b/packages/localizations/src/sr-RS.ts
index 14e8a094adf..8553445c930 100644
--- a/packages/localizations/src/sr-RS.ts
+++ b/packages/localizations/src/sr-RS.ts
@@ -1994,6 +1994,16 @@ export const srRS: LocalizationResource = {
actionText: 'Prijavljen kao {{identifier}}',
},
},
+ taskForceEnterpriseSso: {
+ connectionUnavailable: undefined,
+ formButtonPrimary: undefined,
+ signOut: {
+ actionLink: undefined,
+ actionText: undefined,
+ },
+ subtitle: undefined,
+ title: undefined,
+ },
taskResetPassword: {
formButtonPrimary: undefined,
signOut: {
diff --git a/packages/localizations/src/sv-SE.ts b/packages/localizations/src/sv-SE.ts
index f411009e4f3..209fa118f1a 100644
--- a/packages/localizations/src/sv-SE.ts
+++ b/packages/localizations/src/sv-SE.ts
@@ -1999,6 +1999,16 @@ export const svSE: LocalizationResource = {
actionText: 'Inloggad som {{identifier}}',
},
},
+ taskForceEnterpriseSso: {
+ connectionUnavailable: undefined,
+ formButtonPrimary: undefined,
+ signOut: {
+ actionLink: undefined,
+ actionText: undefined,
+ },
+ subtitle: undefined,
+ title: undefined,
+ },
taskResetPassword: {
formButtonPrimary: undefined,
signOut: {
diff --git a/packages/localizations/src/ta-IN.ts b/packages/localizations/src/ta-IN.ts
index 076f86f9873..7adce48d6fd 100644
--- a/packages/localizations/src/ta-IN.ts
+++ b/packages/localizations/src/ta-IN.ts
@@ -2013,6 +2013,16 @@ export const taIN: LocalizationResource = {
actionText: '{{identifier}} என உள்நுழைந்துள்ளீர்கள்',
},
},
+ taskForceEnterpriseSso: {
+ connectionUnavailable: undefined,
+ formButtonPrimary: undefined,
+ signOut: {
+ actionLink: undefined,
+ actionText: undefined,
+ },
+ subtitle: undefined,
+ title: undefined,
+ },
taskResetPassword: {
formButtonPrimary: 'கடவுச்சொல்லை மீட்டமை',
signOut: {
diff --git a/packages/localizations/src/te-IN.ts b/packages/localizations/src/te-IN.ts
index 8a4c8d6f7a2..70febea28ab 100644
--- a/packages/localizations/src/te-IN.ts
+++ b/packages/localizations/src/te-IN.ts
@@ -2011,6 +2011,16 @@ export const teIN: LocalizationResource = {
actionText: '{{identifier}}గా సైన్ ఇన్ చేయబడింది',
},
},
+ taskForceEnterpriseSso: {
+ connectionUnavailable: undefined,
+ formButtonPrimary: undefined,
+ signOut: {
+ actionLink: undefined,
+ actionText: undefined,
+ },
+ subtitle: undefined,
+ title: undefined,
+ },
taskResetPassword: {
formButtonPrimary: 'పాస్వర్డ్ను రీసెట్ చేయి',
signOut: {
diff --git a/packages/localizations/src/th-TH.ts b/packages/localizations/src/th-TH.ts
index eebb4436919..da0be3a3d2a 100644
--- a/packages/localizations/src/th-TH.ts
+++ b/packages/localizations/src/th-TH.ts
@@ -1996,6 +1996,16 @@ export const thTH: LocalizationResource = {
actionText: 'เข้าสู่ระบบในนาม {{identifier}}',
},
},
+ taskForceEnterpriseSso: {
+ connectionUnavailable: undefined,
+ formButtonPrimary: undefined,
+ signOut: {
+ actionLink: undefined,
+ actionText: undefined,
+ },
+ subtitle: undefined,
+ title: undefined,
+ },
taskResetPassword: {
formButtonPrimary: undefined,
signOut: {
diff --git a/packages/localizations/src/tr-TR.ts b/packages/localizations/src/tr-TR.ts
index e840c7c2b2c..79b93ca9be7 100644
--- a/packages/localizations/src/tr-TR.ts
+++ b/packages/localizations/src/tr-TR.ts
@@ -1997,6 +1997,16 @@ export const trTR: LocalizationResource = {
actionText: '{{identifier}} olarak giriş yapıldı',
},
},
+ taskForceEnterpriseSso: {
+ connectionUnavailable: undefined,
+ formButtonPrimary: undefined,
+ signOut: {
+ actionLink: undefined,
+ actionText: undefined,
+ },
+ subtitle: undefined,
+ title: undefined,
+ },
taskResetPassword: {
formButtonPrimary: undefined,
signOut: {
diff --git a/packages/localizations/src/uk-UA.ts b/packages/localizations/src/uk-UA.ts
index bd77cc5dcd0..3acd9f7aa71 100644
--- a/packages/localizations/src/uk-UA.ts
+++ b/packages/localizations/src/uk-UA.ts
@@ -1993,6 +1993,16 @@ export const ukUA: LocalizationResource = {
actionText: 'Увійшли як {{identifier}}',
},
},
+ taskForceEnterpriseSso: {
+ connectionUnavailable: undefined,
+ formButtonPrimary: undefined,
+ signOut: {
+ actionLink: undefined,
+ actionText: undefined,
+ },
+ subtitle: undefined,
+ title: undefined,
+ },
taskResetPassword: {
formButtonPrimary: undefined,
signOut: {
diff --git a/packages/localizations/src/vi-VN.ts b/packages/localizations/src/vi-VN.ts
index 35e6662dc65..dda6c730f86 100644
--- a/packages/localizations/src/vi-VN.ts
+++ b/packages/localizations/src/vi-VN.ts
@@ -2006,6 +2006,16 @@ export const viVN: LocalizationResource = {
actionText: 'Đã đăng nhập với {{identifier}}',
},
},
+ taskForceEnterpriseSso: {
+ connectionUnavailable: undefined,
+ formButtonPrimary: undefined,
+ signOut: {
+ actionLink: undefined,
+ actionText: undefined,
+ },
+ subtitle: undefined,
+ title: undefined,
+ },
taskResetPassword: {
formButtonPrimary: 'Đặt lại mật khẩu',
signOut: {
diff --git a/packages/localizations/src/zh-CN.ts b/packages/localizations/src/zh-CN.ts
index 7ea16a28b01..633242c716e 100644
--- a/packages/localizations/src/zh-CN.ts
+++ b/packages/localizations/src/zh-CN.ts
@@ -1982,6 +1982,16 @@ export const zhCN: LocalizationResource = {
actionText: '已登录为 {{identifier}}',
},
},
+ taskForceEnterpriseSso: {
+ connectionUnavailable: undefined,
+ formButtonPrimary: undefined,
+ signOut: {
+ actionLink: undefined,
+ actionText: undefined,
+ },
+ subtitle: undefined,
+ title: undefined,
+ },
taskResetPassword: {
formButtonPrimary: undefined,
signOut: {
diff --git a/packages/localizations/src/zh-TW.ts b/packages/localizations/src/zh-TW.ts
index ee115cb297f..c964b76f797 100644
--- a/packages/localizations/src/zh-TW.ts
+++ b/packages/localizations/src/zh-TW.ts
@@ -1986,6 +1986,16 @@ export const zhTW: LocalizationResource = {
actionText: '已登入為 {{identifier}}',
},
},
+ taskForceEnterpriseSso: {
+ connectionUnavailable: undefined,
+ formButtonPrimary: undefined,
+ signOut: {
+ actionLink: undefined,
+ actionText: undefined,
+ },
+ subtitle: undefined,
+ title: undefined,
+ },
taskResetPassword: {
formButtonPrimary: '重設密碼',
signOut: {
diff --git a/packages/shared/src/internal/clerk-js/sessionTasks.ts b/packages/shared/src/internal/clerk-js/sessionTasks.ts
index e0d0fd1e0f8..e450efaae46 100644
--- a/packages/shared/src/internal/clerk-js/sessionTasks.ts
+++ b/packages/shared/src/internal/clerk-js/sessionTasks.ts
@@ -10,6 +10,7 @@ export const INTERNAL_SESSION_TASK_ROUTE_BY_KEY: Record;
};
};
+ taskForceEnterpriseSso: {
+ title: LocalizationValue;
+ subtitle: LocalizationValue<'organizationName'>;
+ formButtonPrimary: LocalizationValue<'connectionName'>;
+ connectionUnavailable: LocalizationValue;
+ signOut: {
+ actionLink: LocalizationValue;
+ actionText: LocalizationValue<'identifier'>;
+ };
+ };
taskResetPassword: {
title: LocalizationValue;
subtitle: LocalizationValue;
diff --git a/packages/shared/src/types/session.ts b/packages/shared/src/types/session.ts
index a492a586be0..d1bec813286 100644
--- a/packages/shared/src/types/session.ts
+++ b/packages/shared/src/types/session.ts
@@ -496,7 +496,7 @@ export interface SessionTask {
/**
* A unique identifier for the task
*/
- key: 'choose-organization' | 'reset-password' | 'setup-mfa';
+ key: 'choose-organization' | 'reset-password' | 'setup-mfa' | 'force-enterprise-sso';
}
/** @generateWithEmptyComment */
diff --git a/packages/ui/src/components/SessionTasks/__tests__/navigateOnSetActive.test.tsx b/packages/ui/src/components/SessionTasks/__tests__/navigateOnSetActive.test.tsx
new file mode 100644
index 00000000000..d7a0851c2f6
--- /dev/null
+++ b/packages/ui/src/components/SessionTasks/__tests__/navigateOnSetActive.test.tsx
@@ -0,0 +1,45 @@
+import type { SessionResource } from '@clerk/shared/types';
+import { useEffect } from 'react';
+import { describe, expect, it, vi } from 'vitest';
+
+import { bindCreateFixtures } from '@/test/create-fixtures';
+import { render, waitFor } from '@/test/utils';
+import { SessionTasksContext, useSessionTasksContext } from '@/ui/contexts/components/SessionTasks';
+
+const { createFixtures } = bindCreateFixtures('SignIn');
+
+const NavigateToNextTask = ({ redirectUrlComplete }: { redirectUrlComplete: string }) => {
+ const { navigateOnSetActive } = useSessionTasksContext();
+ useEffect(() => {
+ void navigateOnSetActive({
+ session: { currentTask: { key: 'force-enterprise-sso' } } as SessionResource,
+ redirectUrlComplete,
+ decorateUrl: url => url,
+ });
+ }, [navigateOnSetActive, redirectUrlComplete]);
+ return null;
+};
+
+describe('navigateOnSetActive', () => {
+ it("sends the next task to the flow's destination, not back to the task that just finished", async () => {
+ const { wrapper, fixtures } = await createFixtures(f =>
+ f.withUser({ email_addresses: ['bart@gmail.com'], tasks: [{ key: 'force-enterprise-sso' }] }),
+ );
+ vi.spyOn(fixtures.clerk, 'buildSignInUrl').mockImplementation(
+ (options?: { redirectUrl?: string }) =>
+ `http://localhost/sign-in?redirect_url=${encodeURIComponent(options?.redirectUrl ?? 'http://localhost/sign-in/tasks/choose-organization')}`,
+ );
+
+ render(
+
+
+ ,
+ { wrapper },
+ );
+
+ await waitFor(() => expect(fixtures.router.navigate).toHaveBeenCalled());
+ const nextTaskUrl = new URL(fixtures.router.navigate.mock.calls[0][0]);
+ expect(nextTaskUrl.pathname + nextTaskUrl.hash).toBe('/sign-in#/tasks/force-enterprise-sso');
+ expect(nextTaskUrl.searchParams.get('redirect_url')).toBe('http://localhost/dashboard');
+ });
+});
diff --git a/packages/ui/src/components/SessionTasks/index.tsx b/packages/ui/src/components/SessionTasks/index.tsx
index 0672ff7ff6a..7d8447b13a8 100644
--- a/packages/ui/src/components/SessionTasks/index.tsx
+++ b/packages/ui/src/components/SessionTasks/index.tsx
@@ -18,6 +18,7 @@ import {
import { Route, Switch, useRouter } from '../../router';
import { TaskChooseOrganization } from './tasks/TaskChooseOrganization';
import { TaskResetPassword } from './tasks/TaskResetPassword';
+import { TaskForceEnterpriseSSO } from './tasks/TaskForceEnterpriseSso';
import { TaskSetupMFA } from './tasks/TaskSetupMfa';
const SessionTasksStart = () => {
@@ -108,6 +109,9 @@ function SessionTasksRoutes(): JSX.Element {
+
+
+
diff --git a/packages/ui/src/components/SessionTasks/tasks/TaskForceEnterpriseSso/__tests__/TaskForceEnterpriseSso.test.tsx b/packages/ui/src/components/SessionTasks/tasks/TaskForceEnterpriseSso/__tests__/TaskForceEnterpriseSso.test.tsx
new file mode 100644
index 00000000000..7927b02b3c0
--- /dev/null
+++ b/packages/ui/src/components/SessionTasks/tasks/TaskForceEnterpriseSso/__tests__/TaskForceEnterpriseSso.test.tsx
@@ -0,0 +1,74 @@
+import type { EnterpriseConnectionResource, ExternalAccountResource } from '@clerk/shared/types';
+import { describe, expect, it } from 'vitest';
+
+import { bindCreateFixtures } from '@/test/create-fixtures';
+import { render, waitFor } from '@/test/utils';
+import { SessionTasksContext } from '@/ui/contexts/components/SessionTasks';
+
+import { TaskForceEnterpriseSSO } from '..';
+
+const { createFixtures } = bindCreateFixtures('SignIn');
+
+const ssoConnection = {
+ id: 'entc_uop',
+ name: 'University of Phoenix',
+ active: true,
+ allowOrganizationAccountLinking: true,
+ samlConnection: { id: 'samlc_uop' },
+} as unknown as EnterpriseConnectionResource;
+
+const renderTask = (wrapper: Parameters[1]['wrapper']) =>
+ render(
+
+
+ ,
+ { wrapper },
+ );
+
+const studentWithTask = {
+ email_addresses: ['bart@gmail.com'],
+ organization_memberships: [{ name: 'University of Phoenix', id: 'org_uop', role: 'basic_member' }],
+};
+
+describe('TaskForceEnterpriseSSO', () => {
+ it('does not render without the session task', async () => {
+ const { wrapper } = await createFixtures(f => f.withUser(studentWithTask));
+
+ const { queryByRole } = renderTask(wrapper);
+
+ expect(queryByRole('button', { name: /continue with/i })).not.toBeInTheDocument();
+ });
+
+ it("starts SSO through the active organization's connection", async () => {
+ const { wrapper, fixtures } = await createFixtures(f =>
+ f.withUser({ ...studentWithTask, tasks: [{ key: 'force-enterprise-sso' }] }),
+ );
+ const organization = fixtures.clerk.user?.organizationMemberships[0].organization;
+ organization?.getEnterpriseConnections.mockResolvedValue([ssoConnection]);
+ fixtures.clerk.user?.createExternalAccount.mockResolvedValue({} as ExternalAccountResource);
+
+ const { findByRole, userEvent } = renderTask(wrapper);
+ await userEvent.click(await findByRole('button', { name: 'Continue with University of Phoenix' }));
+
+ await waitFor(() =>
+ expect(fixtures.clerk.user?.createExternalAccount).toHaveBeenCalledWith({
+ enterpriseConnectionId: 'entc_uop',
+ redirectUrl: window.location.href,
+ }),
+ );
+ });
+
+ it('explains when the organization has no SSO connection to continue with', async () => {
+ const { wrapper, fixtures } = await createFixtures(f =>
+ f.withUser({ ...studentWithTask, tasks: [{ key: 'force-enterprise-sso' }] }),
+ );
+ fixtures.clerk.user?.organizationMemberships[0].organization.getEnterpriseConnections.mockResolvedValue([
+ { ...ssoConnection, allowOrganizationAccountLinking: false } as EnterpriseConnectionResource,
+ ]);
+
+ const { findByText, queryByRole } = renderTask(wrapper);
+
+ expect(await findByText(/SSO connection isn't available/i)).toBeInTheDocument();
+ expect(queryByRole('button', { name: /continue with/i })).not.toBeInTheDocument();
+ });
+});
diff --git a/packages/ui/src/components/SessionTasks/tasks/TaskForceEnterpriseSso/index.tsx b/packages/ui/src/components/SessionTasks/tasks/TaskForceEnterpriseSso/index.tsx
new file mode 100644
index 00000000000..e871240432f
--- /dev/null
+++ b/packages/ui/src/components/SessionTasks/tasks/TaskForceEnterpriseSso/index.tsx
@@ -0,0 +1,155 @@
+import { useClerk, useReverification, useUser } from '@clerk/shared/react';
+import type { EnterpriseConnectionResource } from '@clerk/shared/types';
+import { useEffect, useState } from 'react';
+
+import { useSignOutContext, withCoreSessionSwitchGuard } from '@/ui/contexts';
+import { Button, Col, descriptors, Flow, localizationKeys, Text } from '@/ui/customizables';
+import { Card } from '@/ui/elements/Card';
+import { useCardState, withCardStateProvider } from '@/ui/elements/contexts';
+import { Header } from '@/ui/elements/Header';
+import { LoadingCardContainer } from '@/ui/elements/LoadingCard';
+import { useMultipleSessions } from '@/ui/hooks/useMultipleSessions';
+import { handleError } from '@/ui/utils/errorHandler';
+import { clerkWindowNavigate } from '@/ui/utils/windowNavigate';
+
+import { withTaskGuard } from '../shared';
+
+const isSSORequiredConnection = (connection: EnterpriseConnectionResource) =>
+ connection.active && connection.allowOrganizationAccountLinking && !!connection.samlConnection;
+
+const TaskForceEnterpriseSSOInternal = () => {
+ const clerk = useClerk();
+ const { user } = useUser();
+ const card = useCardState();
+ const { otherSessions } = useMultipleSessions({ user: clerk.user });
+ const { navigateAfterSignOut, navigateAfterMultiSessionSingleSignOutUrl } = useSignOutContext();
+ const [connection, setConnection] = useState();
+
+ const organization = user?.organizationMemberships.find(
+ membership => membership.organization.id === clerk.session?.lastActiveOrganizationId,
+ )?.organization;
+
+ useEffect(() => {
+ if (!organization) {
+ setConnection(null);
+ return;
+ }
+
+ organization
+ .getEnterpriseConnections()
+ .then(connections => setConnection(connections.find(isSSORequiredConnection) ?? null))
+ .catch(err => {
+ setConnection(null);
+ handleError(err, [], card.setError);
+ });
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [organization?.id]);
+
+ const createExternalAccount = useReverification((enterpriseConnectionId: string) =>
+ user?.createExternalAccount({ enterpriseConnectionId, redirectUrl: window.location.href }),
+ );
+
+ const continueWithSSO = (enterpriseConnectionId: string) =>
+ card
+ .runAsync(async () => {
+ const externalAccount = await createExternalAccount(enterpriseConnectionId);
+ const redirectUrl = externalAccount?.verification?.externalVerificationRedirectURL;
+ if (redirectUrl) {
+ clerkWindowNavigate(clerk, redirectUrl);
+ }
+ })
+ .catch(err => handleError(err, [], card.setError));
+
+ const handleSignOut = () => {
+ if (otherSessions.length === 0) {
+ return clerk.signOut(navigateAfterSignOut);
+ }
+
+ return clerk.signOut(navigateAfterMultiSessionSingleSignOutUrl, { sessionId: clerk.session?.id });
+ };
+
+ const identifier = clerk.user?.primaryEmailAddress?.emailAddress ?? clerk.user?.username;
+
+ if (connection === undefined) {
+ return (
+
+
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+
+ {card.error}
+
+ {connection ? (
+
+
+
+ ({ width: '100%' })}
+ >
+ {identifier && (
+
+ )}
+ ({ flexShrink: 0 })}
+ onClick={() => {
+ void handleSignOut();
+ }}
+ localizationKey={localizationKeys('taskForceEnterpriseSso.signOut.actionLink')}
+ />
+
+
+
+
+
+ );
+};
+
+export const TaskForceEnterpriseSSO = withCoreSessionSwitchGuard(
+ withTaskGuard(withCardStateProvider(TaskForceEnterpriseSSOInternal), 'force-enterprise-sso'),
+);
diff --git a/packages/ui/src/contexts/components/SessionTasks.ts b/packages/ui/src/contexts/components/SessionTasks.ts
index a0ed5e7eddb..f3a7bb390b2 100644
--- a/packages/ui/src/contexts/components/SessionTasks.ts
+++ b/packages/ui/src/contexts/components/SessionTasks.ts
@@ -64,7 +64,7 @@ export const useSessionTasksContext = (): SessionTasksContextType => {
// If there's no custom taskUrl, and we are not using virtual routing
// we can just navigate to the task endpoint directly.
- const signInUrl = clerk.buildSignInUrl();
+ const signInUrl = clerk.buildSignInUrl({ redirectUrl: redirectUrlComplete });
return navigate(buildTaskUrl(currentTask, { base: signInUrl }));
};
diff --git a/packages/ui/src/elements/contexts/index.tsx b/packages/ui/src/elements/contexts/index.tsx
index a8345aca4a4..45caa55cae4 100644
--- a/packages/ui/src/elements/contexts/index.tsx
+++ b/packages/ui/src/elements/contexts/index.tsx
@@ -114,7 +114,8 @@ export type FlowMetadata = {
| 'taskChooseOrganization'
| 'enableOrganizations'
| 'taskResetPassword'
- | 'taskSetupMfa';
+ | 'taskSetupMfa'
+ | 'taskForceEnterpriseSso';
part?:
| 'start'
| 'emailCode'