Skip to main content

Authentication flow

Authentication for a React/React Native project is a task that you will see in your project backlog whatever you are working on a simple or complex application. And having a complete guide or a generic approach to do it will help maintain code and save time.

For this guide, We aim to build a generic solution that handles most of the authentication use cases and easy to copy-paste in your next project.

The Following solution is :

  • Generic and reusable.
  • Provide a useAuth hook to access the Auth state and Actions.
  • Secure, using the right solution to secure tokens and user data.
  • Provide A way to invoke the Athentication Action outside React component tree. ( call signOut inside an apollo graphql generic error as an example).
  • Performance

Approach :#

We will need an Auth provider to save our auth state status and create some action as signIn signUp signOut to update the Auth state. Then we need to create a custom hook that we can use to access state and action anywhere in our code. We will use the most secure options to persist state and store user tokens. Finally, our solution will provide a way to get access to auth actions outside components three using some react refs tricks.

Create The Authentication Context.#

Maybe you are familiar with a state Management library solution to handle such cases, but I think using React Conext Api is More than enough to handle authentication and provide a clean and complete solution without installing a third-party library.

First, we are going to create a simple Authentication context and implement the Provider components.

As we have 3 state loading, singOut, signIn for Auth status, I think using an enumeration state is the best choice to prevent bugs and to simplify the implementation. Also, we will need the userToken state to save the token.

Using a reducer hook approach to update the state will help make our code clean and easy to follow.

