Skip to main content
Docs

Build a custom flow for authenticating with enterprise connections

Warning

This guide is for users who want to build a . To use a prebuilt UI, use the Account Portal pages or prebuilt components.

Before you start

You must configure your application instance through the Clerk Dashboard for the enterprise connection(s) that you want to use. Visit the appropriate guide for your platform to learn how to configure your instance.

Create the sign-up and sign-in flow

This example is written for Next.js App Router but it can be adapted for any React-based framework, such as React Router or Tanstack React Start.

The following example will both sign up and sign in users, eliminating the need for a separate sign-up page. However, if you want to have separate sign-up and sign-in pages, the sign-up and sign-in flows are equivalent, meaning that all you have to do is swap out the SignIn object for the SignUp object using the useSignUp() hook.

The following example:

  1. Accesses the SignIn object using the useSignIn() hook.
  2. Starts the authentication process by calling SignIn.authenticateWithRedirect(params). This method requires a redirectUrl param, which is the URL that the browser will be redirected to once the user authenticates with the identity provider.
  3. Creates a route at the URL that the redirectUrl param points to. The following example names this route /sso-callback. This route should either render the prebuilt <AuthenticateWithRedirectCallback/> component or call the Clerk.handleRedirectCallback() method if you're not using the prebuilt component.

The following example shows two files:

  1. The sign-in page where the user can start the authentication flow.
  2. The SSO callback page where the flow is completed.
app/sign-in/[[...sign-in]]/page.tsx
'use client'

import * as React from 'react'
import { useSignIn } from '@clerk/nextjs'

export default function Page() {
  const { signIn, isLoaded } = useSignIn()

  const signInWithEnterpriseSSO = (e: React.FormEvent) => {
    e.preventDefault()

    if (!isLoaded) return null

    const email = (e.target as HTMLFormElement).email.value

    signIn
      .authenticateWithRedirect({
        identifier: email,
        strategy: 'enterprise_sso',
        redirectUrl: '/sign-in/sso-callback',
        redirectUrlComplete: '/',
      })
      .then((res) => {
        console.log(res)
      })
      .catch((err: any) => {
        // See https://clerk.com/docs/guides/development/custom-flows/error-handling
        // for more info on error handling
        console.log(err.errors)
        console.error(err, null, 2)
      })
  }

  return (
    <form onSubmit={(e) => signInWithEnterpriseSSO(e)}>
      <input id="email" type="email" name="email" placeholder="Enter email address" />
      <button>Sign in with Enterprise SSO</button>
    </form>
  )
}
app/sign-in/sso-callback/page.tsx
import { AuthenticateWithRedirectCallback } from '@clerk/nextjs'

export default function Page() {
  // Handle the redirect flow by calling the Clerk.handleRedirectCallback() method
  // or rendering the prebuilt <AuthenticateWithRedirectCallback/> component.
  // This is the final step in the custom Enterprise SSO flow.
  return <AuthenticateWithRedirectCallback />
}

To handle both sign-up and sign-in without a separate sign-up page, the following example:

  1. Uses the useSSO()Expo Icon hook to access the startSSOFlow() method.
  2. Calls the startSSOFlow() method with the strategy param set to enterprise_sso and the identifier param set to the user's email address that they provided. The optional redirect_url param is also set in order to redirect the user once they finish the authentication flow.
    • If authentication is successful, the setActive() method is called to set the active session with the new createdSessionId. You may need to check for that are required for the user to complete after signing up.
    • If authentication is not successful, you can handle the missing requirements, such as MFA, using the signIn or signUp object returned from startSSOFlow(), depending on if the user is signing in or signing up. These objects include properties, like status, that can be used to determine the next steps. See the respective linked references for more information.
app/(auth)/sign-in.tsx
import { ThemedText } from '@/components/themed-text'
import { ThemedView } from '@/components/themed-view'
import { useSSO } from '@clerk/clerk-expo'
import * as AuthSession from 'expo-auth-session'
import { useRouter } from 'expo-router'
import * as WebBrowser from 'expo-web-browser'
import React, { useEffect, useState } from 'react'
import { Platform, Pressable, StyleSheet, TextInput } from 'react-native'

