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
5 changes: 5 additions & 0 deletions .changeset/dirty-dragons-swim.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@clerk/elements": minor
---

Add full SAML support
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<Clerk.Loading scope='provider:saml'>
{isLoading => (
<Clerk.Connection
className='relative flex h-14 w-full cursor-pointer items-center justify-center text-xs text-[rgb(243,243,243)] transition-all duration-150'
disabled={isLoading}
name='saml'
type='button'
>
<span className='inline-flex items-center justify-center leading-loose'>
{isLoading ? (
<>
<Spinner /> Loading...
</>
) : (
children
)}
</span>
</Clerk.Connection>
)}
</Clerk.Loading>
);
}

function CustomProvider({
children,
provider,
Expand Down Expand Up @@ -139,7 +164,10 @@ export default function SignInPage() {
)}
</Clerk.Field>

<CustomSubmit>Sign in with Email</CustomSubmit>
<div className='flex w-full justify-between'>
<CustomSubmit>Sign in with Email</CustomSubmit>
<CustomSamlConnection>Continue with SAML</CustomSamlConnection>
</div>
</>
) : (
<TextButton onClick={() => setContinueWithEmail(true)}>Continue with Email</TextButton>
Expand Down Expand Up @@ -200,7 +228,10 @@ export default function SignInPage() {
)}
</Clerk.Field>

<CustomSubmit>Sign in with Email</CustomSubmit>
<div className='flex w-full justify-between'>
<CustomSubmit>Sign in with Email</CustomSubmit>
<CustomSamlConnection>Continue with SAML</CustomSamlConnection>
</div>
</>
) : (
<TextButton onClick={() => setContinueWithEmail(true)}>Continue with Email</TextButton>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<Clerk.Loading scope='provider:saml'>
{isLoading => (
<Clerk.Connection
className='relative flex h-14 w-full cursor-pointer items-center justify-center text-xs text-[rgb(243,243,243)] transition-all duration-150'
disabled={isLoading}
name='saml'
type='button'
>
<span className='inline-flex items-center justify-center leading-loose'>
{isLoading ? (
<>
<Spinner /> Loading...
</>
) : (
children
)}
</span>
</Clerk.Connection>
)}
</Clerk.Loading>
);
}

function CustomSubmit({ children }: ComponentProps<'button'>) {
return (
<SignUp.Action
Expand Down Expand Up @@ -85,19 +110,11 @@ export default function SignUpPage() {
label='Phone Number'
name='phoneNumber'
/>
<CustomSubmit>
<Clerk.Loading>
{isLoading =>
isLoading ? (
<>
<Spinner /> Loading...
</>
) : (
'Sign Up'
)
}
</Clerk.Loading>
</CustomSubmit>

<div className='flex w-full justify-between'>
<CustomSubmit>Sign Up</CustomSubmit>
<CustomSamlConnection>Continue with SAML</CustomSamlConnection>
</div>
</div>
</div>
</div>
Expand Down Expand Up @@ -154,7 +171,10 @@ export default function SignUpPage() {

<SignUp.Captcha id='test' />

<CustomSubmit>Sign Up</CustomSubmit>
<div className='flex w-full justify-between'>
<CustomSubmit>Sign Up</CustomSubmit>
<CustomSamlConnection>Continue with SAML</CustomSamlConnection>
</div>
</div>
</div>
</SignUp.Step>
Expand Down
15 changes: 12 additions & 3 deletions packages/elements/src/internals/machines/sign-in/router.machine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.',
Expand Down
15 changes: 12 additions & 3 deletions packages/elements/src/internals/machines/sign-up/router.machine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.',
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, Web3Provider } from '@clerk/types';
import type { 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;
name: OAuthProvider | Web3Provider | SamlStrategy;
}

/**
Expand Down
25 changes: 20 additions & 5 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/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';
Expand All @@ -11,18 +11,23 @@ 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
if (!clerk.loaded) {
return null;
}

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

const data = getEnabledThirdPartyProviders(clerk.__unstable__environment);

return (
Expand All @@ -35,10 +40,16 @@ export const useThirdPartyProvider = <
TActor extends ActorRef<any, SignInRouterEvents> | ActorRef<any, SignUpRouterEvents>,
>(
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<Element>) => {
Expand All @@ -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) {
Expand Down
47 changes: 35 additions & 12 deletions packages/elements/src/utils/third-party-strategies.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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'],
Expand Down