diff --git a/.changeset/dirty-dragons-swim.md b/.changeset/dirty-dragons-swim.md new file mode 100644 index 00000000000..d82794d36eb --- /dev/null +++ b/.changeset/dirty-dragons-swim.md @@ -0,0 +1,5 @@ +--- +"@clerk/elements": minor +--- + +Add full SAML support diff --git a/packages/elements/examples/nextjs/app/sign-in/[[...sign-in]]/page.tsx b/packages/elements/examples/nextjs/app/sign-in/[[...sign-in]]/page.tsx index 0b7cbf23952..db8d344480f 100644 --- a/packages/elements/examples/nextjs/app/sign-in/[[...sign-in]]/page.tsx +++ b/packages/elements/examples/nextjs/app/sign-in/[[...sign-in]]/page.tsx @@ -9,6 +9,31 @@ import { H1, H3, P } from '@/components/design'; import { CustomField } from '@/components/form'; import { Spinner } from '@/components/spinner'; +function CustomSamlConnection({ children }: { children: string }) { + return ( + + {isLoading => ( + + + {isLoading ? ( + <> + Loading... + + ) : ( + children + )} + + + )} + + ); +} + function CustomProvider({ children, provider, @@ -139,7 +164,10 @@ export default function SignInPage() { )} - Sign in with Email +
+ Sign in with Email + Continue with SAML +
) : ( setContinueWithEmail(true)}>Continue with Email @@ -200,7 +228,10 @@ export default function SignInPage() { )} - Sign in with Email +
+ Sign in with Email + Continue with SAML +
) : ( setContinueWithEmail(true)}>Continue with Email diff --git a/packages/elements/examples/nextjs/app/sign-up/[[...sign-up]]/page.tsx b/packages/elements/examples/nextjs/app/sign-up/[[...sign-up]]/page.tsx index aeaf6ca5734..da1cd079a6d 100644 --- a/packages/elements/examples/nextjs/app/sign-up/[[...sign-up]]/page.tsx +++ b/packages/elements/examples/nextjs/app/sign-up/[[...sign-up]]/page.tsx @@ -9,6 +9,31 @@ import { H1, HR as Hr, P } from '@/components/design'; import { CustomField } from '@/components/form'; import { Spinner } from '@/components/spinner'; +function CustomSamlConnection({ children }: { children: string }) { + return ( + + {isLoading => ( + + + {isLoading ? ( + <> + Loading... + + ) : ( + children + )} + + + )} + + ); +} + function CustomSubmit({ children }: ComponentProps<'button'>) { return ( - - - {isLoading => - isLoading ? ( - <> - Loading... - - ) : ( - 'Sign Up' - ) - } - - + +
+ Sign Up + Continue with SAML +
@@ -154,7 +171,10 @@ export default function SignUpPage() { - Sign Up +
+ Sign Up + Continue with SAML +
diff --git a/packages/elements/src/internals/machines/sign-in/router.machine.ts b/packages/elements/src/internals/machines/sign-in/router.machine.ts index e368e308003..31f1870acc3 100644 --- a/packages/elements/src/internals/machines/sign-in/router.machine.ts +++ b/packages/elements/src/internals/machines/sign-in/router.machine.ts @@ -191,10 +191,19 @@ export const SignInRouterMachine = setup({ })), }, 'AUTHENTICATE.SAML': { - actions: sendTo(ThirdPartyMachineId, { + actions: sendTo(ThirdPartyMachineId, ({ context }) => ({ type: 'REDIRECT', - params: { strategy: 'saml' }, - }), + params: { + strategy: 'saml', + 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.clerk.buildAfterSignInUrl(), + }, + })), }, 'FORM.ATTACH': { description: 'Attach/re-attach the form to the router.', diff --git a/packages/elements/src/internals/machines/sign-up/router.machine.ts b/packages/elements/src/internals/machines/sign-up/router.machine.ts index 10f184991b8..3fd315a28bc 100644 --- a/packages/elements/src/internals/machines/sign-up/router.machine.ts +++ b/packages/elements/src/internals/machines/sign-up/router.machine.ts @@ -192,10 +192,19 @@ export const SignUpRouterMachine = setup({ })), }, 'AUTHENTICATE.SAML': { - actions: sendTo(ThirdPartyMachineId, { + actions: sendTo(ThirdPartyMachineId, ({ context }) => ({ type: 'REDIRECT', - params: { strategy: 'saml' }, - }), + params: { + strategy: 'saml', + 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.clerk.buildAfterSignUpUrl(), + }, + })), }, 'FORM.ATTACH': { description: 'Attach/re-attach the form to the router.', diff --git a/packages/elements/src/react/common/connections.tsx b/packages/elements/src/react/common/connections.tsx index c8796c4bbc1..0f949337741 100644 --- a/packages/elements/src/react/common/connections.tsx +++ b/packages/elements/src/react/common/connections.tsx @@ -1,4 +1,4 @@ -import type { OAuthProvider, Web3Provider } from '@clerk/types'; +import type { OAuthProvider, SamlStrategy, Web3Provider } from '@clerk/types'; import { Slot } from '@radix-ui/react-slot'; import { createContext, useContext } from 'react'; @@ -29,7 +29,7 @@ export const useConnectionContext = () => { export interface ConnectionProps extends React.ButtonHTMLAttributes { asChild?: boolean; - name: OAuthProvider | Web3Provider; + name: OAuthProvider | Web3Provider | SamlStrategy; } /** diff --git a/packages/elements/src/react/hooks/use-third-party-provider.hook.ts b/packages/elements/src/react/hooks/use-third-party-provider.hook.ts index 0dc95d024e4..d0f31b41d84 100644 --- a/packages/elements/src/react/hooks/use-third-party-provider.hook.ts +++ b/packages/elements/src/react/hooks/use-third-party-provider.hook.ts @@ -1,5 +1,5 @@ import { useClerk } from '@clerk/clerk-react'; -import type { OAuthProvider, Web3Provider } from '@clerk/types'; +import type { OAuthProvider, SamlStrategy, Web3Provider } from '@clerk/types'; import type React from 'react'; import { useCallback } from 'react'; import type { ActorRef } from 'xstate'; @@ -11,11 +11,12 @@ import type { UseThirdPartyProviderReturn } from '~/react/common/connections'; import { getEnabledThirdPartyProviders, isAuthenticatableOauthStrategy, + isSamlStrategy, isWeb3Strategy, providerToDisplayData, } from '~/utils/third-party-strategies'; -const useIsProviderEnabled = (provider: OAuthProvider | Web3Provider): boolean | null => { +const useIsProviderEnabled = (provider: OAuthProvider | Web3Provider | SamlStrategy): boolean | null => { const clerk = useClerk(); // null indicates we don't know for sure @@ -23,6 +24,10 @@ const useIsProviderEnabled = (provider: OAuthProvider | Web3Provider): boolean | return null; } + if (provider === 'saml') { + return clerk.__unstable__environment?.userSettings.saml.enabled ?? false; + } + const data = getEnabledThirdPartyProviders(clerk.__unstable__environment); return ( @@ -35,10 +40,16 @@ export const useThirdPartyProvider = < TActor extends ActorRef | ActorRef, >( ref: TActor, - provider: OAuthProvider | Web3Provider, + provider: OAuthProvider | Web3Provider | SamlStrategy, ): UseThirdPartyProviderReturn => { const isProviderEnabled = useIsProviderEnabled(provider); - const details = providerToDisplayData[provider]; + const isSaml = isSamlStrategy(provider); + const details = isSaml + ? { + name: 'SAML', + strategy: 'saml' as SamlStrategy, + } + : providerToDisplayData[provider]; const authenticate = useCallback( (event: React.MouseEvent) => { @@ -48,13 +59,17 @@ export const useThirdPartyProvider = < event.preventDefault(); + if (isSaml) { + return ref.send({ type: 'AUTHENTICATE.SAML' }); + } + if (provider === 'metamask') { return ref.send({ type: 'AUTHENTICATE.WEB3', strategy: 'web3_metamask_signature' }); } return ref.send({ type: 'AUTHENTICATE.OAUTH', strategy: `oauth_${provider}` }); }, - [provider, isProviderEnabled, ref], + [provider, isProviderEnabled, isSaml, ref], ); if (isProviderEnabled === false) { diff --git a/packages/elements/src/utils/third-party-strategies.ts b/packages/elements/src/utils/third-party-strategies.ts index b559b63f1a2..0653e031374 100644 --- a/packages/elements/src/utils/third-party-strategies.ts +++ b/packages/elements/src/utils/third-party-strategies.ts @@ -1,21 +1,40 @@ // c.f. vendor/clerk-js/src/ui/hooks/useEnabledThirdPartyProviders.tsx [Modified] -import type { EnvironmentResource, OAuthProvider, OAuthStrategy, Web3Provider, Web3Strategy } from '@clerk/types'; +import type { + EnvironmentResource, + OAuthProvider, + OAuthStrategy, + SamlStrategy, + Web3Provider, + Web3Strategy, +} from '@clerk/types'; import { OAUTH_PROVIDERS, WEB3_PROVIDERS } from '@clerk/types'; // TODO: This import shouldn't be part of @clerk/types import { fromEntries, iconImageUrl } from './clerk-js'; -export interface ThirdPartyStrategy { - id: Web3Strategy | OAuthStrategy; - iconUrl: string; - name: string; -} - -export interface ThirdPartyProvider { - strategy: Web3Strategy | OAuthStrategy; - iconUrl: string; - name: string; -} +export type ThirdPartyStrategy = + | { + id: Web3Strategy | OAuthStrategy; + iconUrl: string; + name: string; + } + | { + strategy: SamlStrategy; + iconUrl?: never; + name: string; + }; + +export type ThirdPartyProvider = + | { + strategy: Web3Strategy | OAuthStrategy; + iconUrl: string; + name: string; + } + | { + strategy: SamlStrategy; + iconUrl?: never; + name: string; + }; type ThirdPartyStrategyToDataMap = { [k in Web3Strategy | OAuthStrategy]: ThirdPartyStrategy; @@ -47,6 +66,10 @@ const strategyToDisplayData: ThirdPartyStrategyToDataMap = fromEntries( }), ) as ThirdPartyStrategyToDataMap; +export function isSamlStrategy(strategy: any): strategy is SamlStrategy { + return strategy === 'saml'; +} + export function isWeb3Strategy( strategy: any, available: EnabledThirdPartyProviders['web3Strategies'],