1--- 2title: Authentication in Expo Router 3sidebar_title: Authentication 4description: How to protect routes with Expo Router. 5--- 6 7import { Lock01Icon, LockUnlocked01Icon } from '@expo/styleguide-icons'; 8import { Collapsible } from '~/ui/components/Collapsible'; 9import { FileTree } from '~/ui/components/FileTree'; 10import { Step } from '~/ui/components/Step'; 11 12With Expo Router, all routes are always defined and accessible. You can use runtime logic to redirect users away from specific screens depending on whether they are authenticated. There are two different techniques for authenticating users within routes. This guide provides an example that demonstrates the functionality of standard native apps. 13 14## Using React Context and Route Groups 15 16It's common to restrict specific routes to users who are not authenticated. This is achievable in an organized way by using React Context and Route Groups. Consider the following project structure that has a `/sign-in` route that is always accessible and a `(app)` group that requires authentication: 17 18<FileTree 19 files={[ 20 'app/_layout.js', 21 [ 22 'app/sign-in.js', 23 <span> 24 Always accessible <LockUnlocked01Icon className="inline mb-1" /> 25 </span>, 26 ], 27 ['app/(app)/_layout.js', <span>Protects child routes</span>], 28 [ 29 'app/(app)/index.js', 30 <span> 31 Requires authorization <Lock01Icon className="inline mb-1" /> 32 </span>, 33 ], 34 ]} 35/> 36 37<Step label="1"> 38 39To follow the above example, set up a [React Context provider](https://react.dev/reference/react/createContext) that can expose an authentication session to the entire app. You can implement your custom authentication session provider or use the one from the **Example authentication context** below. 40 41<Collapsible summary="Example authentication context"> 42 43This provider uses a mock implementation. You can replace it with your own [authentication provider](/guides/authentication/). 44 45```js ctx.tsx 46import React from 'react'; 47import { useStorageState } from './useStorageState'; 48 49const AuthContext = React.createContext<{ signIn: () => void; signOut: () => void; session?: string | null, isLoading: boolean } | null>(null); 50 51// This hook can be used to access the user info. 52export function useSession() { 53 const value = React.useContext(AuthContext); 54 if (process.env.NODE_ENV !== 'production') { 55 if (!value) { 56 throw new Error('useSession must be wrapped in a <SessionProvider />'); 57 } 58 } 59 60 return value; 61} 62 63export function SessionProvider(props) { 64 const [[isLoading, session], setSession] = useStorageState('session'); 65 66 return ( 67 <AuthContext.Provider 68 value={{ 69 signIn: () => { 70 // Perform sign-in logic here 71 setSession('xxx'); 72 }, 73 signOut: () => { 74 setSession(null); 75 }, 76 session, 77 isLoading, 78 }}> 79 {props.children} 80 </AuthContext.Provider> 81 ); 82} 83``` 84 85The following code snippet is a basic hook that persists tokens securely on native with [expo-secure-store](/versions/latest/sdk/securestore) and in local storage on web. 86 87{/* prettier-ignore */} 88```ts useStorageState.ts 89import * as SecureStore from 'expo-secure-store'; 90import * as React from 'react'; 91import { Platform } from 'react-native'; 92 93type UseStateHook<T> = [[boolean, T | null], (value?: T | null) => void]; 94 95function useAsyncState<T>( 96 initialValue: [boolean, T | null] = [true, undefined], 97): UseStateHook<T> { 98 return React.useReducer( 99 (state: [boolean, T | null], action: T | null = null) => [false, action], 100 initialValue 101 ) as UseStateHook<T>; 102} 103 104export async function setStorageItemAsync(key: string, value: string | null) { 105 if (Platform.OS === 'web') { 106 try { 107 if (value === null) { 108 localStorage.removeItem(key); 109 } else { 110 localStorage.setItem(key, value); 111 } 112 } catch (e) { 113 console.error('Local storage is unavailable:', e); 114 } 115 } else { 116 if (value == null) { 117 await SecureStore.deleteItemAsync(key); 118 } else { 119 await SecureStore.setItemAsync(key, value); 120 } 121 } 122} 123 124export function useStorageState(key: string): UseStateHook<string> { 125 // Public 126 const [state, setState] = useAsyncState<string>(); 127 128 // Get 129 React.useEffect(() => { 130 if (Platform.OS === 'web') { 131 try { 132 if (typeof localStorage !== 'undefined') { 133 setState(localStorage.getItem(key)); 134 } 135 } catch (e) { 136 console.error('Local storage is unavailable:', e); 137 } 138 } else { 139 SecureStore.getItemAsync(key).then(value => { 140 setState(value); 141 }); 142 } 143 }, [key]); 144 145 // Set 146 const setValue = React.useCallback( 147 (value: string | null) => { 148 setStorageItemAsync(key, value).then(() => { 149 setState(value); 150 }); 151 }, 152 [key] 153 ); 154 155 return [state, setValue]; 156} 157``` 158 159</Collapsible> 160 161</Step> 162 163<Step label="2"> 164 165Use the `SessionProvider` in the root layout to provide the authentication context to the entire app. It's imperative that the `<Slot />` is mounted before any navigation events are triggered. Otherwise, a runtime error will be thrown. 166 167```jsx app/_layout.js 168import { Slot } from 'expo-router'; 169import { SessionProvider } from '../ctx'; 170 171export default function Root() { 172 // Set up the auth context and render our layout inside of it. 173 return ( 174 <SessionProvider> 175 <Slot /> 176 </SessionProvider> 177 ); 178} 179``` 180 181</Step> 182 183<Step label="3"> 184 185Create a nested [Layout route](/routing/layouts) that checks whether users are authenticated before rendering the child route components. This layout route redirects users to the sign-in screen if they are not authenticated. 186 187```jsx app/(app)/_layout.js 188import { Link, Redirect, Stack } from 'expo-router'; 189import { Text, View } from 'react-native'; 190 191import { useSession } from '../../ctx'; 192 193export default function AppLayout() { 194 const { session, isLoading } = useSession(); 195 196 // You can keep the splash screen open, or render a loading screen like we do here. 197 if (isLoading) { 198 return <Text>Loading...</Text>; 199 } 200 201 // Only require authentication within the (app) group's layout as users 202 // need to be able to access the (auth) group and sign in again. 203 if (!session) { 204 // On web, static rendering will stop here as the user is not authenticated 205 // in the headless Node process that the pages are rendered in. 206 return <Redirect href="/sign-in" />; 207 } 208 209 // This layout can be deferred because it's not the root layout. 210 return <Stack />; 211} 212``` 213 214</Step> 215 216<Step label="4"> 217 218Create the `/sign-in` screen. It can toggle the authentication using `signIn()`. Since this screen is outside the `(app)` group, the group's layout and authentication check do not run when rendering this screen. This lets logged-out users see this screen. 219 220```jsx app/sign-in.js 221import { router } from 'expo-router'; 222import { Text, View } from 'react-native'; 223 224import { useSession } from '../ctx'; 225 226export default function SignIn() { 227 const { signIn } = useSession(); 228 return ( 229 <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}> 230 <Text 231 onPress={() => { 232 signIn(); 233 // Navigate after signing in. You may want to tweak this to ensure sign-in is 234 // successful before navigating. 235 router.replace('/'); 236 }}> 237 Sign In 238 </Text> 239 </View> 240 ); 241} 242``` 243 244</Step> 245 246<Step label="5"> 247 248Implement an authenticated screen that lets users sign out. 249 250```jsx app/(app)/index.js 251import { Text, View } from 'react-native'; 252 253import { useSession } from '../../ctx'; 254 255export default function Index() { 256 const { signOut } = useSession(); 257 return ( 258 <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}> 259 <Text 260 onPress={() => { 261 // The `app/(app)/_layout.tsx` will redirect to the sign-in screen. 262 signOut(); 263 }}> 264 Sign Out 265 </Text> 266 </View> 267 ); 268} 269``` 270 271</Step> 272 273You now have an app that can present a loading state while it checks the initial authentication state and redirects to the sign-in screen if the user is not authenticated. If a user visits a deep link to any routes with the authentication check, they'll be redirected to the sign-in screen. 274 275## Alternative loading states 276 277With Expo Router, something must be rendered to the screen while loading the initial auth state. In the example above, the app layout renders a loading message. Alternatively, you can make the `index` route a loading state, and move the initial route to something such as `/home`, which is similar to how Twitter works. 278 279## Modals and per-route authentication 280 281Another common pattern is to render a sign-in modal over the top of the app. This enables you to dismiss partially preserve deep links when the authentication is complete. However, this pattern requires routes to be rendered in the background as these routes require handling data loading without authentication. 282 283<FileTree 284 files={[ 285 ['app/_layout.js', 'Declares global session context'], 286 'app/(app)/_layout.js', 287 ['app/(app)/sign-in.js', <span>Modally presented over the root</span>], 288 ['app/(app)/(root)/_layout.js', <span>Protects child routes</span>], 289 [ 290 'app/(app)/(root)/index.js', 291 <span> 292 Requires authorization <Lock01Icon className="inline mb-1" /> 293 </span>, 294 ], 295 ]} 296/> 297 298```jsx app/(app)/_layout.js 299import { Stack } from 'expo-router'; 300 301export const unstable_settings = { 302 initialRouteName: '(root)', 303}; 304 305export default function AppLayout() { 306 return ( 307 <Stack> 308 <Stack name="(root)" /> 309 <Stack 310 name="sign-in" 311 options={{ 312 presentation: 'modal', 313 }} 314 /> 315 </Stack> 316 ); 317} 318``` 319 320## Navigating without navigation 321 322You may encounter the following error when the app attempts to perform navigation without a navigator mounted in the [root layout](/router/advanced/root-layout). 323 324``` 325Error: Attempted to navigate before mounting the Root Layout component. Ensure the Root Layout component is rendering a Slot, or other navigator on the first render. 326``` 327 328To fix this, add a group and move conditional logic down a level. 329 330### Before 331 332<FileTree files={['app/_layout.js', 'app/about.js']} /> 333 334```js app/_layout.js 335export default function RootLayout() { 336 React.useEffect(() => { 337 // This navigation event will trigger the error above. 338 router.push('/about'); 339 }, []); 340 341 // This conditional statement creates a problem since the root layout's 342 // content (the Slot) must be mounted before any navigation events occur. 343 if (isLoading) { 344 return <Text>Loading...</Text>; 345 } 346 347 return <Slot />; 348} 349``` 350 351### After 352 353<FileTree 354 files={[ 355 ['app/_layout.js', ''], 356 ['app/(app)/_layout.js', 'Move conditional logic down a level'], 357 'app/(app)/about.js', 358 ]} 359/> 360 361```js app/_layout.js 362export default function RootLayout() { 363 return <Slot />; 364} 365``` 366 367```js app/(app)/_layout.js 368export default function RootLayout() { 369 React.useEffect(() => { 370 // This navigation event will trigger the error above. 371 router.push('/about'); 372 }, []); 373 374 // It is OK to defer rendering this nested layout's content. We couldn't 375 // defer rendering the root layout's content since a navigation event (the 376 // redirect) would have been triggered before the root layout's content had 377 // been mounted. 378 if (isLoading) { 379 return <Text>Loading...</Text>; 380 } 381 382 return <Slot />; 383} 384``` 385 386## Middleware 387 388Traditionally, websites may leverage some form of server-side redirection to protect routes. As of Expo Router v2, this library on the web currently only supports build-time static generation and has no support for custom middleware or serving. This can be added in the future to provide a more optimal web experience. In the meantime, authentication can be implemented by using client-side redirects and a loading state. 389