From 07c298343781071ddc1e4b8f8499129b3f9ec14f Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Mon, 13 Jan 2025 09:11:17 -0300 Subject: [PATCH 01/14] Refactor `useOAuth` to `useSso` --- packages/expo/src/hooks/index.ts | 2 +- .../expo/src/hooks/{useOAuth.ts => useSso.ts} | 41 ++++++++++--------- 2 files changed, 23 insertions(+), 20 deletions(-) rename packages/expo/src/hooks/{useOAuth.ts => useSso.ts} (71%) diff --git a/packages/expo/src/hooks/index.ts b/packages/expo/src/hooks/index.ts index 467cadf2b80..58aa8b943bc 100644 --- a/packages/expo/src/hooks/index.ts +++ b/packages/expo/src/hooks/index.ts @@ -10,5 +10,5 @@ export { useUser, } from '@clerk/clerk-react'; -export * from './useOAuth'; +export * from './useSso'; export * from './useAuth'; diff --git a/packages/expo/src/hooks/useOAuth.ts b/packages/expo/src/hooks/useSso.ts similarity index 71% rename from packages/expo/src/hooks/useOAuth.ts rename to packages/expo/src/hooks/useSso.ts index 3a12dd09aa3..e3e62815ca3 100644 --- a/packages/expo/src/hooks/useOAuth.ts +++ b/packages/expo/src/hooks/useSso.ts @@ -1,22 +1,22 @@ import { useSignIn, useSignUp } from '@clerk/clerk-react'; -import type { OAuthStrategy, SetActive, SignInResource, SignUpResource } from '@clerk/types'; +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 UseOAuthFlowParams = { - strategy: OAuthStrategy; +export type UseSsoParams = { + strategy: OAuthStrategy | EnterpriseSSOStrategy; redirectUrl?: string; unsafeMetadata?: SignUpUnsafeMetadata; }; -export type StartOAuthFlowParams = { +export type StartSsoParams = { redirectUrl?: string; unsafeMetadata?: SignUpUnsafeMetadata; }; -export type StartOAuthFlowReturnType = { +export type StartSsoFlowReturnType = { createdSessionId: string; setActive?: SetActive; signIn?: SignInResource; @@ -24,16 +24,16 @@ export type StartOAuthFlowReturnType = { authSessionResult?: WebBrowser.WebBrowserAuthSessionResult; }; -export function useOAuth(useOAuthParams: UseOAuthFlowParams) { - const { strategy } = useOAuthParams || {}; +export function useSso(useSsoParams: UseSsoParams) { + const { strategy } = useSsoParams || {}; if (!strategy) { - return errorThrower.throw('Missing oauth strategy'); + return errorThrower.throw('Missing strategy'); } const { signIn, setActive, isLoaded: isSignInLoaded } = useSignIn(); const { signUp, isLoaded: isSignUpLoaded } = useSignUp(); - async function startOAuthFlow(startOAuthFlowParams?: StartOAuthFlowParams): Promise { + async function startFlow(startSsoFlowParams?: StartSsoParams): Promise { if (!isSignInLoaded || !isSignUpLoaded) { return { createdSessionId: '', @@ -50,24 +50,27 @@ export function useOAuth(useOAuthParams: UseOAuthFlowParams) { // // For more information go to: // https://docs.expo.dev/versions/latest/sdk/auth-session/#authsessionmakeredirecturi - const oauthRedirectUrl = - startOAuthFlowParams?.redirectUrl || - useOAuthParams.redirectUrl || + const redirectUrl = + startSsoFlowParams?.redirectUrl || + useSsoParams.redirectUrl || AuthSession.makeRedirectUri({ - path: 'oauth-native-callback', + path: 'sso-native-callback', }); - await signIn.create({ strategy, redirectUrl: oauthRedirectUrl }); + await signIn.create({ strategy, redirectUrl }); const { externalVerificationRedirectURL } = signIn.firstFactorVerification; + if (!externalVerificationRedirectURL) { + return errorThrower.throw('Missing external verification redirect URL for SSO flow'); + } + const authSessionResult = await WebBrowser.openAuthSessionAsync( - // @ts-ignore externalVerificationRedirectURL.toString(), - oauthRedirectUrl, + redirectUrl, ); - // @ts-expect-error + // @ts-ignore const { type, url } = authSessionResult || {}; // TODO: Check all the possible AuthSession results @@ -96,7 +99,7 @@ export function useOAuth(useOAuthParams: UseOAuthFlowParams) { } else if (firstFactorVerification.status === 'transferable') { await signUp.create({ transfer: true, - unsafeMetadata: startOAuthFlowParams?.unsafeMetadata || useOAuthParams.unsafeMetadata, + unsafeMetadata: startSsoFlowParams?.unsafeMetadata || useSsoParams.unsafeMetadata, }); createdSessionId = signUp.createdSessionId || ''; } @@ -111,6 +114,6 @@ export function useOAuth(useOAuthParams: UseOAuthFlowParams) { } return { - startOAuthFlow, + startFlow, }; } From 94791c8e7894a462a07dfedba8b001931bf39c1d Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Mon, 13 Jan 2025 09:18:01 -0300 Subject: [PATCH 02/14] Add changeset --- .changeset/selfish-worms-switch.md | 8 ++++++++ packages/expo/src/hooks/useSso.ts | 8 ++++---- 2 files changed, 12 insertions(+), 4 deletions(-) create mode 100644 .changeset/selfish-worms-switch.md diff --git a/.changeset/selfish-worms-switch.md b/.changeset/selfish-worms-switch.md new file mode 100644 index 00000000000..75055f90804 --- /dev/null +++ b/.changeset/selfish-worms-switch.md @@ -0,0 +1,8 @@ +--- +'@clerk/clerk-expo': minor +--- + +Introduces support for SSO with SAML + +- Rename `useOAuth` to `useSso` to support a wider range of protocols +- Update default redirect URI to from `oauth-native-callback` to `sso-native-callback` diff --git a/packages/expo/src/hooks/useSso.ts b/packages/expo/src/hooks/useSso.ts index e3e62815ca3..0818fb8e16f 100644 --- a/packages/expo/src/hooks/useSso.ts +++ b/packages/expo/src/hooks/useSso.ts @@ -50,14 +50,14 @@ export function useSso(useSsoParams: UseSsoParams) { // // For more information go to: // https://docs.expo.dev/versions/latest/sdk/auth-session/#authsessionmakeredirecturi - const redirectUrl = + const oauthRedirectUrl = startSsoFlowParams?.redirectUrl || useSsoParams.redirectUrl || AuthSession.makeRedirectUri({ path: 'sso-native-callback', }); - await signIn.create({ strategy, redirectUrl }); + await signIn.create({ strategy, redirectUrl: oauthRedirectUrl }); const { externalVerificationRedirectURL } = signIn.firstFactorVerification; @@ -67,10 +67,10 @@ export function useSso(useSsoParams: UseSsoParams) { const authSessionResult = await WebBrowser.openAuthSessionAsync( externalVerificationRedirectURL.toString(), - redirectUrl, + oauthRedirectUrl, ); - // @ts-ignore + // @ts-expect-error const { type, url } = authSessionResult || {}; // TODO: Check all the possible AuthSession results From f3391771d6e7a4eb41016bbcb17dbecf69d33588 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Mon, 13 Jan 2025 09:55:59 -0300 Subject: [PATCH 03/14] Pass identifier when creating sign in --- packages/expo/src/hooks/useSso.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/expo/src/hooks/useSso.ts b/packages/expo/src/hooks/useSso.ts index 0818fb8e16f..6d3eff43a69 100644 --- a/packages/expo/src/hooks/useSso.ts +++ b/packages/expo/src/hooks/useSso.ts @@ -14,6 +14,7 @@ export type UseSsoParams = { export type StartSsoParams = { redirectUrl?: string; unsafeMetadata?: SignUpUnsafeMetadata; + identifier?: string; }; export type StartSsoFlowReturnType = { @@ -57,7 +58,7 @@ export function useSso(useSsoParams: UseSsoParams) { path: 'sso-native-callback', }); - await signIn.create({ strategy, redirectUrl: oauthRedirectUrl }); + await signIn.create({ strategy, redirectUrl: oauthRedirectUrl, identifier: startSsoFlowParams?.identifier }); const { externalVerificationRedirectURL } = signIn.firstFactorVerification; From 2ca164768a31c586b85ce32a31c2c1d1c5fc2e81 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Tue, 14 Jan 2025 09:34:23 -0300 Subject: [PATCH 04/14] Deprecate `useOAuth` in favor of `useSSO` --- .changeset/selfish-worms-switch.md | 4 +- packages/expo/src/hooks/index.ts | 3 +- packages/expo/src/hooks/useOAuth.ts | 119 ++++++++++++++++++++++++++++ packages/expo/src/hooks/useSso.ts | 20 ++--- 4 files changed, 133 insertions(+), 13 deletions(-) create mode 100644 packages/expo/src/hooks/useOAuth.ts diff --git a/.changeset/selfish-worms-switch.md b/.changeset/selfish-worms-switch.md index 75055f90804..3c6c5ce55ef 100644 --- a/.changeset/selfish-worms-switch.md +++ b/.changeset/selfish-worms-switch.md @@ -4,5 +4,5 @@ Introduces support for SSO with SAML -- Rename `useOAuth` to `useSso` to support a wider range of protocols -- Update default redirect URI to from `oauth-native-callback` to `sso-native-callback` +- Introduce `useSSO` hook to support a wider range of SSO flow types +- Deprecated `useOAuth` in favor of new `useSSO` hook diff --git a/packages/expo/src/hooks/index.ts b/packages/expo/src/hooks/index.ts index 58aa8b943bc..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 './useSSO'; +export * from './useOAuth'; export * from './useAuth'; diff --git a/packages/expo/src/hooks/useOAuth.ts b/packages/expo/src/hooks/useOAuth.ts new file mode 100644 index 00000000000..78cb793c242 --- /dev/null +++ b/packages/expo/src/hooks/useOAuth.ts @@ -0,0 +1,119 @@ +import { useSignIn, useSignUp } from '@clerk/clerk-react'; +import type { 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 UseOAuthFlowParams = { + strategy: OAuthStrategy; + redirectUrl?: string; + unsafeMetadata?: SignUpUnsafeMetadata; +}; + +export type StartOAuthFlowParams = { + redirectUrl?: string; + unsafeMetadata?: SignUpUnsafeMetadata; +}; + +export type StartOAuthFlowReturnType = { + createdSessionId: string; + setActive?: SetActive; + signIn?: SignInResource; + signUp?: SignUpResource; + authSessionResult?: WebBrowser.WebBrowserAuthSessionResult; +}; + +/** + * @deprecated Use `useSSO` instead + */ +export function useOAuth(useOAuthParams: UseOAuthFlowParams) { + const { strategy } = useOAuthParams || {}; + if (!strategy) { + return errorThrower.throw('Missing oauth strategy'); + } + + const { signIn, setActive, isLoaded: isSignInLoaded } = useSignIn(); + const { signUp, isLoaded: isSignUpLoaded } = useSignUp(); + + async function startOAuthFlow(startOAuthFlowParams?: StartOAuthFlowParams): Promise { + if (!isSignInLoaded || !isSignUpLoaded) { + return { + createdSessionId: '', + signIn, + signUp, + setActive, + }; + } + + // Create a redirect url for the current platform and environment. + // + // This redirect URL needs to be whitelisted for your Clerk production instance via + // https://clerk.com/docs/reference/backend-api/tag/Redirect-URLs#operation/CreateRedirectURL + // + // For more information go to: + // https://docs.expo.dev/versions/latest/sdk/auth-session/#authsessionmakeredirecturi + const oauthRedirectUrl = + startOAuthFlowParams?.redirectUrl || + useOAuthParams.redirectUrl || + AuthSession.makeRedirectUri({ + path: 'oauth-native-callback', + }); + + await signIn.create({ strategy, redirectUrl: oauthRedirectUrl }); + + const { externalVerificationRedirectURL } = signIn.firstFactorVerification; + + const authSessionResult = await WebBrowser.openAuthSessionAsync( + // @ts-ignore + externalVerificationRedirectURL.toString(), + oauthRedirectUrl, + ); + + // @ts-expect-error + const { type, url } = authSessionResult || {}; + + // TODO: Check all the possible AuthSession results + // https://docs.expo.dev/versions/latest/sdk/auth-session/#returns-7 + if (type !== 'success') { + return { + authSessionResult, + createdSessionId: '', + setActive, + signIn, + signUp, + }; + } + + const params = new URL(url).searchParams; + + const rotatingTokenNonce = params.get('rotating_token_nonce') || ''; + await signIn.reload({ rotatingTokenNonce }); + + const { status, firstFactorVerification } = signIn; + + let createdSessionId = ''; + + if (status === 'complete') { + createdSessionId = signIn.createdSessionId!; + } else if (firstFactorVerification.status === 'transferable') { + await signUp.create({ + transfer: true, + unsafeMetadata: startOAuthFlowParams?.unsafeMetadata || useOAuthParams.unsafeMetadata, + }); + createdSessionId = signUp.createdSessionId || ''; + } + + return { + authSessionResult, + createdSessionId, + setActive, + signIn, + signUp, + }; + } + + return { + startOAuthFlow, + }; +} diff --git a/packages/expo/src/hooks/useSso.ts b/packages/expo/src/hooks/useSso.ts index 6d3eff43a69..2a1d2560e21 100644 --- a/packages/expo/src/hooks/useSso.ts +++ b/packages/expo/src/hooks/useSso.ts @@ -5,19 +5,19 @@ import * as WebBrowser from 'expo-web-browser'; import { errorThrower } from '../utils/errors'; -export type UseSsoParams = { +export type UseSSOParams = { strategy: OAuthStrategy | EnterpriseSSOStrategy; redirectUrl?: string; unsafeMetadata?: SignUpUnsafeMetadata; }; -export type StartSsoParams = { +export type StartSSOParams = { redirectUrl?: string; unsafeMetadata?: SignUpUnsafeMetadata; identifier?: string; }; -export type StartSsoFlowReturnType = { +export type StartSSOFlowReturnType = { createdSessionId: string; setActive?: SetActive; signIn?: SignInResource; @@ -25,8 +25,8 @@ export type StartSsoFlowReturnType = { authSessionResult?: WebBrowser.WebBrowserAuthSessionResult; }; -export function useSso(useSsoParams: UseSsoParams) { - const { strategy } = useSsoParams || {}; +export function useSSO(useSSOParams: UseSSOParams) { + const { strategy } = useSSOParams || {}; if (!strategy) { return errorThrower.throw('Missing strategy'); } @@ -34,7 +34,7 @@ export function useSso(useSsoParams: UseSsoParams) { const { signIn, setActive, isLoaded: isSignInLoaded } = useSignIn(); const { signUp, isLoaded: isSignUpLoaded } = useSignUp(); - async function startFlow(startSsoFlowParams?: StartSsoParams): Promise { + async function startFlow(startSSOFlowParams?: StartSSOParams): Promise { if (!isSignInLoaded || !isSignUpLoaded) { return { createdSessionId: '', @@ -52,13 +52,13 @@ export function useSso(useSsoParams: UseSsoParams) { // For more information go to: // https://docs.expo.dev/versions/latest/sdk/auth-session/#authsessionmakeredirecturi const oauthRedirectUrl = - startSsoFlowParams?.redirectUrl || - useSsoParams.redirectUrl || + startSSOFlowParams?.redirectUrl || + useSSOParams.redirectUrl || AuthSession.makeRedirectUri({ path: 'sso-native-callback', }); - await signIn.create({ strategy, redirectUrl: oauthRedirectUrl, identifier: startSsoFlowParams?.identifier }); + await signIn.create({ strategy, redirectUrl: oauthRedirectUrl, identifier: startSSOFlowParams?.identifier }); const { externalVerificationRedirectURL } = signIn.firstFactorVerification; @@ -100,7 +100,7 @@ export function useSso(useSsoParams: UseSsoParams) { } else if (firstFactorVerification.status === 'transferable') { await signUp.create({ transfer: true, - unsafeMetadata: startSsoFlowParams?.unsafeMetadata || useSsoParams.unsafeMetadata, + unsafeMetadata: startSSOFlowParams?.unsafeMetadata || useSSOParams.unsafeMetadata, }); createdSessionId = signUp.createdSessionId || ''; } From 4ff145cad25385d53d50925df95895a78b8dbbad Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Tue, 14 Jan 2025 15:04:39 -0300 Subject: [PATCH 05/14] Refactor implementation --- .changeset/selfish-worms-switch.md | 4 +- .../expo/src/hooks/{useSso.ts => useSSO.ts} | 73 ++++++++----------- 2 files changed, 34 insertions(+), 43 deletions(-) rename packages/expo/src/hooks/{useSso.ts => useSSO.ts} (59%) diff --git a/.changeset/selfish-worms-switch.md b/.changeset/selfish-worms-switch.md index 3c6c5ce55ef..b4cf08be6f0 100644 --- a/.changeset/selfish-worms-switch.md +++ b/.changeset/selfish-worms-switch.md @@ -2,7 +2,7 @@ '@clerk/clerk-expo': minor --- -Introduces support for SSO with SAML +Introduce support for SSO with SAML - Introduce `useSSO` hook to support a wider range of SSO flow types -- Deprecated `useOAuth` in favor of new `useSSO` hook +- Deprecate `useOAuth` in favor of new `useSSO` hook diff --git a/packages/expo/src/hooks/useSso.ts b/packages/expo/src/hooks/useSSO.ts similarity index 59% rename from packages/expo/src/hooks/useSso.ts rename to packages/expo/src/hooks/useSSO.ts index 2a1d2560e21..b1751903631 100644 --- a/packages/expo/src/hooks/useSso.ts +++ b/packages/expo/src/hooks/useSSO.ts @@ -7,18 +7,22 @@ import { errorThrower } from '../utils/errors'; export type UseSSOParams = { strategy: OAuthStrategy | EnterpriseSSOStrategy; - redirectUrl?: string; unsafeMetadata?: SignUpUnsafeMetadata; + redirectUrl?: string; }; export type StartSSOParams = { - redirectUrl?: string; - unsafeMetadata?: SignUpUnsafeMetadata; identifier?: string; + unsafeMetadata?: SignUpUnsafeMetadata; + redirectUrl?: string; }; export type StartSSOFlowReturnType = { - createdSessionId: string; + /** + * Session ID created upon sign-in completion, or null if incomplete. + * If incomplete, use signIn or signUp for next steps like MFA. + */ + createdSessionId: string | null; setActive?: SetActive; signIn?: SignInResource; signUp?: SignUpResource; @@ -26,83 +30,70 @@ export type StartSSOFlowReturnType = { }; export function useSSO(useSSOParams: UseSSOParams) { - const { strategy } = useSSOParams || {}; - if (!strategy) { - return errorThrower.throw('Missing strategy'); - } - const { signIn, setActive, isLoaded: isSignInLoaded } = useSignIn(); const { signUp, isLoaded: isSignUpLoaded } = useSignUp(); async function startFlow(startSSOFlowParams?: StartSSOParams): Promise { if (!isSignInLoaded || !isSignUpLoaded) { return { - createdSessionId: '', + createdSessionId: null, signIn, signUp, setActive, }; } - // Create a redirect url for the current platform and environment. - // - // This redirect URL needs to be whitelisted for your Clerk production instance via - // https://clerk.com/docs/reference/backend-api/tag/Redirect-URLs#operation/CreateRedirectURL - // - // For more information go to: - // https://docs.expo.dev/versions/latest/sdk/auth-session/#authsessionmakeredirecturi - const oauthRedirectUrl = + let createdSessionId = signIn.createdSessionId; + + const redirectUrl = startSSOFlowParams?.redirectUrl || useSSOParams.redirectUrl || AuthSession.makeRedirectUri({ path: 'sso-native-callback', }); - await signIn.create({ strategy, redirectUrl: oauthRedirectUrl, identifier: startSSOFlowParams?.identifier }); + await signIn.create({ + strategy: useSSOParams.strategy, + redirectUrl, + identifier: startSSOFlowParams?.identifier, + }); const { externalVerificationRedirectURL } = signIn.firstFactorVerification; - if (!externalVerificationRedirectURL) { - return errorThrower.throw('Missing external verification redirect URL for SSO flow'); + return errorThrower.throw( + 'Missing external verification redirect URL for SSO flow. This indicates an API issue - please contact support for assistance.', + ); } const authSessionResult = await WebBrowser.openAuthSessionAsync( externalVerificationRedirectURL.toString(), - oauthRedirectUrl, + redirectUrl, ); - - // @ts-expect-error - const { type, url } = authSessionResult || {}; - - // TODO: Check all the possible AuthSession results - // https://docs.expo.dev/versions/latest/sdk/auth-session/#returns-7 - if (type !== 'success') { + if (authSessionResult.type !== 'success' || !authSessionResult.url) { return { authSessionResult, - createdSessionId: '', + createdSessionId, setActive, signIn, signUp, }; } - const params = new URL(url).searchParams; + const params = new URL(authSessionResult.url).searchParams; + const rotatingTokenNonce = params.get('rotating_token_nonce'); + if (!rotatingTokenNonce) { + return errorThrower.throw( + 'Missing rotating_token_nonce in SSO callback. This indicates an API issue - please contact support for assistance.', + ); + } - const rotatingTokenNonce = params.get('rotating_token_nonce') || ''; await signIn.reload({ rotatingTokenNonce }); - - const { status, firstFactorVerification } = signIn; - - let createdSessionId = ''; - - if (status === 'complete') { - createdSessionId = signIn.createdSessionId!; - } else if (firstFactorVerification.status === 'transferable') { + if (signIn.firstFactorVerification.status === 'transferable') { await signUp.create({ transfer: true, unsafeMetadata: startSSOFlowParams?.unsafeMetadata || useSSOParams.unsafeMetadata, }); - createdSessionId = signUp.createdSessionId || ''; + createdSessionId = signUp.createdSessionId; } return { From fbbfe8bae796e994393b50ed5a723b3736982df0 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Tue, 14 Jan 2025 17:12:49 -0300 Subject: [PATCH 06/14] Close web browser popup on SSO callback --- packages/expo/src/hooks/useSSO.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/expo/src/hooks/useSSO.ts b/packages/expo/src/hooks/useSSO.ts index b1751903631..a70902fbd7c 100644 --- a/packages/expo/src/hooks/useSSO.ts +++ b/packages/expo/src/hooks/useSSO.ts @@ -79,6 +79,8 @@ export function useSSO(useSSOParams: UseSSOParams) { }; } + WebBrowser.maybeCompleteAuthSession(); + const params = new URL(authSessionResult.url).searchParams; const rotatingTokenNonce = params.get('rotating_token_nonce'); if (!rotatingTokenNonce) { From e104927d756d5c5af04c8b9e6c3d16db17379c35 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Tue, 14 Jan 2025 18:30:55 -0300 Subject: [PATCH 07/14] fix: Create first factor for enterprise_sso strategy --- packages/expo/src/hooks/useSSO.ts | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/packages/expo/src/hooks/useSSO.ts b/packages/expo/src/hooks/useSSO.ts index a70902fbd7c..236bbaaece1 100644 --- a/packages/expo/src/hooks/useSSO.ts +++ b/packages/expo/src/hooks/useSSO.ts @@ -52,11 +52,22 @@ export function useSSO(useSSOParams: UseSSOParams) { path: 'sso-native-callback', }); - await signIn.create({ - strategy: useSSOParams.strategy, - redirectUrl, - identifier: startSSOFlowParams?.identifier, - }); + if (useSSOParams.strategy === 'enterprise_sso') { + await signIn.create({ + identifier: startSSOFlowParams?.identifier as string, + }); + + await signIn.prepareFirstFactor({ + strategy: useSSOParams.strategy, + redirectUrl, + actionCompleteRedirectUrl: redirectUrl, + }); + } else { + await signIn.create({ + strategy: useSSOParams.strategy, + redirectUrl, + }); + } const { externalVerificationRedirectURL } = signIn.firstFactorVerification; if (!externalVerificationRedirectURL) { From 05c9b823010e2358208ba9788849d476e1aa8340 Mon Sep 17 00:00:00 2001 From: Nicolas Lopes Date: Tue, 14 Jan 2025 20:04:36 -0300 Subject: [PATCH 08/14] debug --- packages/expo/src/hooks/useSSO.ts | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/packages/expo/src/hooks/useSSO.ts b/packages/expo/src/hooks/useSSO.ts index 236bbaaece1..adb0c7465ba 100644 --- a/packages/expo/src/hooks/useSSO.ts +++ b/packages/expo/src/hooks/useSSO.ts @@ -45,23 +45,21 @@ export function useSSO(useSSOParams: UseSSOParams) { let createdSessionId = signIn.createdSessionId; - const redirectUrl = - startSSOFlowParams?.redirectUrl || - useSSOParams.redirectUrl || - AuthSession.makeRedirectUri({ - path: 'sso-native-callback', - }); + const redirectUrl = AuthSession.makeRedirectUri({ + path: 'sso-native-callback', + }); if (useSSOParams.strategy === 'enterprise_sso') { - await signIn.create({ + let signInResource = await signIn.create({ identifier: startSSOFlowParams?.identifier as string, }); - - await signIn.prepareFirstFactor({ + console.log('POST:', signInResource); + signInResource = await signIn.prepareFirstFactor({ strategy: useSSOParams.strategy, redirectUrl, actionCompleteRedirectUrl: redirectUrl, }); + console.log('PREPARE:', signInResource); } else { await signIn.create({ strategy: useSSOParams.strategy, From 6088af12177e75c4c1a17c4df3670911b5ea1466 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Tue, 14 Jan 2025 21:06:33 -0300 Subject: [PATCH 09/14] fix: Handle transfer flow --- packages/expo/src/hooks/useSSO.ts | 44 ++++++++++--------------------- 1 file changed, 14 insertions(+), 30 deletions(-) diff --git a/packages/expo/src/hooks/useSSO.ts b/packages/expo/src/hooks/useSSO.ts index adb0c7465ba..03cac291ef6 100644 --- a/packages/expo/src/hooks/useSSO.ts +++ b/packages/expo/src/hooks/useSSO.ts @@ -33,7 +33,7 @@ export function useSSO(useSSOParams: UseSSOParams) { const { signIn, setActive, isLoaded: isSignInLoaded } = useSignIn(); const { signUp, isLoaded: isSignUpLoaded } = useSignUp(); - async function startFlow(startSSOFlowParams?: StartSSOParams): Promise { + async function startFlow(startSSOFlowParams: StartSSOParams): Promise { if (!isSignInLoaded || !isSignUpLoaded) { return { createdSessionId: null, @@ -45,27 +45,18 @@ export function useSSO(useSSOParams: UseSSOParams) { let createdSessionId = signIn.createdSessionId; - const redirectUrl = AuthSession.makeRedirectUri({ - path: 'sso-native-callback', - }); - - if (useSSOParams.strategy === 'enterprise_sso') { - let signInResource = await signIn.create({ - identifier: startSSOFlowParams?.identifier as string, - }); - console.log('POST:', signInResource); - signInResource = await signIn.prepareFirstFactor({ - strategy: useSSOParams.strategy, - redirectUrl, - actionCompleteRedirectUrl: redirectUrl, - }); - console.log('PREPARE:', signInResource); - } else { - await signIn.create({ - strategy: useSSOParams.strategy, - redirectUrl, + const redirectUrl = + startSSOFlowParams?.redirectUrl || + useSSOParams.redirectUrl || + AuthSession.makeRedirectUri({ + path: 'sso-native-callback', }); - } + + await signIn.create({ + strategy: useSSOParams.strategy, + redirectUrl, + identifier: startSSOFlowParams.identifier, + }); const { externalVerificationRedirectURL } = signIn.firstFactorVerification; if (!externalVerificationRedirectURL) { @@ -88,17 +79,10 @@ export function useSSO(useSSOParams: UseSSOParams) { }; } - WebBrowser.maybeCompleteAuthSession(); - const params = new URL(authSessionResult.url).searchParams; - const rotatingTokenNonce = params.get('rotating_token_nonce'); - if (!rotatingTokenNonce) { - return errorThrower.throw( - 'Missing rotating_token_nonce in SSO callback. This indicates an API issue - please contact support for assistance.', - ); - } - + const rotatingTokenNonce = params.get('rotating_token_nonce') ?? ''; await signIn.reload({ rotatingTokenNonce }); + if (signIn.firstFactorVerification.status === 'transferable') { await signUp.create({ transfer: true, From 55de7d925a163e3eb7b8653b857a97d098eb6fa8 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Wed, 15 Jan 2025 10:11:19 -0300 Subject: [PATCH 10/14] chore: Experiment with not providing a redirect URL --- packages/expo/src/hooks/useSSO.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/expo/src/hooks/useSSO.ts b/packages/expo/src/hooks/useSSO.ts index 03cac291ef6..67e7ccb9a59 100644 --- a/packages/expo/src/hooks/useSSO.ts +++ b/packages/expo/src/hooks/useSSO.ts @@ -65,11 +65,10 @@ export function useSSO(useSSOParams: UseSSOParams) { ); } - const authSessionResult = await WebBrowser.openAuthSessionAsync( - externalVerificationRedirectURL.toString(), - redirectUrl, - ); + const authSessionResult = await WebBrowser.openAuthSessionAsync(externalVerificationRedirectURL.toString()); if (authSessionResult.type !== 'success' || !authSessionResult.url) { + WebBrowser.dismissBrowser(); + return { authSessionResult, createdSessionId, From 8ba1692dfa382f4b58b5c35910cd6f0668404d7e Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Wed, 15 Jan 2025 10:38:58 -0300 Subject: [PATCH 11/14] chore: Experiment with manually opening deep link --- packages/expo/src/hooks/useSSO.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/packages/expo/src/hooks/useSSO.ts b/packages/expo/src/hooks/useSSO.ts index 67e7ccb9a59..d5f6a262255 100644 --- a/packages/expo/src/hooks/useSSO.ts +++ b/packages/expo/src/hooks/useSSO.ts @@ -11,7 +11,7 @@ export type UseSSOParams = { redirectUrl?: string; }; -export type StartSSOParams = { +export type StartSSOFlowParams = { identifier?: string; unsafeMetadata?: SignUpUnsafeMetadata; redirectUrl?: string; @@ -33,7 +33,7 @@ export function useSSO(useSSOParams: UseSSOParams) { const { signIn, setActive, isLoaded: isSignInLoaded } = useSignIn(); const { signUp, isLoaded: isSignUpLoaded } = useSignUp(); - async function startFlow(startSSOFlowParams: StartSSOParams): Promise { + async function startSSOFlow(startSSOFlowParams: StartSSOFlowParams = {}): Promise { if (!isSignInLoaded || !isSignUpLoaded) { return { createdSessionId: null, @@ -60,15 +60,11 @@ export function useSSO(useSSOParams: UseSSOParams) { const { externalVerificationRedirectURL } = signIn.firstFactorVerification; if (!externalVerificationRedirectURL) { - return errorThrower.throw( - 'Missing external verification redirect URL for SSO flow. This indicates an API issue - please contact support for assistance.', - ); + return errorThrower.throw('Missing external verification redirect URL for SSO flow'); } const authSessionResult = await WebBrowser.openAuthSessionAsync(externalVerificationRedirectURL.toString()); if (authSessionResult.type !== 'success' || !authSessionResult.url) { - WebBrowser.dismissBrowser(); - return { authSessionResult, createdSessionId, @@ -100,6 +96,6 @@ export function useSSO(useSSOParams: UseSSOParams) { } return { - startFlow, + startSSOFlow, }; } From 41e76d640ff6ea77dfa827ee5555ad1afb5b5ae9 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Fri, 17 Jan 2025 14:44:01 -0300 Subject: [PATCH 12/14] chore: Refactor `redirectUrl` approach to be used for URL parsing only --- packages/expo/src/hooks/useSSO.ts | 48 ++++++++++++++----------------- 1 file changed, 21 insertions(+), 27 deletions(-) diff --git a/packages/expo/src/hooks/useSSO.ts b/packages/expo/src/hooks/useSSO.ts index d5f6a262255..43ddc86e209 100644 --- a/packages/expo/src/hooks/useSSO.ts +++ b/packages/expo/src/hooks/useSSO.ts @@ -5,35 +5,30 @@ import * as WebBrowser from 'expo-web-browser'; import { errorThrower } from '../utils/errors'; -export type UseSSOParams = { - strategy: OAuthStrategy | EnterpriseSSOStrategy; - unsafeMetadata?: SignUpUnsafeMetadata; - redirectUrl?: string; -}; - export type StartSSOFlowParams = { - identifier?: string; unsafeMetadata?: SignUpUnsafeMetadata; - redirectUrl?: string; -}; +} & ( + | { + strategy: OAuthStrategy; + } + | { + strategy: EnterpriseSSOStrategy; + identifier: string; + } +); export type StartSSOFlowReturnType = { - /** - * Session ID created upon sign-in completion, or null if incomplete. - * If incomplete, use signIn or signUp for next steps like MFA. - */ createdSessionId: string | null; setActive?: SetActive; signIn?: SignInResource; signUp?: SignUpResource; - authSessionResult?: WebBrowser.WebBrowserAuthSessionResult; }; -export function useSSO(useSSOParams: UseSSOParams) { +export function useSSO() { const { signIn, setActive, isLoaded: isSignInLoaded } = useSignIn(); const { signUp, isLoaded: isSignUpLoaded } = useSignUp(); - async function startSSOFlow(startSSOFlowParams: StartSSOFlowParams = {}): Promise { + async function startSSOFlow(startSSOFlowParams: StartSSOFlowParams): Promise { if (!isSignInLoaded || !isSignUpLoaded) { return { createdSessionId: null, @@ -43,19 +38,20 @@ export function useSSO(useSSOParams: UseSSOParams) { }; } + const { strategy, unsafeMetadata } = startSSOFlowParams ?? {}; let createdSessionId = signIn.createdSessionId; - const redirectUrl = - startSSOFlowParams?.redirectUrl || - useSSOParams.redirectUrl || - AuthSession.makeRedirectUri({ - path: 'sso-native-callback', - }); + // Used to handle redirection back to the mobile application, however deep linking it not applied + // We only leverage it to extract the `rotating_token_nonce` query param + // It's up to the consumer to navigate once `createdSessionId` gets defined + const redirectUrl = AuthSession.makeRedirectUri({ + path: 'sso-callback', + }); await signIn.create({ - strategy: useSSOParams.strategy, + strategy, redirectUrl, - identifier: startSSOFlowParams.identifier, + ...(startSSOFlowParams.strategy === 'enterprise_sso' ? { identifier: startSSOFlowParams.identifier } : {}), }); const { externalVerificationRedirectURL } = signIn.firstFactorVerification; @@ -66,7 +62,6 @@ export function useSSO(useSSOParams: UseSSOParams) { const authSessionResult = await WebBrowser.openAuthSessionAsync(externalVerificationRedirectURL.toString()); if (authSessionResult.type !== 'success' || !authSessionResult.url) { return { - authSessionResult, createdSessionId, setActive, signIn, @@ -81,13 +76,12 @@ export function useSSO(useSSOParams: UseSSOParams) { if (signIn.firstFactorVerification.status === 'transferable') { await signUp.create({ transfer: true, - unsafeMetadata: startSSOFlowParams?.unsafeMetadata || useSSOParams.unsafeMetadata, + unsafeMetadata, }); createdSessionId = signUp.createdSessionId; } return { - authSessionResult, createdSessionId, setActive, signIn, From 9a43d07f8bf2480022f2354a27e5f96db15666c5 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Wed, 22 Jan 2025 10:55:11 -0300 Subject: [PATCH 13/14] Fix transfer flow --- packages/expo/src/hooks/useSSO.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/expo/src/hooks/useSSO.ts b/packages/expo/src/hooks/useSSO.ts index 43ddc86e209..8f5388faf51 100644 --- a/packages/expo/src/hooks/useSSO.ts +++ b/packages/expo/src/hooks/useSSO.ts @@ -19,6 +19,7 @@ export type StartSSOFlowParams = { export type StartSSOFlowReturnType = { createdSessionId: string | null; + authSessionResult?: WebBrowser.WebBrowserAuthSessionResult; setActive?: SetActive; signIn?: SignInResource; signUp?: SignUpResource; @@ -39,7 +40,6 @@ export function useSSO() { } const { strategy, unsafeMetadata } = startSSOFlowParams ?? {}; - let createdSessionId = signIn.createdSessionId; // Used to handle redirection back to the mobile application, however deep linking it not applied // We only leverage it to extract the `rotating_token_nonce` query param @@ -62,7 +62,7 @@ export function useSSO() { const authSessionResult = await WebBrowser.openAuthSessionAsync(externalVerificationRedirectURL.toString()); if (authSessionResult.type !== 'success' || !authSessionResult.url) { return { - createdSessionId, + createdSessionId: null, setActive, signIn, signUp, @@ -73,16 +73,16 @@ export function useSSO() { const rotatingTokenNonce = params.get('rotating_token_nonce') ?? ''; await signIn.reload({ rotatingTokenNonce }); - if (signIn.firstFactorVerification.status === 'transferable') { + const userNeedsToBeCreated = signIn.firstFactorVerification.status === 'transferable'; + if (userNeedsToBeCreated) { await signUp.create({ transfer: true, unsafeMetadata, }); - createdSessionId = signUp.createdSessionId; } return { - createdSessionId, + createdSessionId: signUp.createdSessionId ?? signIn.createdSessionId, setActive, signIn, signUp, From d79d29771bdd50283d224d73d2791a1f812b3a96 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Wed, 22 Jan 2025 16:57:08 -0300 Subject: [PATCH 14/14] Update comment regarding redirect URL --- packages/expo/src/hooks/useSSO.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/expo/src/hooks/useSSO.ts b/packages/expo/src/hooks/useSSO.ts index 8f5388faf51..d7f3226bc50 100644 --- a/packages/expo/src/hooks/useSSO.ts +++ b/packages/expo/src/hooks/useSSO.ts @@ -41,9 +41,12 @@ export function useSSO() { const { strategy, unsafeMetadata } = startSSOFlowParams ?? {}; - // Used to handle redirection back to the mobile application, however deep linking it not applied - // We only leverage it to extract the `rotating_token_nonce` query param - // It's up to the consumer to navigate once `createdSessionId` gets defined + /** + * 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', });