diff --git a/.changeset/quiet-moons-shine.md b/.changeset/quiet-moons-shine.md new file mode 100644 index 00000000000..b229db264f0 --- /dev/null +++ b/.changeset/quiet-moons-shine.md @@ -0,0 +1,7 @@ +--- +'@clerk/clerk-js': patch +'@clerk/shared': patch +'@clerk/ui': patch +--- + +Enable self-serve OIDC configuration for every application. Organization admins can now select an OIDC provider in the `` 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 `` and `Clerk.load()`. diff --git a/packages/shared/src/types/clerk.ts b/packages/shared/src/types/clerk.ts index 77ce5435aae..cb3999bcd70 100644 --- a/packages/shared/src/types/clerk.ts +++ b/packages/shared/src/types/clerk.ts @@ -1513,10 +1513,6 @@ export type ClerkOptions = ClerkOptionsNavigation & * directly with the provided Clerk instance. Used by React Native / Expo. */ runtimeEnvironment: 'headless'; - /** - * Temporary flag that gates the self-serve OIDC flow in ``. Remove once the self-serve OIDC flow reaches GA. - */ - oidcSelfServe: boolean; }, Record >; diff --git a/packages/ui/src/components/ConfigureSSO/ConfigureSSOContext.tsx b/packages/ui/src/components/ConfigureSSO/ConfigureSSOContext.tsx index 156fc15eeed..cb6f963ea62 100644 --- a/packages/ui/src/components/ConfigureSSO/ConfigureSSOContext.tsx +++ b/packages/ui/src/components/ConfigureSSO/ConfigureSSOContext.tsx @@ -1,8 +1,6 @@ import type { EnterpriseConnectionResource, OrganizationDomainResource } from '@clerk/shared/types'; import React, { type PropsWithChildren } from 'react'; -import { useOptions } from '@/contexts'; - import type { OrganizationEnterpriseConnection } from './domain/organizationEnterpriseConnection'; import type { EnterpriseConnectionMutations, @@ -28,8 +26,6 @@ export interface ConfigureSSOData { testRuns: TestRunsView; organizationDomains: OrganizationDomainResource[] | undefined; onExit?: () => void; - /** Temporary gate for the self-serve OIDC flow; remove at OIDC GA. */ - isOIDCFlowEnabled: boolean; } interface ConfigureSSOProviderProps { @@ -57,8 +53,6 @@ export const ConfigureSSOProvider = ({ onExit, children, }: PropsWithChildren): JSX.Element => { - const isOIDCFlowEnabled = useOptions().experimental?.oidcSelfServe ?? false; - const value = React.useMemo( () => ({ contentRef, @@ -69,7 +63,6 @@ export const ConfigureSSOProvider = ({ enterpriseConnectionMutations, organizationDomainMutations, onExit, - isOIDCFlowEnabled, }), [ contentRef, @@ -80,7 +73,6 @@ export const ConfigureSSOProvider = ({ organizationDomains, enterpriseConnection, onExit, - isOIDCFlowEnabled, ], ); diff --git a/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep/__tests__/ConfigureStep.test.tsx b/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep/__tests__/ConfigureStep.test.tsx index f4511cc458e..60d4af2e957 100644 --- a/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep/__tests__/ConfigureStep.test.tsx +++ b/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep/__tests__/ConfigureStep.test.tsx @@ -14,7 +14,6 @@ import type { EnterpriseConnectionProviderType } from '../../../types'; // left undefined so that footer self-hides in this isolated render. const contextState = vi.hoisted(() => ({ provider: undefined as string | undefined, - isOIDCFlowEnabled: true, enterpriseConnection: undefined as | { id: string; @@ -45,7 +44,6 @@ vi.mock('../../../ConfigureSSOContext', () => ({ provider: contextState.provider, hasConnection: true, }, - isOIDCFlowEnabled: contextState.isOIDCFlowEnabled, }), })); @@ -63,34 +61,28 @@ const { createFixtures } = bindCreateFixtures('ConfigureSSO'); describe('resolveConfigureSteps', () => { it('dispatches custom and legacy OIDC provider keys to the OIDC sub-flow', () => { - expect(resolveConfigureSteps('oauth_custom_clerk_dev', true)).toBe(OidcCustomConfigureSteps); - expect(resolveConfigureSteps('oidc_clerk_dev', true)).toBe(OidcCustomConfigureSteps); - expect(resolveConfigureSteps('oidc_ghe_acme', true)).toBe(OidcCustomConfigureSteps); - expect(resolveConfigureSteps('oidc_gitlab_ent_acme', true)).toBe(OidcCustomConfigureSteps); - expect(resolveConfigureSteps('oidc_custom', true)).toBe(OidcCustomConfigureSteps); - }); - - it('does not dispatch OIDC providers while the experimental flow is disabled', () => { - expect(resolveConfigureSteps('oauth_custom_clerk_dev', false)).toBeUndefined(); - expect(resolveConfigureSteps('oidc_clerk_dev', false)).toBeUndefined(); + expect(resolveConfigureSteps('oauth_custom_clerk_dev')).toBe(OidcCustomConfigureSteps); + expect(resolveConfigureSteps('oidc_clerk_dev')).toBe(OidcCustomConfigureSteps); + expect(resolveConfigureSteps('oidc_ghe_acme')).toBe(OidcCustomConfigureSteps); + expect(resolveConfigureSteps('oidc_gitlab_ent_acme')).toBe(OidcCustomConfigureSteps); + expect(resolveConfigureSteps('oidc_custom')).toBe(OidcCustomConfigureSteps); }); it('dispatches SAML providers by exact literal', () => { - expect(resolveConfigureSteps('saml_okta', false)).toBe(SamlOktaConfigureSteps); - expect(resolveConfigureSteps('saml_custom', false)).toBe(SamlCustomConfigureSteps); - expect(resolveConfigureSteps('saml_google', false)).toBe(SamlGoogleConfigureSteps); - expect(resolveConfigureSteps('saml_microsoft', false)).toBe(SamlMicrosoftConfigureSteps); + expect(resolveConfigureSteps('saml_okta')).toBe(SamlOktaConfigureSteps); + expect(resolveConfigureSteps('saml_custom')).toBe(SamlCustomConfigureSteps); + expect(resolveConfigureSteps('saml_google')).toBe(SamlGoogleConfigureSteps); + expect(resolveConfigureSteps('saml_microsoft')).toBe(SamlMicrosoftConfigureSteps); }); it('returns undefined for an unrecognized provider so the caller can degrade', () => { - expect(resolveConfigureSteps('ldap_enterprise' as EnterpriseConnectionProviderType, true)).toBeUndefined(); + expect(resolveConfigureSteps('ldap_enterprise' as EnterpriseConnectionProviderType)).toBeUndefined(); }); }); describe('ConfigureProviderStep', () => { beforeEach(() => { contextState.provider = undefined; - contextState.isOIDCFlowEnabled = true; contextState.enterpriseConnection = undefined; updateConnection.mockReset(); }); @@ -469,15 +461,4 @@ describe('ConfigureProviderStep', () => { expect(await screen.findByText(/unsupported provider/i)).toBeInTheDocument(); }); - - it('degrades to the unsupported-provider state for an existing OIDC connection when the flag is off', async () => { - contextState.provider = 'oauth_custom_clerk_dev'; - contextState.isOIDCFlowEnabled = false; - const { wrapper } = await createFixtures(); - - renderStep(wrapper); - - expect(await screen.findByText(/unsupported provider/i)).toBeInTheDocument(); - expect(screen.queryByText(/create a new oidc application/i)).not.toBeInTheDocument(); - }); }); diff --git a/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep/index.tsx b/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep/index.tsx index 6abf3e61005..c687f4c7d3b 100644 --- a/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep/index.tsx +++ b/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep/index.tsx @@ -28,13 +28,8 @@ const STEPS_BY_SAML_PROVIDER: Record export const resolveConfigureSteps = ( provider: EnterpriseConnectionProviderType, - isOIDCFlowEnabled: boolean, -): ConfigureStepsComponent | undefined => { - if (isOidcProvider(provider)) { - return isOIDCFlowEnabled ? OidcCustomConfigureSteps : undefined; - } - return STEPS_BY_SAML_PROVIDER[provider]; -}; +): ConfigureStepsComponent | undefined => + isOidcProvider(provider) ? OidcCustomConfigureSteps : STEPS_BY_SAML_PROVIDER[provider]; export const ConfigureStep = (): JSX.Element => { const { organizationEnterpriseConnection: c } = useConfigureSSO(); @@ -68,13 +63,13 @@ export const ConfigureStep = (): JSX.Element => { }; export const ConfigureProviderStep = (): JSX.Element | null => { - const { organizationEnterpriseConnection: c, isOIDCFlowEnabled } = useConfigureSSO(); + const { organizationEnterpriseConnection: c } = useConfigureSSO(); if (!c.provider) { return null; } - const ConfigureSteps = resolveConfigureSteps(c.provider, isOIDCFlowEnabled); + const ConfigureSteps = resolveConfigureSteps(c.provider); return ( diff --git a/packages/ui/src/components/ConfigureSSO/steps/SelectProviderStep.tsx b/packages/ui/src/components/ConfigureSSO/steps/SelectProviderStep.tsx index 7fde28ee673..c939cd88784 100644 --- a/packages/ui/src/components/ConfigureSSO/steps/SelectProviderStep.tsx +++ b/packages/ui/src/components/ConfigureSSO/steps/SelectProviderStep.tsx @@ -78,16 +78,10 @@ export const SelectProviderStep = (): JSX.Element => { organizationEnterpriseConnection: c, enterpriseConnectionMutations: { createConnection, changeProvider }, contentRef, - isOIDCFlowEnabled, } = useConfigureSSO(); const { goNext, goPrev, isFirstStep } = useWizard(); const { t } = useLocalizations(); - const providerGroups = React.useMemo( - () => PROVIDER_GROUPS.filter(group => group.id !== 'oidc' || isOIDCFlowEnabled), - [isOIDCFlowEnabled], - ); - const currentCard = c.provider ? toProviderCard(c.provider) : null; const [selected, setSelected] = React.useState(currentCard); @@ -166,7 +160,7 @@ export const SelectProviderStep = (): JSX.Element => { ({ gap: theme.space.$5 })}> - {providerGroups.map(group => ( + {PROVIDER_GROUPS.map(group => ( ({ provider: undefined as 'saml_okta' | 'saml_custom' | 'saml_google' | undefined, hasConnection: false, - isOIDCFlowEnabled: false, })); vi.mock('../../ConfigureSSOContext', () => ({ @@ -40,7 +39,6 @@ vi.mock('../../ConfigureSSOContext', () => ({ provider: contextState.provider, hasConnection: contextState.hasConnection, }, - isOIDCFlowEnabled: contextState.isOIDCFlowEnabled, }), })); @@ -64,7 +62,6 @@ const resetMocks = () => { changeProvider.mockResolvedValue(undefined); contextState.provider = undefined; contextState.hasConnection = false; - contextState.isOIDCFlowEnabled = false; }; describe('SelectProviderStep', () => { @@ -96,7 +93,7 @@ describe('SelectProviderStep', () => { // Each provider card is a