Authentication in React Native, Easy, Secure, and Reusable solution πͺ.
Youssouf El Azizi
βAugust 25, 2020
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.
In todayβs article, I will share my solution, that I fell itβs the right way to handle authentication in a React Native Project. If you have some ideas to improve the implementation feel free to leave a comment.
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.
From my experience dealing with the same task, i would say that the solution must be:
- Generic and reusable.
- Should provide a
useAuth
hook to access the Auth state and Actions. - Should be secure, using the right solution to secure tokens and user data.
- Should 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.
will use Typescript for code example as we start using it for new projects π
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.
if you are using a state library to manage your project state, you can use it for Authentication too, and maybe get some inspiration from my solution.
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.
1/// Auth.tsx2import * as React from "react";3import { getToken, setToken, removeToken } from "./utils.tsx";45interface AuthState {6 userToken: string | undefined | null;7 status: "idle" | "signOut" | "signIn";8}9type AuthAction = { type: "SIGN_IN"; token: string } | { type: "SIGN_OUT" };1011type AuthPayload = string;1213interface AuthContextActions {14 signIn: (data: AuthPayload) => void;15 signOut: () => void;16}1718interface AuthContextType extends AuthState, AuthContextActions {}1920const AuthContext = React.createContext<AuthContextType>({21 status: "idle",22 userToken: null,23 signIn: () => {},24 signOut: () => {},25});2627export const AuthProvider = ({ children }: { children: React.ReactNode }) => {28 const [state, dispatch] = React.useReducer(AuthReducer, {29 status: "idle",30 userToken: null,31 });3233 React.useEffect(() => {34 const initState = async () => {35 try {36 const userToken = await getToken();37 if (userToken !== null) {38 dispatch({ type: "SIGN_IN", token: userToken });39 } else {40 dispatch({ type: "SIGN_OUT" });41 }42 } catch (e) {43 // catch error here44 // Maybe sign_out user!45 }46 };4748 initState();49 }, []);5051 const authActions: AuthContextActions = React.useMemo(52 () => ({53 signIn: async (token: string) => {54 dispatch({ type: "SIGN_IN", token });55 await setToken(token);56 },57 signOut: async () => {58 await removeToken(); // TODO: use Vars59 dispatch({ type: "SIGN_OUT" });60 },61 }),62 []63 );6465 return (66 <AuthContext.Provider value={{ ...state, ...authActions }}>67 {children}68 </AuthContext.Provider>69 );70};7172const AuthReducer = (prevState: AuthState, action: AuthAction): AuthState => {73 switch (action.type) {74 case "SIGN_IN":75 return {76 ...prevState,77 status: "signIn",78 userToken: action.token,79 };80 case "SIGN_OUT":81 return {82 ...prevState,83 status: "signOut",84 userToken: null,85 };86 }87};
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
1// Auth.tsx2// ...34export const useAuth = (): AuthContextType => {5 const context = React.useContext(AuthContext);6 if (!context) {7 throw new Error("useAuth must be inside an AuthProvider with a value");8 }9 /*10 you can add more drived state here11 const isLoggedIn = context.status ==== 'signIn'12 return ({ ...context, isloggedIn})13 */14 return context;15};
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.
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 :
1yarn add react-native-sensitive-info@next
Then we can eaisly implement our helpers functions like the fllowing:
1//utils.tsx2import SInfo from "react-native-sensitive-info";34const TOKEN = "token";5const SHARED_PERFS = "MyAppSharedPerfs";6const KEYCHAIN_SERVICE = "MyAppKeychain";7const keyChainOptions = {8 sharedPreferencesName: SHARED_PERFS,9 keychainService: KEYCHAIN_SERVICE,10};1112export async function getItem<T>(key: string): Promise<T | null> {13 const value = await SInfo.getItem(key, keyChainOptions);14 return value ? value : null;15}1617export async function setItem<T>(key: string, value: T): Promise<void> {18 return SInfo.setItem(key, value, keyChainOptions);19}20export async function removeItem(key: string): Promise<void> {21 return SInfo.deleteItem(key, keyChainOptions);22}2324export const getToken = () => getItem<string>(TOKEN);25export const removeToken = () => removeItem(TOKEN);26export 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
1// utils-expo.tsx2import * as SecureStore from "expo-secure-store";34const TOKEN = "token";56export async function getItem(key: string): Promise<string | null> {7 const value = await SecureStore.getItemAsync(key);8 return value ? value : null;9}1011export async function setItem(key: string, value: string): Promise<void> {12 return SecureStore.setItemAsync(key, value);13}14export async function removeItem(key: string): Promise<void> {15 return SecureStore.deleteItemAsync(key);16}1718export const getToken = () => getItem(TOKEN);19export const removeToken = () => removeItem(TOKEN);20export 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 :
1// Auth.tsx23// In case you want to use Auth functions outside React tree4export const AuthRef = React.createRef<AuthContextActions>();567export const AuthProvider = ({children}: {children: React.ReactNode}) => {89 ....10 // we add all Auth Action to ref11 React.useImperativeHandle(AuthRef, () => authActions);1213 const authActions: AuthContextActions = React.useMemo(14 () => ({15 signIn: async (token: string) => {16 dispatch({type: 'SIGN_IN', token});17 await setToken(token);18 },19 signOut: async () => {20 await removeToken(); // TODO: use Vars21 dispatch({type: 'SIGN_OUT'});22 },23 }),24 [],25 );2627 return (28 <AuthContext.Provider value={{...state, ...authActions}}>29 {children}30 </AuthContext.Provider>31 );32};3334/*35you can eaisly import AuthRef and start using Auth actions36AuthRef.current.signOut()37*/
Demo
To use the solution we need to wrap our Root component with AuthProvider and start using useAuth
to access and update state.
1// App.tsx2import * as React from "react";3import { Text, View, StyleSheet, Button } from "react-native";4import { AuthProvider, useAuth, AuthRef } from "./Auth";56// you can access to Auth action directly from AuthRef7// AuthRef.current.signOut()89const LogOutButton = () => {10 const { signOut } = useAuth();11 return <Button title="log Out" onPress={signOut} />;12};1314const LogInButton = () => {15 const { signIn } = useAuth();16 return <Button title="log IN" onPress={() => signIn("my_token")} />;17};18const Main = () => {19 const { status, userToken } = useAuth();2021 return (22 <View style={styles.container}>23 <Text style={styles.text}>status : {status}</Text>24 <Text style={styles.text}>25 userToken : {userToken ? userToken : "null"}26 </Text>27 <View style={styles.actions}>28 <LogInButton />29 <LogOutButton />30 </View>31 </View>32 );33};3435export default function App() {36 return (37 <AuthProvider>38 <Main />39 </AuthProvider>40 );41}
Wrap Up
- You can find complete source code π react-native-auth
- Expo Demo : Demo
I hope you found that interesting, informative, and entertaining. I would be more than happy to hear your remarks and thoughts about this solution in The comments.
If you think other people should read this post. Tweet,share and Follow me on twitter for the next articles.
Originally published on https://obytes.com/