Skip to main content
Docs

Build a custom flow for authenticating with OAuth connections

Warning

This guide is for users who want to build a custom user interface using the Clerk API. 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 social 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

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 object using the useSignIn() hook.
  2. Starts the authentication process by calling . 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 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/page.tsx
'use client'

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

export default function OauthSignIn() {
  const { signIn } = useSignIn()

  if (!signIn) return null

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

  // Render a button for each supported OAuth provider
  // you want to add to your app. This example uses only Google.
  return (
    <div>
      <button onClick={() => signInWith('oauth_google')}>Sign in with Google</button>
    </div>
  )
}
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 OAuth flow.
  return <AuthenticateWithRedirectCallback />
}

The following example will both sign up and sign in users, eliminating the need for a separate sign-up page.

The following example:

  1. Uses the hook to access the startSSOFlow() method.
  2. Calls the startSSOFlow() method with the strategy param set to oauth_google, but you can use any of the . 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.
    • If authentication is not successful, you can handle the missing requirements, such as MFA, using the or 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 React, { useCallback, useEffect } from 'react'
import * as WebBrowser from 'expo-web-browser'
import * as AuthSession from 'expo-auth-session'
import { useSSO } from '@clerk/clerk-expo'
import { View, Button } 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
    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()

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

  const onPress = useCallback(async () => {
    try {
      // Start the authentication process by calling `startSSOFlow()`
      const { createdSessionId, setActive, signIn, signUp } = await startSSOFlow({
        strategy: 'oauth_google',
        // 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) {
              // Check for tasks and navigate to custom UI to help users resolve them
              // See https://clerk.com/docs/custom-flows/overview#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/custom-flows/error-handling
      // for more info on error handling
      console.error(JSON.stringify(err, null, 2))
    }
  }, [])

  return (
    <View>
      <Button title="Sign in with Google" onPress={onPress} />
    </View>
  )
}
OAuthView.swift
import SwiftUI
import Clerk

struct OAuthView: View {
  var body: some View {
    // Render a button for each supported OAuth provider
    // you want to add to your app. This example uses only Google.
    Button("Sign In with Google") {
      Task { await signInWithOAuth(provider: .google) }
    }
  }
}

extension OAuthView {

  func signInWithOAuth(provider: OAuthProvider) async {
    do {
      // Start the sign-in process using the selected OAuth provider.
      let result = try await SignIn.authenticateWithRedirect(strategy: .oauth(provider: provider))

      // It is common for users who are authenticating with OAuth to use
      // a sign-in button when they mean to sign-up, and vice versa.
      // Clerk will handle this transfer for you if possible.
      // Therefore, a TransferFlowResult can be either a SignIn or SignUp.

      switch result {
      case .signIn(let signIn):
        switch signIn.status {
        case .complete:
          // If sign-in process is complete, navigate the user as needed.
          dump(Clerk.shared.session)
        default:
          // If the status is not complete, check why. User may need to
          // complete further steps.
          dump(signIn.status)
        }
      case .signUp(let signUp):
        switch signUp.status {
        case .complete:
          // If sign-up process is complete, navigate the user as needed.
          dump(Clerk.shared.session)
        default:
          // If the status is not complete, check why. User may need to
          // complete further steps.
          dump(signUp.status)
        }
      }
    } catch {
      // See https://clerk.com/docs/custom-flows/error-handling
      // for more info on error handling.
      dump(error)
    }
  }
}
OAuthViewModel.kt
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.clerk.api.Clerk
import com.clerk.api.network.serialization.longErrorMessageOrNull
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.OAuthProvider
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 OAuthViewModel : 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 signInWithOAuth(provider: OAuthProvider) {
        viewModelScope.launch {
            SignIn.authenticateWithRedirect(SignIn.AuthenticateWithRedirectParams.OAuth(provider)).onSuccess {
                when(it.resultType) {
                    ResultType.SIGN_IN -> {
                        // The OAuth flow resulted in a 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 -> {
                        // The OAuth flow resulted in a 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/custom-flows/error-handling
                // for more info on error handling
                Log.e("OAuthViewModel", it.longErrorMessageOrNull, it.throwable)
            }
        }
    }

    sealed interface UiState {
        data object Loading : UiState

        data object SignedOut : UiState

        data object Authenticated : UiState
    }
}
OAuthActivity.kt
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Text
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.clerk.api.sso.OAuthProvider

class OAuthActivity : ComponentActivity() {
    val viewModel: OAuthViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            val state by viewModel.uiState.collectAsStateWithLifecycle()
            Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
                when(state) {
                    OAuthViewModel.UiState.Authenticated -> Text("Authenticated")
                    OAuthViewModel.UiState.Loading -> CircularProgressIndicator()
                    OAuthViewModel.UiState.SignedOut -> {
                        val provider = OAuthProvider.GOOGLE // Or .GITHUB, .SLACK etc.
                        Button(onClick = {
                            viewModel.signInWithOAuth(provider)
                        }) {
                            Text("Sign in with ${provider.name}")
                        }
                    }
                }
            }
        }
    }
}

Feedback

What did you think of this content?

Last updated on