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'],