Skip to content
Merged
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
6 changes: 6 additions & 0 deletions .changeset/quiet-dingos-laugh.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@clerk/elements': patch
'@clerk/types': patch
---

Support `enterprise_sso` strategy (SAML, OIDC, EASIE) on custom flows with `@clerk/elements`
16 changes: 16 additions & 0 deletions packages/elements/src/internals/machines/sign-in/router.machine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,22 @@ export const SignInRouterMachine = setup({
},
})),
},
'AUTHENTICATE.ENTERPRISE_SSO': {
actions: sendTo(ThirdPartyMachineId, ({ context }) => ({
type: 'REDIRECT',
params: {
strategy: 'enterprise_sso',
identifier: context.formRef.getSnapshot().context.fields.get('identifier')?.value,
redirectUrl: `${
context.router?.mode === ROUTING.virtual
? context.clerk.__unstable__environment?.displayConfig.signInUrl
: context.router?.basePath
}${SSO_CALLBACK_PATH_ROUTE}`,
redirectUrlComplete:
context.router?.searchParams().get('redirect_url') || context.clerk.buildAfterSignInUrl(),
},
})),
},
'FORM.ATTACH': {
description: 'Attach/re-attach the form to the router.',
actions: enqueueActions(({ enqueue, event }) => {
Expand Down
16 changes: 16 additions & 0 deletions packages/elements/src/internals/machines/sign-up/router.machine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,22 @@ export const SignUpRouterMachine = setup({
},
})),
},
'AUTHENTICATE.ENTERPRISE_SSO': {
actions: sendTo(ThirdPartyMachineId, ({ context }) => ({
type: 'REDIRECT',
params: {
strategy: 'enterprise_sso',
emailAddress: context.formRef.getSnapshot().context.fields.get('emailAddress')?.value,
redirectUrl: `${
context.router?.mode === ROUTING.virtual
? context.clerk.__unstable__environment?.displayConfig.signUpUrl
: context.router?.basePath
}${SSO_CALLBACK_PATH_ROUTE}`,
redirectUrlComplete:
context.router?.searchParams().get('redirect_url') || context.clerk.buildAfterSignUpUrl(),
},
})),
},
'AUTHENTICATE.WEB3': {
actions: sendTo('start', ({ event }) => event),
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { ClerkAPIResponseError } from '@clerk/shared/error';
import type { OAuthStrategy, SamlStrategy, Web3Strategy } from '@clerk/types';
import type { EnterpriseSSOStrategy, OAuthStrategy, SamlStrategy, Web3Strategy } from '@clerk/types';
import type { ActorRefFrom, ErrorActorEvent } from 'xstate';

import type { FormMachine } from '~/internals/machines/form';
Expand All @@ -18,12 +18,17 @@ export type SignUpStartSubmitEvent = { type: 'SUBMIT'; action: 'submit' };
// TODO: Consolidate with SignInStartMachine
export type SignUpStartRedirectOauthEvent = { type: 'AUTHENTICATE.OAUTH'; strategy: OAuthStrategy };
export type SignUpStartRedirectSamlEvent = { type: 'AUTHENTICATE.SAML'; strategy?: SamlStrategy };
export type SignUpStartRedirectEnterpriseSSOEvent = {
type: 'AUTHENTICATE.ENTERPRISE_SSO';
strategy?: EnterpriseSSOStrategy;
};
export type SignUpStartRedirectWeb3Event = { type: 'AUTHENTICATE.WEB3'; strategy: Web3Strategy };

export type SignUpStartRedirectEvent =
| SignUpStartRedirectOauthEvent
| SignUpStartRedirectSamlEvent
| SignUpStartRedirectWeb3Event;
| SignUpStartRedirectWeb3Event
| SignUpStartRedirectEnterpriseSSOEvent;

export type SignUpStartEvents = ErrorActorEvent | SignUpStartSubmitEvent | SignUpStartRedirectEvent | SetFormEvent;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { ClerkRouter } from '@clerk/shared/router';
import type {
ClerkResource,
EnterpriseSSOStrategy,
LoadedClerk,
OAuthStrategy,
SamlStrategy,
Expand Down Expand Up @@ -46,13 +47,18 @@ export type BaseRouterLoadingEvent<TSteps extends BaseRouterLoadingStep> = (

export type BaseRouterRedirectOauthEvent = { type: 'AUTHENTICATE.OAUTH'; strategy: OAuthStrategy };
export type BaseRouterRedirectSamlEvent = { type: 'AUTHENTICATE.SAML'; strategy?: SamlStrategy };
export type BaseRouterRedirectEnterpriseSSOEvent = {
type: 'AUTHENTICATE.ENTERPRISE_SSO';
strategy?: EnterpriseSSOStrategy;
};
export type BaseRouterRedirectWeb3Event = { type: 'AUTHENTICATE.WEB3'; strategy: Web3Strategy };
export type BaseRouterSetClerkEvent = { type: 'CLERK.SET'; clerk: LoadedClerk };

export type BaseRouterRedirectEvent =
| BaseRouterRedirectOauthEvent
| BaseRouterRedirectSamlEvent
| BaseRouterRedirectWeb3Event;
| BaseRouterRedirectWeb3Event
| BaseRouterRedirectEnterpriseSSOEvent;

// ---------------------------------- Input ---------------------------------- //

Expand Down
4 changes: 2 additions & 2 deletions packages/elements/src/react/common/connections.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { OAuthProvider, SamlStrategy, Web3Provider } from '@clerk/types';
import type { EnterpriseSSOStrategy, OAuthProvider, SamlStrategy, Web3Provider } from '@clerk/types';
import { Slot } from '@radix-ui/react-slot';
import { createContext, useContext } from 'react';

Expand Down Expand Up @@ -29,7 +29,7 @@ export const useConnectionContext = () => {

export interface ConnectionProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
asChild?: boolean;
name: OAuthProvider | Web3Provider | SamlStrategy;
name: OAuthProvider | Web3Provider | SamlStrategy | EnterpriseSSOStrategy;
}

/**
Expand Down
4 changes: 2 additions & 2 deletions packages/elements/src/react/common/loading.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useClerk } from '@clerk/shared/react';
import { eventComponentMounted } from '@clerk/shared/telemetry';
import type { OAuthProvider, SamlStrategy, Web3Provider } from '@clerk/types';
import type { EnterpriseSSOStrategy, OAuthProvider, SamlStrategy, Web3Provider } from '@clerk/types';
import { useSelector } from '@xstate/react';
import * as React from 'react';

Expand All @@ -15,7 +15,7 @@ import type { TSignUpStep } from '~/react/sign-up/step';
import { SIGN_UP_STEPS } from '~/react/sign-up/step';
import { isProviderStrategyScope, mapScopeToStrategy } from '~/react/utils/map-scope-to-strategy';

type Strategy = OAuthProvider | SamlStrategy | Web3Provider;
type Strategy = OAuthProvider | SamlStrategy | EnterpriseSSOStrategy | Web3Provider;
type LoadingScope<T extends TSignInStep | TSignUpStep> =
| 'global'
| `step:${T}`
Expand Down
37 changes: 25 additions & 12 deletions packages/elements/src/react/hooks/use-third-party-provider.hook.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useClerk } from '@clerk/shared/react';
import type { OAuthProvider, SamlStrategy, Web3Provider } from '@clerk/types';
import type { EnterpriseSSOStrategy, OAuthProvider, SamlStrategy, Web3Provider } from '@clerk/types';
import type React from 'react';
import { useCallback } from 'react';
import type { ActorRef } from 'xstate';
Expand All @@ -11,21 +11,28 @@ import type { UseThirdPartyProviderReturn } from '~/react/common/connections';
import {
getEnabledThirdPartyProviders,
isAuthenticatableOauthStrategy,
isEnterpriseSSOStrategy,
isSamlStrategy,
isWeb3Strategy,
providerToDisplayData,
} from '~/utils/third-party-strategies';

const useIsProviderEnabled = (provider: OAuthProvider | Web3Provider | SamlStrategy): boolean | null => {
const useIsProviderEnabled = (
provider: OAuthProvider | Web3Provider | SamlStrategy | EnterpriseSSOStrategy,
): boolean | null => {
const clerk = useClerk();

// null indicates we don't know for sure
if (!clerk.loaded) {
return null;
}

if (provider === 'saml') {
return clerk.__unstable__environment?.userSettings.saml.enabled ?? false;
if (provider === 'saml' || provider === 'enterprise_sso') {
return (
clerk.__unstable__environment?.userSettings.saml.enabled ??
clerk.__unstable__environment?.userSettings.enterpriseSSO.enabled ??
false
);
}

const data = getEnabledThirdPartyProviders(clerk.__unstable__environment);
Expand All @@ -40,16 +47,18 @@ export const useThirdPartyProvider = <
TActor extends ActorRef<any, SignInRouterEvents> | ActorRef<any, SignUpRouterEvents>,
>(
ref: TActor,
provider: OAuthProvider | Web3Provider | SamlStrategy,
provider: OAuthProvider | Web3Provider | SamlStrategy | EnterpriseSSOStrategy,
): UseThirdPartyProviderReturn => {
const isProviderEnabled = useIsProviderEnabled(provider);
const isSaml = isSamlStrategy(provider);
const details = isSaml
? {
name: 'SAML',
strategy: 'saml' as SamlStrategy,
}
: providerToDisplayData[provider];
const isEnterpriseSSO = isEnterpriseSSOStrategy(provider);
const details =
isEnterpriseSSO || isSaml
? {
name: 'SSO',
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where this name is surfaced? On the alt of Clerk.Icon?

Also, saml and enterprise_sso don't have predefined icons, since they depend on the enterprise connection type and FAPI doesn't expose this beforehand.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@LauraBeatris Yes, it's largely surfaced there. Given the nature of Elements, it's just a default, and devs would know what connections they have available and either slot in their icon to Clerk.Icon or explicitly use their own.

strategy: provider,
}
: providerToDisplayData[provider];

const authenticate = useCallback(
(event: React.MouseEvent<Element>) => {
Expand All @@ -63,6 +72,10 @@ export const useThirdPartyProvider = <
return ref.send({ type: 'AUTHENTICATE.SAML' });
}

if (isEnterpriseSSO) {
return ref.send({ type: 'AUTHENTICATE.ENTERPRISE_SSO' });
}

if (provider === 'metamask') {
return ref.send({ type: 'AUTHENTICATE.WEB3', strategy: 'web3_metamask_signature' });
}
Expand All @@ -77,7 +90,7 @@ export const useThirdPartyProvider = <

return ref.send({ type: 'AUTHENTICATE.OAUTH', strategy: `oauth_${provider}` });
},
[provider, isProviderEnabled, isSaml, ref],
[provider, isProviderEnabled, isSaml, isEnterpriseSSO, ref],
);

if (isProviderEnabled === false) {
Expand Down
8 changes: 6 additions & 2 deletions packages/elements/src/react/utils/map-scope-to-strategy.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { OAuthProvider, SamlStrategy, SignInStrategy, Web3Provider } from '@clerk/types';
import type { EnterpriseSSOStrategy, OAuthProvider, SamlStrategy, SignInStrategy, Web3Provider } from '@clerk/types';

type Strategy = OAuthProvider | SamlStrategy | Web3Provider;
type Strategy = OAuthProvider | SamlStrategy | EnterpriseSSOStrategy | Web3Provider;

export function isProviderStrategyScope(value: string): value is Strategy {
return value.startsWith('provider:');
Expand All @@ -21,6 +21,10 @@ export function mapScopeToStrategy<T extends `provider:${Strategy}`>(scope: T):
return 'saml';
}

if (scope === 'provider:enterprise_sso') {
return 'enterprise_sso';
}

const scopeWithoutPrefix = scope.replace('provider:', '') as OAuthProvider;

return `oauth_${scopeWithoutPrefix}`;
Expand Down
9 changes: 7 additions & 2 deletions packages/elements/src/utils/third-party-strategies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { iconImageUrl } from '@clerk/shared/constants';
import { OAUTH_PROVIDERS } from '@clerk/shared/oauth';
import { WEB3_PROVIDERS } from '@clerk/shared/web3';
import type {
EnterpriseSSOStrategy,
EnvironmentResource,
OAuthProvider,
OAuthStrategy,
Expand All @@ -21,7 +22,7 @@ export type ThirdPartyStrategy =
name: string;
}
| {
strategy: SamlStrategy;
strategy: SamlStrategy | EnterpriseSSOStrategy;
iconUrl?: never;
name: string;
};
Expand All @@ -33,7 +34,7 @@ export type ThirdPartyProvider =
name: string;
}
| {
strategy: SamlStrategy;
strategy: SamlStrategy | EnterpriseSSOStrategy;
iconUrl?: never;
name: string;
};
Expand Down Expand Up @@ -72,6 +73,10 @@ export function isSamlStrategy(strategy: any): strategy is SamlStrategy {
return strategy === 'saml';
}

export function isEnterpriseSSOStrategy(strategy: any): strategy is EnterpriseSSOStrategy {
return strategy === 'enterprise_sso';
}

export function isWeb3Strategy(
strategy: any,
available: EnabledThirdPartyProviders['web3Strategies'],
Expand Down
2 changes: 1 addition & 1 deletion packages/types/src/signUp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ export type PrepareVerificationParams =
oidcLoginHint?: string;
}
| {
strategy: SamlStrategy;
strategy: SamlStrategy | EnterpriseSSOStrategy;
redirectUrl?: string;
actionCompleteRedirectUrl?: string;
};
Expand Down
Loading