Listen to this story
senior software engineer and seasonal founder
Walkthroughs, tutorials, guides, and tips. This story will teach you how to do something new or how to do something better.
The writer is smart, but don't just like, take their word for it. #DoYourOwnResearch before making any investment decisions or decisions regarding your health or security. (Do not regard any of this content as professional investment advice, or health advice)
The code in this story is for educational purposes. The readers are solely responsible for whatever they build with it.
Implementing a robust authentication system is crucial for mobile applications. In this guide, I'll walk you through setting up Auth0 authentication with Expo Router, creating a seamless and secure user experience.
Before starting, ensure you have:
First, install the Auth0 React Native SDK:
yarn add react-native-auth0
Create an auth0.config.js
file in your project root:
const config = {
clientId: "YOUR_AUTH0_CLIENT_ID",
domain: "YOUR_AUTH0_DOMAIN",
}
export default config
Replace the placeholders with your actual Auth0 credentials.
The authentication context will manage the auth state throughout your app. Create a file called useAuth.tsx
in your hooks directory:
import { createContext, useContext, useEffect, useState } from "react"
import { useAuth0 } from "react-native-auth0"
import { router, useSegments, useRootNavigationState } from "expo-router"
// Define the shape of our auth context
type AuthContextType = {
signIn: () => Promise<void>
signOut: () => Promise<void>
isAuthenticated: boolean
isLoading: boolean
user: any
error: Error | null
}
// Create the context with a default value
const AuthContext = createContext<AuthContextType | null>(null)
// Provider component that wraps the app
export function AuthProvider({ children }: { children: React.ReactNode }) {
const { authorize, clearSession, user, error, getCredentials, isLoading } =
useAuth0()
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(false)
const segments = useSegments()
const navigationState = useRootNavigationState()
// Check if the user is authenticated and redirect accordingly
useEffect(() => {
if (!navigationState?.key) return
const inAuthGroup = segments[0] === "(auth)"
if (isAuthenticated && inAuthGroup) {
// Redirect authenticated users from auth screens to the main app
router.replace("/(tabs)")
} else if (!isAuthenticated && !inAuthGroup) {
// Redirect unauthenticated users to the login screen
router.replace("/(auth)/login")
}
}, [isAuthenticated, segments, navigationState?.key])
// Update authentication state when user changes
useEffect(() => {
setIsAuthenticated(!!user)
}, [user])
// Sign in function
const signIn = async () => {
try {
await authorize()
const credentials = await getCredentials()
console.log("Auth credentials:", credentials)
setIsAuthenticated(true)
} catch (e) {
console.error("Login error:", e)
}
}
// Sign out function
const signOut = async () => {
try {
await clearSession()
setIsAuthenticated(false)
} catch (e) {
console.error("Logout error:", e)
}
}
return (
<AuthContext.Provider
value={{
signIn,
signOut,
isAuthenticated,
isLoading,
user,
error,
}}
>
{children}
</AuthContext.Provider>
)
}
// Custom hook to use the auth context
export function useAuth() {
const context = useContext(AuthContext)
if (!context) {
throw new Error("useAuth must be used within an AuthProvider")
}
return context
}
This context provides:
Update your app/_layout.tsx
file to include the Auth0Provider and AuthProvider:
import { Auth0Provider } from "react-native-auth0"
import config from "@/auth0.config"
import { AuthProvider } from "@/hooks/useAuth"
// Other imports...
export default function RootLayout() {
// Other code...
return (
<Auth0Provider domain={config.domain} clientId={config.clientId}>
<AuthProvider>
<ThemeProvider value={colorScheme === "dark" ? DarkTheme : DefaultTheme}>
<Stack>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen name="(auth)" options={{ headerShown: false }} />
<Stack.Screen name="+not-found" />
</Stack>
<StatusBar style={colorScheme === "dark" ? "light" : "dark"} />
</ThemeProvider>
</AuthProvider>
</Auth0Provider>
)
}
Expo Router uses directory-based routing. Create an (auth)
directory in your app folder with a layout file:
// app/(auth)/_layout.tsx
import { Stack } from "expo-router"
export default function AuthLayout() {
return (
<Stack screenOptions={{ headerShown: false }}>
<Stack.Screen name="login" />
</Stack>
)
}
Create a login screen in app/(auth)/login.tsx
:
import { ThemedText } from "@/components/ThemedText"
import { useAuth } from "@/hooks/useAuth"
import {
StyleSheet,
View,
TouchableOpacity,
ActivityIndicator,
} from "react-native"
export default function LoginScreen() {
const { signIn, isLoading, error } = useAuth()
return (
<View style={styles.container}>
<View style={styles.content}>
<ThemedText style={styles.title}>Welcome to Your App</ThemedText>
<ThemedText style={styles.subtitle}>Sign in to continue</ThemedText>
<TouchableOpacity
style={styles.button}
onPress={signIn}
disabled={isLoading}
>
{isLoading ? (
<ActivityIndicator color="#fff" />
) : (
<ThemedText style={styles.buttonText}>Sign In</ThemedText>
)}
</TouchableOpacity>
{error && (
<ThemedText style={styles.errorText}>{error.message}</ThemedText>
)}
</View>
</View>
)
}
// Styles...
Add a profile screen to display user information and provide a logout option:
// app/(tabs)/profile.tsx
import { ThemedText } from "@/components/ThemedText"
import { useAuth } from "@/hooks/useAuth"
import {
StyleSheet,
View,
TouchableOpacity,
Image,
ScrollView,
} from "react-native"
export default function ProfileScreen() {
const { user, signOut, isLoading } = useAuth()
return (
<ScrollView style={styles.container}>
<View style={styles.header}>
{user?.picture ? (
<Image source={{ uri: user.picture }} style={styles.avatar} />
) : (
<View style={styles.avatarPlaceholder}>
<ThemedText style={styles.avatarText}>
{user?.name?.charAt(0) || user?.email?.charAt(0) || "?"}
</ThemedText>
</View>
)}
<ThemedText style={styles.name}>{user?.name || "User"}</ThemedText>
<ThemedText style={styles.email}>{user?.email || ""}</ThemedText>
</View>
{/* User information display */}
<View style={styles.actions}>
<TouchableOpacity
style={styles.logoutButton}
onPress={signOut}
disabled={isLoading}
>
<ThemedText style={styles.logoutText}>Sign Out</ThemedText>
</TouchableOpacity>
</View>
</ScrollView>
)
}
// Styles...
Ensure your tabs layout includes the profile tab and checks authentication:
// app/(tabs)/_layout.tsx
import { useAuth } from "@/hooks/useAuth"
// Other imports...
export default function TabLayout() {
const { isAuthenticated } = useAuth()
// Redirect to login if not authenticated
React.useEffect(() => {
if (!isAuthenticated) {
// The AuthProvider will handle the redirect
}
}, [isAuthenticated])
return (
<Tabs
screenOptions={{
// Tab options...
}}
>
{/* Other tabs */}
<Tabs.Screen
name="profile"
options={{
title: "Profile",
tabBarIcon: ({ color }) => (
<IconSymbol size={28} name="person.fill" color={color} />
),
}}
/>
</Tabs>
)
}
Finally, create a root index file to handle initial routing:
// app/index.tsx
import { Redirect } from "expo-router"
import { useAuth } from "@/hooks/useAuth"
export default function Index() {
const { isAuthenticated, isLoading } = useAuth()
// While checking authentication status, don't redirect yet
if (isLoading) {
return null
}
// Redirect based on authentication status
return isAuthenticated ? (
<Redirect href="/(tabs)" />
) : (
<Redirect href="/(auth)/login" />
)
}
Setting up Auth0 with Expo Router provides a robust authentication system for your mobile application. This approach leverages Expo Router's group-based routing to create a clean separation between authenticated and unauthenticated content, while the authentication context manages the state and provides a consistent interface for authentication operations.
By following this guide, you've implemented a complete authentication flow that handles login, logout, and protected routes in a maintainable and scalable way.