/// Auth.tsximport * as React from "react";import { getToken, setToken, removeToken } from "./utils.tsx";
interface AuthState {  userToken: string | undefined | null;  status: "idle" | "signOut" | "signIn";}type AuthAction = { type: "SIGN_IN"; token: string } | { type: "SIGN_OUT" };
type AuthPayload = string;
interface AuthContextActions {  signIn: (data: AuthPayload) => void;  signOut: () => void;}
interface AuthContextType extends AuthState, AuthContextActions {}
const AuthContext = React.createContext<AuthContextType>({  status: "idle",  userToken: null,  signIn: () => {},  signOut: () => {},});
export const AuthProvider = ({ children }: { children: React.ReactNode }) => {  const [state, dispatch] = React.useReducer(AuthReducer, {    status: "idle",    userToken: null,  });
  React.useEffect(() => {    const initState = async () => {      try {        const userToken = await getToken();        if (userToken !== null) {          dispatch({ type: "SIGN_IN", token: userToken });        } else {          dispatch({ type: "SIGN_OUT" });        }      } catch (e) {        // catch error here        // Maybe sign_out user!      }    };
    initState();  }, []);
  const authActions: AuthContextActions = React.useMemo(    () => ({      signIn: async (token: string) => {        dispatch({ type: "SIGN_IN", token });        await setToken(token);      },      signOut: async () => {        await removeToken(); // TODO: use Vars        dispatch({ type: "SIGN_OUT" });      },    }),    []  );
  return (    <AuthContext.Provider value={{ ...state, ...authActions }}>      {children}    </AuthContext.Provider>  );};
const AuthReducer = (prevState: AuthState, action: AuthAction): AuthState => {  switch (action.type) {    case "SIGN_IN":      return {        ...prevState,        status: "signIn",        userToken: action.token,      };    case "SIGN_OUT":      return {        ...prevState,        status: "signOut",        userToken: null,      };  }};

We create our auth actions using useMemo hook to memoize them, This optimization helps to avoid generating new instances on every render.

To make using our state more enjoyable and easy, we will create a simple Hook that returns our Auth state and actions and throw an error whenever the user trying to get Auth state without wrapping React tree with AuthProvider

// Auth.tsx// ...
export const useAuth = (): AuthContextType => {  const context = React.useContext(AuthContext);  if (!context) {    throw new Error("useAuth must be inside an AuthProvider with a value");  }  /*    you can add more drived state here    const isLoggedIn  = context.status ==== 'signIn'    return ({ ...context, isloggedIn})  */  return context;};

Store Tokens: The secure way#

It's a little bit confusing to see almost all people using local storage to store token as it's not the most secure one, maybe i can understand if people using it to make their post easy to follow, but i think it's not recommended to use it in production and instead you need to use secure storage solution such as Keychain. Unfortunately React Native does not come bundled with any way of storing sensitive data. However, there are pre-existing solutions for Android and iOS platforms.

Read more about security in React Native

In this part we are going to implement the getToken, setToken and removeToken using react-native-sensitive-data package.

On Android, RNSInfo will automatically encrypt the token using keystore and save it into shared preferences and for IOS, RNSInfo will automatically save your data into user's keychain which is handled by OS.

To get started, First Make sure to install react-native-sensitive-data dependency :

yarn add [email protected]

Then we can easily implement our helpers functions like the fllowing:

//utils.tsximport SInfo from "react-native-sensitive-info";
const TOKEN = "token";const SHARED_PERFS = "ObytesSharedPerfs";const KEYCHAIN_SERVICE = "ObytesKeychain";const keyChainOptions = {  sharedPreferencesName: SHARED_PERFS,  keychainService: KEYCHAIN_SERVICE,};
export async function getItem<T>(key: string): Promise<T | null> {  const value = await SInfo.getItem(key, keyChainOptions);  return value ? JSON.parse(value)?.[key] || null : null;}
export async function setItem<T>(key: string, value: T): Promise<void> {  SInfo.setItem(key, JSON.stringify({ [key]: value }), keyChainOptions);}export async function removeItem(key: string): Promise<void> {  SInfo.deleteItem(key, keyChainOptions);}
export const getToken = () => getItem<string>(TOKEN);export const removeToken = () => removeItem(TOKEN);export const setToken = (value: string) => setItem<string>(TOKEN, value);

If you are using expo you can switch to expo-secure-data package instead of react-native-sensitive-info

// utils-expo.tsximport * as SecureStore from "expo-secure-store";
const TOKEN = "token";
export async function getItem(key: string): Promise<string | null> {  const value = await SecureStore.getItemAsync(key);  return value ? value : null;}
export async function setItem(key: string, value: string): Promise<void> {  return SecureStore.setItemAsync(key, value);}export async function removeItem(key: string): Promise<void> {  return SecureStore.deleteItemAsync(key);}
export const getToken = () => getItem(TOKEN);export const removeToken = () => removeItem(TOKEN);export const setToken = (value: string) => setItem(TOKEN, value);

Access Auth Actions outside component three.#

One of the limitations using the provider in react is that you can't use context action outside the React component tree.

For Authentication workflow, i found my self want to signOut user on some error issue caught on Apollo client or maybe or inside a non-component thing.

Recently I found a quick and easy solution that lets you get access Auth context actions outside the component tree using a react reference.

The idea was to create a global React reference and use useImperativeHandle hook to expose auth actions to our global Ref like the following :

// Auth.tsx
// In case you want to use Auth functions outside React treeexport const AuthRef = React.createRef<AuthContextActions>();

export const AuthProvider = ({children}: {children: React.ReactNode}) => {
   ....  // we add all Auth Action to ref  React.useImperativeHandle(AuthRef, () => authActions);
  const authActions: AuthContextActions = React.useMemo(    () => ({      signIn: async (token: string) => {        dispatch({type: 'SIGN_IN', token});        await setToken(token);      },      signOut: async () => {        await removeToken(); // TODO: use Vars        dispatch({type: 'SIGN_OUT'});      },    }),    [],  );
  return (    <AuthContext.Provider value={{...state, ...authActions}}>      {children}    </AuthContext.Provider>  );};
/*you can eaisly import  AuthRef and start using Auth actionsAuthRef.current.signOut()*/

Simple Component tree Demo#

To use the solution we need to wrap our Root component with AuthProvider and start using useAuth to access and update state.

// App.tsximport * as React from "react";import { Text, View, StyleSheet, Button } from "react-native";import { AuthProvider, useAuth, AuthRef } from "./Auth";
// you can access to Auth action directly from AuthRef// AuthRef.current.signOut()
const LogOutButton = () => {  const { signOut } = useAuth();  return <Button title="log Out" onPress={signOut} />;};
const LogInButton = () => {  const { signIn } = useAuth();  return <Button title="log IN" onPress={() => signIn("my_token")} />;};const Main = () => {  const { status, userToken } = useAuth();
  return (    <View style={styles.container}>      <Text style={styles.text}>status : {status}</Text>      <Text style={styles.text}>        userToken : {userToken ? userToken : "null"}      </Text>      <View style={styles.actions}>        <LogInButton />        <LogOutButton />      </View>    </View>  );};
export default function App() {  return (    <AuthProvider>      <Main />    </AuthProvider>  );}

Auth Flow with React Navigation#

Install the react-navigation library

import React from 'react';import {useAuthContext} from './context';import SignInScreen from './SignIn';import HomeScreen from './Home;import {createStackNavigator} from '@react-navigation/stack';
const Stack = createStackNavigator();const AppNavigator = () => { const {userToken, isLoading} = useAuthContext();
  if (isLoading) {    return <SplashScreen />;  }
 return (   <Stack.Navigator>     {userToken == null ? (       <Stack.Screen name="SignIn" component={SignInScreen} />     ) : (       <Stack.Screen name="Home" component={HomeScreen} />     )}   </Stack.Navigator> );};
export default AppNavigator;
  • Finally let's wrap the App component with the AuthProvider.
import React from "react";import AppNavigator from "./AppNavigator";import "react-native-gesture-handler";import AuthProvider from "./context";import { NavigationContainer } from "@react-navigation/native";
export default function App() {  return (    <AuthProvider>      <NavigationContainer>        <AppNavigator />      </NavigationContainer>    </AuthProvider>  );}

Source Code#

Last updated on by Youssouf El Azizi