---
title: Authentication in Expo Router
sidebar_title: Authentication
description: How to protect routes with Expo Router.
---

import { Lock01Icon, LockUnlocked01Icon } from '@expo/styleguide-icons';
import { Collapsible } from '~/ui/components/Collapsible';
import { FileTree } from '~/ui/components/FileTree';
import { Step } from '~/ui/components/Step';

With 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.

## Using React Context and Route Groups

It'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:

<FileTree
  files={[
    'app/_layout.js',
    [
      'app/sign-in.js',
      <span>
        Always accessible <LockUnlocked01Icon className="inline mb-1" />
      </span>,
    ],
    ['app/(app)/_layout.js', <span>Protects child routes</span>],
    [
      'app/(app)/index.js',
      <span>
        Requires authorization <Lock01Icon className="inline mb-1" />
      </span>,
    ],
  ]}
/>

<Step label="1">

To 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.

<Collapsible summary="Example authentication context">

This provider uses a mock implementation. You can replace it with your own [authentication provider](/guides/authentication/).

```js ctx.tsx
import React from 'react';
import { useStorageState } from './useStorageState';

const AuthContext = React.createContext<{ signIn: () => void; signOut: () => void; session?: string | null, isLoading: boolean } | null>(null);

// This hook can be used to access the user info.
export function useSession() {
  const value = React.useContext(AuthContext);
  if (process.env.NODE_ENV !== 'production') {
    if (!value) {
      throw new Error('useSession must be wrapped in a <SessionProvider />');
    }
  }

  return value;
}

export function SessionProvider(props) {
  const [[isLoading, session], setSession] = useStorageState('session');

  return (
    <AuthContext.Provider
      value={{
        signIn: () => {
          // Perform sign-in logic here
          setSession('xxx');
        },
        signOut: () => {
          setSession(null);
        },
        session,
        isLoading,
      }}>
      {props.children}
    </AuthContext.Provider>
  );
}
```

The 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.

{/* prettier-ignore */}
```ts useStorageState.ts
import * as SecureStore from 'expo-secure-store';
import * as React from 'react';
import { Platform } from 'react-native';

type UseStateHook<T> = [[boolean, T | null], (value?: T | null) => void];

function useAsyncState<T>(
  initialValue: [boolean, T | null] = [true, undefined],
): UseStateHook<T> {
  return React.useReducer(
    (state: [boolean, T | null], action: T | null = null) => [false, action],
    initialValue
  ) as UseStateHook<T>;
}

export async function setStorageItemAsync(key: string, value: string | null) {
  if (Platform.OS === 'web') {
    try {
      if (value === null) {
        localStorage.removeItem(key);
      } else {
        localStorage.setItem(key, value);
      }
    } catch (e) {
      console.error('Local storage is unavailable:', e);
    }
  } else {
    if (value == null) {
      await SecureStore.deleteItemAsync(key);
    } else {
      await SecureStore.setItemAsync(key, value);
    }
  }
}

export function useStorageState(key: string): UseStateHook<string> {
  // Public
  const [state, setState] = useAsyncState<string>();

  // Get
  React.useEffect(() => {
    if (Platform.OS === 'web') {
      try {
        if (typeof localStorage !== 'undefined') {
          setState(localStorage.getItem(key));
        }
      } catch (e) {
        console.error('Local storage is unavailable:', e);
      }
    } else {
      SecureStore.getItemAsync(key).then(value => {
        setState(value);
      });
    }
  }, [key]);

  // Set
  const setValue = React.useCallback(
    (value: string | null) => {
      setStorageItemAsync(key, value).then(() => {
        setState(value);
      });
    },
    [key]
  );

  return [state, setValue];
}
```

</Collapsible>

</Step>

<Step label="2">

Use 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.

```jsx app/_layout.js
import { Slot } from 'expo-router';
import { SessionProvider } from '../ctx';

export default function Root() {
  // Set up the auth context and render our layout inside of it.
  return (
    <SessionProvider>
      <Slot />
    </SessionProvider>
  );
}
```

</Step>

<Step label="3">

Create 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.

