Build a custom flow for authenticating with OAuth connections
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
First, in your .env file, set the CLERK_SIGN_IN_URL environment variable to tell Clerk where the sign-in page is being hosted. Otherwise, your app may default to using the Account Portal sign-in page instead. This guide uses the /sign-in route.
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-inThe 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
SignInobject using the useSignIn() hook. - Starts the authentication process by calling
SignIn.authenticateWithRedirect(params). This method requires aredirectUrlparam, which is the URL that the browser will be redirected to once the user authenticates with the identity provider. - Creates a route at the URL that the
redirectUrlparam points to. The following example names this route/sso-callback. This route should either render the prebuilt <AuthenticateWithRedirectCallback/> component or call theClerk.handleRedirectCallback()method if you're not using the prebuilt component.
The following example shows two files:
- The sign-in page where the user can start the authentication flow.
- The SSO callback page where the flow is completed.
'use client'
import * as React from 'react'
import { OAuthStrategy } from '@clerk/shared/types'
import { useSignIn } from '@clerk/nextjs/legacy'
export default function Page() {
const { signIn } = useSignIn()
if (!signIn) return null
const signInWith = (strategy: OAuthStrategy) => {
return signIn
.authenticateWithRedirect({
strategy,
redirectUrl: '/sign-in/sso-callback',
redirectUrlComplete: '/sign-in/tasks', // Learn more about session tasks at https://clerk.com/docs/guides/development/custom-flows/authentication/session-tasks
})
.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)
})
}
// 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>
)
}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.
return (
<>
<AuthenticateWithRedirectCallback />
{/* Required for sign-up flows
Clerk's bot sign-up protection is enabled by default */}
<div id="clerk-captcha" />
</>
)
}To handle both sign-up and sign-in without a separate sign-up page, the following example:
- Uses the useSSO() hook to access the
startSSOFlow()method. - Calls the
startSSOFlow()method with thestrategyparam set tooauth_google, but you can use any of the supported OAuth strategies. The optionalredirect_urlparam 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 newcreatedSessionId. - If authentication is not successful, you can handle the missing requirements, such as MFA, using the
signInorsignUpobject returned fromstartSSOFlow(), 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 respective linked references for more information.
- If authentication is successful, the
import { ThemedText } from '@/components/themed-text'
import { ThemedView } from '@/components/themed-view'
import { useSSO } from '@clerk/expo'
import * as AuthSession from 'expo-auth-session'
import { useRouter } from 'expo-router'
import * as WebBrowser from 'expo-web-browser'
import React, { useCallback, useEffect } from 'react'
import { Platform, Pressable, StyleSheet } from 'react-native'
// Preloads the browser for Android devices to reduce authentication load time
// See: https://docs.expo.dev/guides/authentication/#improving-user-experience
export const useWarmUpBrowser = () => {
useEffect(() => {
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()
// 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,
// Handle pending session tasks
// See https://clerk.com/docs/guides/development/custom-flows/authentication/session-tasks
navigate: async ({ session, decorateUrl }) => {
if (session?.currentTask) {
console.log(session?.currentTask)
return
}
const url = decorateUrl('/')
if (url.startsWith('http')) {
window.location.href = url
} else {
router.push(url)
}
},
})
} else {
// If there is no `createdSessionId`,
// there are missing requirements, such as MFA
// See https://clerk.com/docs/guides/development/custom-flows/authentication/oauth-connections#handle-missing-requirements
}
} 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
</ThemedText>
<ThemedText style={styles.description}>
Sign in with your Google account to continue
</ThemedText>
<Pressable
style={({ pressed }) => [styles.button, pressed && styles.buttonPressed]}
onPress={onPress}
>
<ThemedText style={styles.buttonText}>Sign in with Google</ThemedText>
</Pressable>
</ThemedView>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
gap: 12,
},
title: {
marginBottom: 8,
},
description: {
fontSize: 14,
marginBottom: 16,
opacity: 0.8,
},
button: {
backgroundColor: '#0a7ea4',
paddingVertical: 12,
paddingHorizontal: 24,
borderRadius: 8,
alignItems: 'center',
marginTop: 8,
},
buttonPressed: {
opacity: 0.7,
},
buttonText: {
color: '#fff',
fontWeight: '600',
},
})Handle missing requirements
Depending on your instance settings, users might need to provide extra information before their sign-up can be completed, such as when a username or accepting legal terms is required. In these cases, the SignUp object returns a status of "missing_requirements" along with a missingFields array. You can create a "Continue" page to collect these missing fields and complete the sign-up flow. Handling the missing requirements will depend on your instance settings. For example, if your instance settings require a phone number, you will need to handle verifying the phone number.
'use client'
import { useState } from 'react'
import { useSignUp } from '@clerk/nextjs/legacy'
import { useRouter } from 'next/navigation'
export default function Page() {
const router = useRouter()
// Use `useSignUp()` hook to access the `SignUp` object
// `missing_requirements` and `missingFields` are only available on the `SignUp` object
const { isLoaded, signUp, setActive } = useSignUp()
const [formData, setFormData] = useState<Record<string, string>>({})
if (!isLoaded) return <div>Loading…</div>
// Protect the page from users who are not in the sign-up flow
// such as users who visited this route directly
if (!signUp.id) router.push('/sign-in')
const status = signUp?.status
const missingFields = signUp?.missingFields ?? []
const handleChange = (field: string, value: string) => {
setFormData((prev) => ({ ...prev, [field]: value }))
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
try {
// Update the `SignUp` object with the missing fields
// The logic that goes here will depend on your instance settings
// E.g. if your app requires a phone number, you will need to collect and verify it here
const res = await signUp?.update(formData)
if (res?.status === 'complete') {
await setActive({
session: res.createdSessionId,
navigate: async ({ 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)
}
},
})
}
} 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))
}
}
if (status === 'missing_requirements') {
// For simplicity, all missing fields in this example are text inputs.
// In a real app, you might want to handle them differently:
// - legal_accepted: checkbox
// - username: text with validation
// - phone_number: phone input, etc.
return (
<div>
<h1>Continue sign-up</h1>
<form onSubmit={handleSubmit}>
{missingFields.map((field) => (
<div key={field}>
<label>
{field}:
<input
type="text"
value={formData[field] || ''}
onChange={(e) => handleChange(field, e.target.value)}
/>
</label>
</div>
))}
{/* Required for sign-up flows
Clerk's bot sign-up protection is enabled by default */}
<div id="clerk-captcha" />
<button type="submit">Submit</button>
</form>
</div>
)
}
// Handle other statuses if needed
return (
<>
{/* Required for sign-up flows
Clerk's bot sign-up protection is enabled by default */}
<div id="clerk-captcha" />
</>
)
}import { AuthenticateWithRedirectCallback } from '@clerk/nextjs'
export default function Page() {
// Set the `continueSignUpUrl` to the route of your "Continue" page
// Once a user authenticates with the OAuth provider, they will be redirected to that route
return (
<>
<AuthenticateWithRedirectCallback continueSignUpUrl="/sign-in/continue" />
{/* Required for sign-up flows
Clerk's bot sign-up protection is enabled by default */}
<div id="clerk-captcha" />
</>
)
}import { ThemedText } from '@/components/themed-text'
import { ThemedView } from '@/components/themed-view'
import { useSignUp } from '@clerk/expo/legacy'
import { useRouter } from 'expo-router'
import { useState } from 'react'
import { Pressable, StyleSheet, TextInput, View } from 'react-native'
export default function Page() {
const router = useRouter()
// Use `useSignUp()` hook to access the `SignUp` object
// `missing_requirements` and `missingFields` are only available on the `SignUp` object
const { isLoaded, signUp, setActive } = useSignUp()
const [formData, setFormData] = useState<Record<string, string>>({})
if (!isLoaded)
return (
<ThemedView style={styles.container}>
<ThemedText>Loading…</ThemedText>
</ThemedView>
)
// Protect the page from users who are not in the sign-up flow
// such as users who visited this route directly
if (!signUp.id) router.push('/sign-in')
const status = signUp?.status
const missingFields = signUp?.missingFields ?? []
const handleChange = (field: string, value: string) => {
setFormData((prev) => ({ ...prev, [field]: value }))
}
const handleSubmit = async () => {
try {
// Update the `SignUp` object with the missing fields
// The logic that goes here will depend on your instance settings
// E.g. if your app requires a phone number, you will need to collect and verify it here
console.log(formData)
const res = await signUp?.update(formData)
if (res?.status === 'complete') {
await setActive({
session: res.createdSessionId,
navigate: async ({ 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)
}
},
})
}
} 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))
}
}
if (status === 'missing_requirements') {
// For simplicity, all missing fields in this example are text inputs.
// In a real app, you might want to handle them differently:
// - legal_accepted: checkbox
// - username: text with validation
// - phone_number: phone input, etc.
return (
<ThemedView style={styles.container}>
<ThemedText type="title" style={styles.title}>
Continue sign-up
</ThemedText>
<ThemedText style={styles.description}>
Please complete the required fields to continue
</ThemedText>
{missingFields.map((field) => (
<View key={field} style={styles.fieldContainer}>
<ThemedText style={styles.label}>{field.replace(/_/g, ' ')}</ThemedText>
<TextInput
style={styles.input}
value={formData[field] || ''}
onChangeText={(value) => handleChange(field, value)}
placeholder={`Enter ${field.replace(/_/g, ' ')}`}
placeholderTextColor="#666666"
/>
</View>
))}
{/* Required for sign-up flows
Clerk's bot sign-up protection is enabled by default */}
<View id="clerk-captcha" />
<Pressable
style={({ pressed }) => [styles.button, pressed && styles.buttonPressed]}
onPress={handleSubmit}
>
<ThemedText style={styles.buttonText}>Submit</ThemedText>
</Pressable>
</ThemedView>
)
}
// Handle other statuses if needed
return (
<ThemedView style={styles.container}>
{/* Required for sign-up flows
Clerk's bot sign-up protection is enabled by default */}
<View id="clerk-captcha" />
</ThemedView>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
gap: 12,
},
title: {
marginBottom: 8,
},
description: {
fontSize: 14,
marginBottom: 16,
opacity: 0.8,
},
fieldContainer: {
gap: 4,
},
label: {
fontWeight: '600',
fontSize: 14,
textTransform: 'capitalize',
},
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,
},
buttonText: {
color: '#fff',
fontWeight: '600',
},
})Feedback
Last updated on