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