Skip to content

Commit aaea141

Browse files
feat(ui): remove experimental gate from self-serve OIDC flow (#9288)
Co-authored-by: Robert Soriano <[email protected]>
1 parent 49d61ff commit aaea141

7 files changed

Lines changed: 30 additions & 78 deletions

File tree

‎.changeset/quiet-moons-shine.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@clerk/clerk-js': patch
3+
'@clerk/shared': patch
4+
'@clerk/ui': patch
5+
---
6+
7+
Enable self-serve OIDC configuration for every application. Organization admins can now select an OIDC provider in the `<OrganizationProfile />` Security tab without the `experimental.oidcSelfServe` option, and existing OIDC connections open their configuration steps instead of the unsupported-provider state. The `experimental.oidcSelfServe` option no longer does anything and can be removed from `<ClerkProvider />` and `Clerk.load()`.

‎packages/shared/src/types/clerk.ts‎

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1513,10 +1513,6 @@ export type ClerkOptions = ClerkOptionsNavigation &
15131513
* directly with the provided Clerk instance. Used by React Native / Expo.
15141514
*/
15151515
runtimeEnvironment: 'headless';
1516-
/**
1517-
* Temporary flag that gates the self-serve OIDC flow in `<ConfigureSSO />`. Remove once the self-serve OIDC flow reaches GA.
1518-
*/
1519-
oidcSelfServe: boolean;
15201516
},
15211517
Record<string, any>
15221518
>;

‎packages/ui/src/components/ConfigureSSO/ConfigureSSOContext.tsx‎

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import type { EnterpriseConnectionResource, OrganizationDomainResource } from '@clerk/shared/types';
22
import React, { type PropsWithChildren } from 'react';
33