```jsx app/(app)/_layout.js
import { Link, Redirect, Stack } from 'expo-router';
import { Text, View } from 'react-native';

import { useSession } from '../../ctx';

export default function AppLayout() {
  const { session, isLoading } = useSession();

  // You can keep the splash screen open, or render a loading screen like we do here.
  if (isLoading) {
    return <Text>Loading...</Text>;
  }

  // Only require authentication within the (app) group's layout as users
  // need to be able to access the (auth) group and sign in again.
  if (!session) {
    // On web, static rendering will stop here as the user is not authenticated
    // in the headless Node process that the pages are rendered in.
    return <Redirect href="/sign-in" />;
  }

  // This layout can be deferred because it's not the root layout.
  return <Stack />;
}
```

</Step>

<Step label="4">

Create 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.

```jsx app/sign-in.js
import { router } from 'expo-router';
import { Text, View } from 'react-native';

import { useSession } from '../ctx';

export default function SignIn() {
  const { signIn } = useSession();
  return (
    <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
      <Text
        onPress={() => {
          signIn();
          // Navigate after signing in. You may want to tweak this to ensure sign-in is
          // successful before navigating.
          router.replace('/');
        }}>
        Sign In
      </Text>
    </View>
  );
}
```

</Step>

<Step label="5">

Implement an authenticated screen that lets users sign out.

```jsx app/(app)/index.js
import { Text, View } from 'react-native';

import { useSession } from '../../ctx';

export default function Index() {
  const { signOut } = useSession();
  return (
    <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
      <Text
        onPress={() => {
          // The `app/(app)/_layout.tsx` will redirect to the sign-in screen.
          signOut();
        }}>
        Sign Out
      </Text>
    </View>
  );
}
```

</Step>

You 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.

## Alternative loading states

With 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.

## Modals and per-route authentication

Another 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.

<FileTree
  files={[
    ['app/_layout.js', 'Declares global session context'],
    'app/(app)/_layout.js',
    ['app/(app)/sign-in.js', <span>Modally presented over the root</span>],
    ['app/(app)/(root)/_layout.js', <span>Protects child routes</span>],
    [
      'app/(app)/(root)/index.js',
      <span>
        Requires authorization <Lock01Icon className="inline mb-1" />
      </span>,
    ],
  ]}
/>

```jsx app/(app)/_layout.js
import { Stack } from 'expo-router';

export const unstable_settings = {
  initialRouteName: '(root)',
};

export default function AppLayout() {
  return (
    <Stack>
      <Stack name="(root)" />
      <Stack
        name="sign-in"
        options={{
          presentation: 'modal',
        }}
      />
    </Stack>
  );
}
```

## Navigating without navigation

You may encounter the following error when the app attempts to perform navigation without a navigator mounted in the [root layout](/router/advanced/root-layout).

```
Error: 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.
```

To fix this, add a group and move conditional logic down a level.

### Before

<FileTree files={['app/_layout.js', 'app/about.js']} />

```js app/_layout.js
export default function RootLayout() {
  React.useEffect(() => {
    // This navigation event will trigger the error above.
    router.push('/about');
  }, []);

  // This conditional statement creates a problem since the root layout's
  // content (the Slot) must be mounted before any navigation events occur.
  if (isLoading) {
    return <Text>Loading...</Text>;
  }

  return <Slot />;
}
```

### After

<FileTree
  files={[
    ['app/_layout.js', ''],
    ['app/(app)/_layout.js', 'Move conditional logic down a level'],
    'app/(app)/about.js',
  ]}
/>

```js app/_layout.js
export default function RootLayout() {
  return <Slot />;
}
```

```js app/(app)/_layout.js
export default function RootLayout() {
  React.useEffect(() => {
    // This navigation event will trigger the error above.
    router.push('/about');
  }, []);

  // It is OK to defer rendering this nested layout's content. We couldn't
  // defer rendering the root layout's content since a navigation event (the
  // redirect) would have been triggered before the root layout's content had
  // been mounted.
  if (isLoading) {
    return <Text>Loading...</Text>;
  }

  return <Slot />;
}
```

## Middleware

Traditionally, 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.