export const useWarmUpBrowser = () => {
  useEffect(() => {
    // Preloads the browser for Android devices to reduce authentication load time
    // See: https://docs.expo.dev/guides/authentication/#improving-user-experience
    if (Platform.OS !== 'android') return
    void WebBrowser.warmUpAsync()
    return () => {
      // Cleanup: closes browser when component unmounts
      void WebBrowser.coolDownAsync()
    }
  }, [])
}

// Handle any pending authentication sessions
WebBrowser.maybeCompleteAuthSession()

export default function Page() {
  useWarmUpBrowser()

  const router = useRouter()
  const [email, setEmail] = useState('')

  // Use the `useSSO()` hook to access the `startSSOFlow()` method
  const { startSSOFlow } = useSSO()

  const onPress = async () => {
    try {
      // Start the authentication process by calling `startSSOFlow()`
      const { createdSessionId, setActive, signIn, signUp } = await startSSOFlow({
        strategy: 'enterprise_sso',
        identifier: email,
        // For web, defaults to current path
        // For native, you must pass a scheme, like AuthSession.makeRedirectUri({ scheme, path })
        // For more info, see https://docs.expo.dev/versions/latest/sdk/auth-session/#authsessionmakeredirecturioptions
        redirectUrl: AuthSession.makeRedirectUri(),
      })

      // If sign in was successful, set the active session
      if (createdSessionId) {
        setActive!({
          session: createdSessionId,
          navigate: async ({ session }) => {
            if (session?.currentTask) {
              // Handle pending session tasks
              // See https://clerk.com/docs/guides/development/custom-flows/authentication/session-tasks
              console.log(session?.currentTask)
              return
            }

            router.push('/')
          },
        })
      } else {
        // If there is no `createdSessionId`,
        // there are missing requirements, such as MFA
        // Use the `signIn` or `signUp` returned from `startSSOFlow`
        // to handle next steps
      }
    } catch (err) {
      // See https://clerk.com/docs/guides/development/custom-flows/error-handling
      // for more info on error handling
      console.error(JSON.stringify(err, null, 2))
    }
  }

  return (
    <ThemedView style={styles.container}>
      <ThemedText type="title" style={styles.title}>
        Sign in with SAML
      </ThemedText>
      <ThemedText style={styles.label}>Email address</ThemedText>
      <TextInput
        style={styles.input}
        value={email}
        onChangeText={setEmail}
        placeholder="Enter email"
        placeholderTextColor="#666666"
        autoCapitalize="none"
        keyboardType="email-address"
      />
      <Pressable
        style={({ pressed }) => [
          styles.button,
          !email && styles.buttonDisabled,
          pressed && styles.buttonPressed,
        ]}
        onPress={onPress}
        disabled={!email}
      >
        <ThemedText style={styles.buttonText}>Sign in with SAML</ThemedText>
      </Pressable>
    </ThemedView>
  )
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    padding: 20,
    gap: 12,
  },
  title: {
    marginBottom: 8,
  },
  label: {
    fontWeight: '600',
    fontSize: 14,
  },
  input: {
    borderWidth: 1,
    borderColor: '#ccc',
    borderRadius: 8,
    padding: 12,
    fontSize: 16,
    backgroundColor: '#fff',
  },
  button: {
    backgroundColor: '#0a7ea4',
    paddingVertical: 12,
    paddingHorizontal: 24,
    borderRadius: 8,
    alignItems: 'center',
    marginTop: 8,
  },
  buttonPressed: {
    opacity: 0.7,
  },
  buttonDisabled: {
    opacity: 0.5,
  },
  buttonText: {
    color: '#fff',
    fontWeight: '600',
  },
})
EnterpriseSSOView.swift
    import SwiftUI
    import ClerkKit

    struct EnterpriseSSOView: View {
      @Environment(Clerk.self) private var clerk
      @State private var email = ""

      var body: some View {
        TextField("Enter email", text: $email)
        Button("Sign in with Enterprise SSO") {
          Task { await signInWithEnterpriseSSO(email: email) }
        }
      }
    }

    extension EnterpriseSSOView {

      func signInWithEnterpriseSSO(email: String) async {
        do {
          let result = try await clerk.auth.signInWithEnterpriseSSO(emailAddress: email)

          // Enterprise SSO can complete as either a sign-in or sign-up
          // Clerk returns a TransferFlowResult to cover both cases
          switch result {
          case .signIn(let signIn):
            switch signIn.status {
            case .complete:
              dump(clerk.session)
            default:
              dump(signIn.status)
            }
          case .signUp(let signUp):
            switch signUp.status {
            case .complete:
              dump(clerk.session)
            default:
              dump(signUp.status)
            }
          }
        } catch {
          // See https://clerk.com/docs/guides/development/custom-flows/error-handling
          // for more info on error handling
          dump(error)
        }
      }
    }