4-
import { useOptions } from '@/contexts';
5-
64
import type { OrganizationEnterpriseConnection } from './domain/organizationEnterpriseConnection';
75
import type {
86
EnterpriseConnectionMutations,
@@ -28,8 +26,6 @@ export interface ConfigureSSOData {
2826
testRuns: TestRunsView;
2927
organizationDomains: OrganizationDomainResource[] | undefined;
3028
onExit?: () => void;
31-
/** Temporary gate for the self-serve OIDC flow; remove at OIDC GA. */
32-
isOIDCFlowEnabled: boolean;
3329
}
3430

3531
interface ConfigureSSOProviderProps {
@@ -57,8 +53,6 @@ export const ConfigureSSOProvider = ({
5753
onExit,
5854
children,
5955
}: PropsWithChildren<ConfigureSSOProviderProps>): JSX.Element => {
60-
const isOIDCFlowEnabled = useOptions().experimental?.oidcSelfServe ?? false;
61-
6256
const value = React.useMemo<ConfigureSSOData>(
6357
() => ({
6458
contentRef,
@@ -69,7 +63,6 @@ export const ConfigureSSOProvider = ({
6963
enterpriseConnectionMutations,
7064
organizationDomainMutations,
7165
onExit,
72-
isOIDCFlowEnabled,
7366
}),
7467
[
7568
contentRef,
@@ -80,7 +73,6 @@ export const ConfigureSSOProvider = ({
8073
organizationDomains,
8174
enterpriseConnection,
8275
onExit,
83-
isOIDCFlowEnabled,
8476
],
8577
);
8678

‎packages/ui/src/components/ConfigureSSO/steps/ConfigureStep/__tests__/ConfigureStep.test.tsx‎

Lines changed: 10 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import type { EnterpriseConnectionProviderType } from '../../../types';
1414
// left undefined so that footer self-hides in this isolated render.
1515
const contextState = vi.hoisted(() => ({
1616
provider: undefined as string | undefined,
17-
isOIDCFlowEnabled: true,
1817
enterpriseConnection: undefined as
1918
| {
2019
id: string;
@@ -45,7 +44,6 @@ vi.mock('../../../ConfigureSSOContext', () => ({
4544
provider: contextState.provider,
4645
hasConnection: true,
4746
},
48-
isOIDCFlowEnabled: contextState.isOIDCFlowEnabled,
4947
}),
5048
}));
5149

@@ -63,34 +61,28 @@ const { createFixtures } = bindCreateFixtures('ConfigureSSO');
6361

6462
describe('resolveConfigureSteps', () => {
6563
it('dispatches custom and legacy OIDC provider keys to the OIDC sub-flow', () => {
66-
expect(resolveConfigureSteps('oauth_custom_clerk_dev', true)).toBe(OidcCustomConfigureSteps);
67-
expect(resolveConfigureSteps('oidc_clerk_dev', true)).toBe(OidcCustomConfigureSteps);
68-
expect(resolveConfigureSteps('oidc_ghe_acme', true)).toBe(OidcCustomConfigureSteps);
69-
expect(resolveConfigureSteps('oidc_gitlab_ent_acme', true)).toBe(OidcCustomConfigureSteps);
70-
expect(resolveConfigureSteps('oidc_custom', true)).toBe(OidcCustomConfigureSteps);
71-
});
72-
73-
it('does not dispatch OIDC providers while the experimental flow is disabled', () => {
74-
expect(resolveConfigureSteps('oauth_custom_clerk_dev', false)).toBeUndefined();
75-
expect(resolveConfigureSteps('oidc_clerk_dev', false)).toBeUndefined();
64+
expect(resolveConfigureSteps('oauth_custom_clerk_dev')).toBe(OidcCustomConfigureSteps);
65+
expect(resolveConfigureSteps('oidc_clerk_dev')).toBe(OidcCustomConfigureSteps);
66+
expect(resolveConfigureSteps('oidc_ghe_acme')).toBe(OidcCustomConfigureSteps);
67+
expect(resolveConfigureSteps('oidc_gitlab_ent_acme')).toBe(OidcCustomConfigureSteps);
68+
expect(resolveConfigureSteps('oidc_custom')).toBe(OidcCustomConfigureSteps);
7669
});
7770

7871
it('dispatches SAML providers by exact literal', () => {
79-
expect(resolveConfigureSteps('saml_okta', false)).toBe(SamlOktaConfigureSteps);
80-
expect(resolveConfigureSteps('saml_custom', false)).toBe(SamlCustomConfigureSteps);
81-
expect(resolveConfigureSteps('saml_google', false)).toBe(SamlGoogleConfigureSteps);
82-
expect(resolveConfigureSteps('saml_microsoft', false)).toBe(SamlMicrosoftConfigureSteps);
72+
expect(resolveConfigureSteps('saml_okta')).toBe(SamlOktaConfigureSteps);
73+
expect(resolveConfigureSteps('saml_custom')).toBe(SamlCustomConfigureSteps);
74+
expect(resolveConfigureSteps('saml_google')).toBe(SamlGoogleConfigureSteps);
75+
expect(resolveConfigureSteps('saml_microsoft')).toBe(SamlMicrosoftConfigureSteps);
8376
});
8477

8578
it('returns undefined for an unrecognized provider so the caller can degrade', () => {
86-
expect(resolveConfigureSteps('ldap_enterprise' as EnterpriseConnectionProviderType, true)).toBeUndefined();
79+
expect(resolveConfigureSteps('ldap_enterprise' as EnterpriseConnectionProviderType)).toBeUndefined();
8780
});
8881
});
8982

9083
describe('ConfigureProviderStep', () => {
9184
beforeEach(() => {
9285
contextState.provider = undefined;
93-
contextState.isOIDCFlowEnabled = true;
9486
contextState.enterpriseConnection = undefined;
9587
updateConnection.mockReset();
9688
});
@@ -469,15 +461,4 @@ describe('ConfigureProviderStep', () => {
469461

470462
expect(await screen.findByText(/unsupported provider/i)).toBeInTheDocument();
471463
});
472-
473-
it('degrades to the unsupported-provider state for an existing OIDC connection when the flag is off', async () => {
474-
contextState.provider = 'oauth_custom_clerk_dev';
475-
contextState.isOIDCFlowEnabled = false;
476-
const { wrapper } = await createFixtures();
477-
478-
renderStep(wrapper);
479-
480-
expect(await screen.findByText(/unsupported provider/i)).toBeInTheDocument();
481-
expect(screen.queryByText(/create a new oidc application/i)).not.toBeInTheDocument();
482-
});
483464
});

‎packages/ui/src/components/ConfigureSSO/steps/ConfigureStep/index.tsx‎

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,8 @@ const STEPS_BY_SAML_PROVIDER: Record<SamlProviderType, ConfigureStepsComponent>
2828

2929
export const resolveConfigureSteps = (
3030
provider: EnterpriseConnectionProviderType,
31-
isOIDCFlowEnabled: boolean,
32-
): ConfigureStepsComponent | undefined => {
33-
if (isOidcProvider(provider)) {
34-
return isOIDCFlowEnabled ? OidcCustomConfigureSteps : undefined;
35-
}
36-
return STEPS_BY_SAML_PROVIDER[provider];
37-
};
31+
): ConfigureStepsComponent | undefined =>
32+
isOidcProvider(provider) ? OidcCustomConfigureSteps : STEPS_BY_SAML_PROVIDER[provider];
3833

3934
export const ConfigureStep = (): JSX.Element => {
4035
const { organizationEnterpriseConnection: c } = useConfigureSSO();
@@ -68,13 +63,13 @@ export const ConfigureStep = (): JSX.Element => {
6863
};
6964

7065
export const ConfigureProviderStep = (): JSX.Element | null => {
71-
const { organizationEnterpriseConnection: c, isOIDCFlowEnabled } = useConfigureSSO();
66+
const { organizationEnterpriseConnection: c } = useConfigureSSO();
7267

7368
if (!c.provider) {
7469
return null;
7570
}
7671

77-
const ConfigureSteps = resolveConfigureSteps(c.provider, isOIDCFlowEnabled);
72+
const ConfigureSteps = resolveConfigureSteps(c.provider);
7873

7974
return (
8075
<Flow.Part part='configureCreateApp'>

‎packages/ui/src/components/ConfigureSSO/steps/SelectProviderStep.tsx‎

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -78,16 +78,10 @@ export const SelectProviderStep = (): JSX.Element => {
7878
organizationEnterpriseConnection: c,
7979
enterpriseConnectionMutations: { createConnection, changeProvider },
8080
contentRef,
81-
isOIDCFlowEnabled,
8281
} = useConfigureSSO();
8382
const { goNext, goPrev, isFirstStep } = useWizard();
8483
const { t } = useLocalizations();
8584

86-
const providerGroups = React.useMemo(
87-
() => PROVIDER_GROUPS.filter(group => group.id !== 'oidc' || isOIDCFlowEnabled),
88-
[isOIDCFlowEnabled],
89-
);
90-
9185
const currentCard = c.provider ? toProviderCard(c.provider) : null;
9286

9387
const [selected, setSelected] = React.useState<ProviderType | null>(currentCard);
@@ -166,7 +160,7 @@ export const SelectProviderStep = (): JSX.Element => {
166160

167161
<Step.Body>
168162
<Step.Section sx={theme => ({ gap: theme.space.$5 })}>
169-
{providerGroups.map(group => (
163+
{PROVIDER_GROUPS.map(group => (
170164
<Col
171165
key={group.id}
172166
elementDescriptor={descriptors.configureSSOProviderGroup}

‎packages/ui/src/components/ConfigureSSO/steps/__tests__/SelectProviderStep.test.tsx‎

Lines changed: 8 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ const changeProvider = vi.fn();
2525
const contextState = vi.hoisted(() => ({
2626
provider: undefined as 'saml_okta' | 'saml_custom' | 'saml_google' | undefined,
2727
hasConnection: false,
28-
isOIDCFlowEnabled: false,
2928
}));
3029

3130
vi.mock('../../ConfigureSSOContext', () => ({
@@ -40,7 +39,6 @@ vi.mock('../../ConfigureSSOContext', () => ({
4039
provider: contextState.provider,
4140
hasConnection: contextState.hasConnection,
4241
},
43-
isOIDCFlowEnabled: contextState.isOIDCFlowEnabled,
4442
}),
4543
}));
4644

@@ -64,7 +62,6 @@ const resetMocks = () => {
6462
changeProvider.mockResolvedValue(undefined);
6563
contextState.provider = undefined;
6664
contextState.hasConnection = false;
67-
contextState.isOIDCFlowEnabled = false;
6865
};
6966

7067
describe('SelectProviderStep', () => {
@@ -96,7 +93,7 @@ describe('SelectProviderStep', () => {
9693
// Each provider card is a <label> wrapping a visually-hidden native radio; the
9794
// aria-hidden icon span lives inside it.
9895
const iconSpans = Array.from(container.querySelectorAll('label span[aria-hidden]'));
99-
expect(iconSpans).toHaveLength(4);
96+
expect(iconSpans).toHaveLength(5);
10097

10198
const collectedStyles = [
10299
...Array.from(document.head.querySelectorAll('style')).map(s => s.textContent ?? ''),
@@ -106,26 +103,16 @@ describe('SelectProviderStep', () => {
106103
expect(collectedStyles).toMatch(/img\.clerk\.com\/static\/okta\.svg/);
107104
expect(collectedStyles).toMatch(/img\.clerk\.com\/static\/saml\.svg/);
108105
expect(collectedStyles).toMatch(/img\.clerk\.com\/static\/google\.svg/);
106+
expect(collectedStyles).toMatch(/img\.clerk\.com\/static\/oidc\.svg/);
109107
});
110108

111-
describe('OIDC provider (experimental flag)', () => {
112-
it('hides the OIDC provider tile when the flag is off', async () => {
113-
resetMocks();
114-
const { wrapper } = await createFixtures();
115-
const { container } = renderStep(wrapper);
116-
117-
expect(screen.getByRole('radio', { name: 'Okta Workforce' })).toBeInTheDocument();
118-
expect(container.querySelector('input[value="oidc_custom"]')).not.toBeInTheDocument();
119-
});
120-
121-
it('shows the OIDC provider tile when the flag is on', async () => {
122-
resetMocks();
123-
contextState.isOIDCFlowEnabled = true;
124-
const { wrapper } = await createFixtures();
125-
const { container } = renderStep(wrapper);
109+
it('renders the OIDC provider tile', async () => {
110+
resetMocks();
111+
const { wrapper } = await createFixtures();
112+
const { container } = renderStep(wrapper);
126113

127-
expect(container.querySelector('input[value="oidc_custom"]')).toBeInTheDocument();
128-
});
114+
expect(screen.getByRole('radio', { name: 'Okta Workforce' })).toBeInTheDocument();
115+
expect(container.querySelector('input[value="oidc_custom"]')).toBeInTheDocument();
129116
});
130117

131118
it('disables Continue when no provider is selected', async () => {

0 commit comments

Comments
 (0)