Build a custom flow for authenticating with enterprise connections
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.
Build the custom flow
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:
- Accesses the SignIn object using the useSignIn() hook.
- Starts the authentication process by calling SignIn.sso(params). This method requires the following params:
redirectUrl: The URL that the browser will be redirected to once the user authenticates with the identity provider if no additional requirements are needed, and a session has been created.redirectCallbackUrl: The URL that the browser will be redirected to once the user authenticates with the identity provider if additional requirements are needed.
- Creates a route at the URL that the
redirectCallbackUrlparam points to. The following example re-uses the/sign-inroute, which should be written to handle when a sign-in attempt is in a non-complete status such asneeds_second_factor.
'use client'
import * as React from 'react'
import { useSignIn } from '@clerk/nextjs'
export default function Page() {
const { signIn, errors, fetchStatus } = useSignIn()
const signInWithEnterpriseSSO = async (formData: FormData) => {
const email = formData.get('email') as string
const { error } = await signIn.sso({
identifier: email,
strategy: 'enterprise_sso',
// The URL that the user will be redirected to if additional requirements are needed
redirectCallbackUrl: '/sign-in',
redirectUrl: '/sign-in/tasks', // Learn more about session tasks at https://clerk.com/docs/guides/development/custom-flows/authentication/session-tasks
})
if (error) {
// See https://clerk.com/docs/guides/development/custom-flows/error-handling
// for more info on error handling
console.error(JSON.stringify(error, null, 2))
return
}
if (signIn.status === 'needs_second_factor') {
// See https://clerk.com/docs/guides/development/custom-flows/authentication/multi-factor-authentication
} else if (signIn.status === 'needs_client_trust') {
// See https://clerk.com/docs/guides/development/custom-flows/authentication/client-trust
} else {
// Check why the sign-in is not complete
console.error('Sign-in attempt not complete:', signIn)
}
}
return (
<>
<form action={signInWithEnterpriseSSO}>
<input id="email" type="email" name="email" placeholder="Enter email address" />
<button type="submit" disabled={fetchStatus === 'fetching'}>
Sign in with Enterprise SSO
</button>
</form>
{/* For your debugging purposes. You can just console.log errors, but we put them in the UI for convenience */}
{errors && <p>{JSON.stringify(errors, null, 2)}</p>}
</>
)
}The following example will both sign up and sign in users, eliminating the need for a separate sign-up page.
The following example:
- Uses the useSSO() hook to access the
startSSOFlow()method. - Calls the
startSSOFlow()method with thestrategyparam set toenterprise_ssoand theidentifierparam set to the user's email address that they provided. The optionalredirect_urlparam is also set in order to redirect the user once they finish the authentication flow. The redirect URL must be registered in the Clerk Dashboard under Redirect URLs. Without this, the Enterprise SSO provider will complete authentication but the session will not be created.- If authentication is successful, the
setActive()method is called to set the active session with the newcreatedSessionId. 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, likestatus, that can be used to determine the next steps. See the section on handling missing requirements for more information.
- If authentication is successful, the
import * as React from 'react'
import * as AuthSession from 'expo-auth-session'
import * as WebBrowser from 'expo-web-browser'
import { useSSO } from '@clerk/expo'
import { ThemedText } from '@/components/themed-text'
import { ThemedView } from '@/components/themed-view'
import { type Href, useRouter } from 'expo-router'
import { Platform, Pressable, StyleSheet, TextInput } from 'react-native'
export const useWarmUpBrowser = () => {
React.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 () => {
void WebBrowser.coolDownAsync()
}
}, [])
}
// Handle any pending authentication sessions
WebBrowser.maybeCompleteAuthSession()
export default function Page() {
useWarmUpBrowser()
const router = useRouter()
const [email, setEmail] = React.useState('')
const [isSubmitting, setIsSubmitting] = React.useState(false)
// Use the `useSSO()` hook to access the `startSSOFlow()` method
const { startSSOFlow } = useSSO()
const onPress = async () => {
setIsSubmitting(true)
try {
// Start the authentication process by calling `startSSOFlow()`
const { createdSessionId, setActive } = 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: ({ session, decorateUrl }) => {
if (session?.currentTask) {
// Handle pending session tasks
// See https://clerk.com/docs/guides/development/custom-flows/authentication/session-tasks
console.log(session?.currentTask)
return
}
const url = decorateUrl('/')
if (url.startsWith('http')) {
window.location.href = url
} else {
router.push(url as Href)
}
},
})
} else {
// If the session was not created, navigate to the continue page to collect missing information
// See https://clerk.com/docs/guides/development/custom-flows/authentication/oauth-connections#handle-missing-requirements
router.push('/continue')
}
} 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))
} finally {
setIsSubmitting(false)
}
}
const canSubmit = email.trim().length > 0 && !isSubmitting
return (
<ThemedView style={styles.container}>
<ThemedText type="title" style={styles.title}>
Sign in
</ThemedText>
<ThemedText style={styles.subtitle}>Enterprise SSO (SAML)</ThemedText>
<ThemedText style={styles.label}>Email</ThemedText>
<TextInput
style={styles.input}
value={email}
placeholder="name@company.com"
placeholderTextColor="#666666"
onChangeText={setEmail}
keyboardType="email-address"
autoCapitalize="none"
autoCorrect={false}
autoComplete="email"
/>
<Pressable
style={({ pressed }) => [
styles.button,
!canSubmit && styles.buttonDisabled,
pressed && styles.buttonPressed,
]}
onPress={onPress}
disabled={!canSubmit}
>
<ThemedText style={styles.buttonText}>{isSubmitting ? 'Opening…' : 'Sign in'}</ThemedText>
</Pressable>
</ThemedView>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
gap: 12,
},
title: {
marginBottom: 8,
},
subtitle: {
fontSize: 14,
marginBottom: 8,
opacity: 0.85,
},
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',
},
linkContainer: {
flexDirection: 'row',
gap: 4,
marginTop: 12,
alignItems: 'center',
},
})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)
}
}
}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
}
}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")
}
}
}
}
}
}
}Feedback
Last updated on