EnterpriseSSOView.kt
    import android.util.Log
    import androidx.lifecycle.ViewModel
    import androidx.lifecycle.viewModelScope
    import com.clerk.api.Clerk
    import com.clerk.api.network.serialization.errorMessage
    import com.clerk.api.network.serialization.onFailure
    import com.clerk.api.network.serialization.onSuccess
    import com.clerk.api.signin.SignIn
    import com.clerk.api.signup.SignUp
    import com.clerk.api.sso.ResultType
    import kotlinx.coroutines.flow.MutableStateFlow
    import kotlinx.coroutines.flow.asStateFlow
    import kotlinx.coroutines.flow.combine
    import kotlinx.coroutines.flow.launchIn
    import kotlinx.coroutines.launch

    class EnterpriseSSOViewModel : ViewModel() {
        private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
        val uiState = _uiState.asStateFlow()

        init {
            combine(Clerk.isInitialized, Clerk.userFlow) { isInitialized, user ->
                _uiState.value = when {
                    !isInitialized -> UiState.Loading
                    user != null -> UiState.Authenticated
                    else -> UiState.SignedOut
                }
            }.launchIn(viewModelScope)
        }

        fun signInWithEnterpriseSSO(email: String) {
            viewModelScope.launch {
                Clerk.auth.signInWithEnterpriseSso { this.email = email }.onSuccess {
                    // Enterprise SSO can complete as either a sign-in or sign-up.
                    when (it.resultType) {
                        ResultType.SIGN_IN -> {
                            if (it.signIn?.status == SignIn.Status.COMPLETE) {
                                _uiState.value = UiState.Authenticated
                            } else {
                                // If the status is not complete, check why. User may need to
                                // complete further steps.
                            }
                        }
                        ResultType.SIGN_UP -> {
                            if (it.signUp?.status == SignUp.Status.COMPLETE) {
                                _uiState.value = UiState.Authenticated
                            } else {
                                // If the status is not complete, check why. User may need to
                                // complete further steps.
                            }
                        }
                    }
                }.onFailure {
                    // See https://clerk.com/docs/guides/development/custom-flows/error-handling
                    // for more info on error handling
                    Log.e("EnterpriseSSO", it.errorMessage, it.throwable)
                }
            }
        }

        sealed interface UiState {
            data object Loading : UiState

            data object SignedOut : UiState

            data object Authenticated : UiState
        }
    }
EnterpriseSSOActivity.kt
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle

class EnterpriseSSOActivity : ComponentActivity() {
    private val viewModel: EnterpriseSSOViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            val state by viewModel.uiState.collectAsStateWithLifecycle()
            var email by remember { mutableStateOf("") }

            Column(
                modifier = Modifier.fillMaxSize().padding(24.dp),
                verticalArrangement = Arrangement.Center,
                horizontalAlignment = Alignment.CenterHorizontally
            ) {
                when (state) {
                    EnterpriseSSOViewModel.UiState.Authenticated -> Text("Authenticated")
                    EnterpriseSSOViewModel.UiState.Loading -> CircularProgressIndicator()
                    EnterpriseSSOViewModel.UiState.SignedOut -> {
                        TextField(
                            value = email,
                            onValueChange = { email = it },
                            label = { Text("Work email") }
                        )
                        Button(
                            modifier = Modifier.padding(top = 12.dp),
                            enabled = email.isNotBlank(),
                            onClick = { viewModel.signInWithEnterpriseSSO(email) }
                        ) {
                            Text("Sign in with Enterprise SSO")
                        }
                    }
                }
            }
        }
    }
}

Examples for this SDK aren't available yet. For now, try switching to a supported SDK, such as Next.js, and converting the code to fit your SDK.

Feedback

What did you think of this content?

Last updated on