diff --git a/.changeset/selfish-worms-switch.md b/.changeset/selfish-worms-switch.md new file mode 100644 index 00000000000..b4cf08be6f0 --- /dev/null +++ b/.changeset/selfish-worms-switch.md @@ -0,0 +1,8 @@ +--- +'@clerk/clerk-expo': minor +--- + +Introduce support for SSO with SAML + +- Introduce `useSSO` hook to support a wider range of SSO flow types +- Deprecate `useOAuth` in favor of new `useSSO` hook diff --git a/packages/expo/src/hooks/index.ts b/packages/expo/src/hooks/index.ts index 467cadf2b80..7b2b8313b36 100644 --- a/packages/expo/src/hooks/index.ts +++ b/packages/expo/src/hooks/index.ts @@ -10,5 +10,6 @@ export { useUser, } from '@clerk/clerk-react'; +export * from './useSSO'; export * from './useOAuth'; export * from './useAuth'; diff --git a/packages/expo/src/hooks/useOAuth.ts b/packages/expo/src/hooks/useOAuth.ts index 3a12dd09aa3..78cb793c242 100644 --- a/packages/expo/src/hooks/useOAuth.ts +++ b/packages/expo/src/hooks/useOAuth.ts @@ -24,6 +24,9 @@ export type StartOAuthFlowReturnType = { authSessionResult?: WebBrowser.WebBrowserAuthSessionResult; }; +/** + * @deprecated Use `useSSO` instead + */ export function useOAuth(useOAuthParams: UseOAuthFlowParams) { const { strategy } = useOAuthParams || {}; if (!strategy) { diff --git a/packages/expo/src/hooks/useSSO.ts b/packages/expo/src/hooks/useSSO.ts new file mode 100644 index 00000000000..d7f3226bc50 --- /dev/null +++ b/packages/expo/src/hooks/useSSO.ts @@ -0,0 +1,98 @@ +import { useSignIn, useSignUp } from '@clerk/clerk-react'; +import type { EnterpriseSSOStrategy, OAuthStrategy, SetActive, SignInResource, SignUpResource } from '@clerk/types'; +import * as AuthSession from 'expo-auth-session'; +import * as WebBrowser from 'expo-web-browser'; + +import { errorThrower } from '../utils/errors'; + +export type StartSSOFlowParams = { + unsafeMetadata?: SignUpUnsafeMetadata; +} & ( + | { + strategy: OAuthStrategy; + } + | { + strategy: EnterpriseSSOStrategy; + identifier: string; + } +); + +export type StartSSOFlowReturnType = { + createdSessionId: string | null; + authSessionResult?: WebBrowser.WebBrowserAuthSessionResult; + setActive?: SetActive; + signIn?: SignInResource; + signUp?: SignUpResource; +}; + +export function useSSO() { + const { signIn, setActive, isLoaded: isSignInLoaded } = useSignIn(); + const { signUp, isLoaded: isSignUpLoaded } = useSignUp(); + + async function startSSOFlow(startSSOFlowParams: StartSSOFlowParams): Promise { + if (!isSignInLoaded || !isSignUpLoaded) { + return { + createdSessionId: null, + signIn, + signUp, + setActive, + }; + } + + const { strategy, unsafeMetadata } = startSSOFlowParams ?? {}; + + /** + * Creates a redirect URL based on the application platform + * It must be whitelisted, either via Clerk Dashboard, or BAPI, in order + * to include the `rotating_token_nonce` on SSO callback + * @ref https://clerk.com/docs/reference/backend-api/tag/Redirect-URLs#operation/CreateRedirectURL + */ + const redirectUrl = AuthSession.makeRedirectUri({ + path: 'sso-callback', + }); + + await signIn.create({ + strategy, + redirectUrl, + ...(startSSOFlowParams.strategy === 'enterprise_sso' ? { identifier: startSSOFlowParams.identifier } : {}), + }); + + const { externalVerificationRedirectURL } = signIn.firstFactorVerification; + if (!externalVerificationRedirectURL) { + return errorThrower.throw('Missing external verification redirect URL for SSO flow'); + } + + const authSessionResult = await WebBrowser.openAuthSessionAsync(externalVerificationRedirectURL.toString()); + if (authSessionResult.type !== 'success' || !authSessionResult.url) { + return { + createdSessionId: null, + setActive, + signIn, + signUp, + }; + } + + const params = new URL(authSessionResult.url).searchParams; + const rotatingTokenNonce = params.get('rotating_token_nonce') ?? ''; + await signIn.reload({ rotatingTokenNonce }); + + const userNeedsToBeCreated = signIn.firstFactorVerification.status === 'transferable'; + if (userNeedsToBeCreated) { + await signUp.create({ + transfer: true, + unsafeMetadata, + }); + } + + return { + createdSessionId: signUp.createdSessionId ?? signIn.createdSessionId, + setActive, + signIn, + signUp, + }; + } + + return { + startSSOFlow, + }; +}