Authentication in React Native, Easy, Secure, and Reusable solution.

Youssouf EL Azizi
August 17th, 2020 · 3 min read

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

lets Go

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.tsx
2import * as React from 'react'
3import { getToken, setToken, removeToken } from './utils.tsx'
4
5interface AuthState {
6 userToken: string | undefined | null
7 status: 'idle' | 'signOut' | 'signIn'
8}
9type AuthAction = { type: 'SIGN_IN'; token: string } | { type: 'SIGN_OUT' }
10
11type AuthPayload = string
12
13interface AuthContextActions {
14 signIn: (data: AuthPayload) => void
15 signOut: () => void
16}
17
18interface AuthContextType extends AuthState, AuthContextActions {}
19
20const AuthContext = React.createContext<AuthContextType>({
21 status: 'idle',
22 userToken: null,
23 signIn: () => {},
24 signOut: () => {},
25})
26
27export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
28 const [state, dispatch] = React.useReducer(AuthReducer, {
29 status: 'idle',
30 userToken: null,
31 })
32
33 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 here
44 // Maybe sign_out user!
45 }
46 }
47
48 initState()
49 }, [])
50
51 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 Vars
59 dispatch({ type: 'SIGN_OUT' })
60 },
61 }),
62 []
63 )
64
65 return (
66 <AuthContext.Provider value={{ ...state, ...authActions }}>
67 {children}
68 </AuthContext.Provider>
69 )
70}
71
72const 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.tsx
2// ...
3
4export 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 here
11 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.

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 :

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

1//utils.tsx
2import SInfo from 'react-native-sensitive-info';
3
4const TOKEN = 'token';
5const SHARED_PERFS = 'ObytesSharedPerfs';
6const KEYCHAIN_SERVICE = 'ObytesKeychain';
7const keyChainOptions = {
8 sharedPreferencesName: SHARED_PERFS,
9 keychainService: KEYCHAIN_SERVICE,
10};
11
12export async function getItem<T>(key: string): Promise<T | null> {
13 const value = await SInfo.getItem(key, keyChainOptions);
14 return value ? JSON.parse(value)?.[key] || null : null;
15}
16
17export async function setItem<T>(key: string, value: T): Promise<void> {
18 SInfo.setItem(key, JSON.stringify({[key]: value}), keyChainOptions);
19}
20export async function removeItem(key: string): Promise<void> {
21 SInfo.deleteItem(key, keyChainOptions);
22}
23
24export 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.tsx
2import * as SecureStore from 'expo-secure-store'
3
4const TOKEN = 'token'
5
6export async function getItem(key: string): Promise<string | null> {
7 const value = await SecureStore.getItemAsync(key)
8 return value ? value : null
9}
10
11export 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}
17
18export 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.tsx
2
3// In case you want to use Auth functions outside React tree
4export const AuthRef = React.createRef<AuthContextActions>();
5
6
7export const AuthProvider = ({children}: {children: React.ReactNode}) => {
8
9 ....
10 // we add all Auth Action to ref
11 React.useImperativeHandle(AuthRef, () => authActions);
12
13 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 Vars
21 dispatch({type: 'SIGN_OUT'});
22 },
23 }),
24 [],
25 );
26
27 return (
28 <AuthContext.Provider value={{...state, ...authActions}}>
29 {children}
30 </AuthContext.Provider>
31 );
32};
33
34/*
35you can eaisly import AuthRef and start using Auth actions
36AuthRef.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.tsx
2import * as React from 'react'
3import { Text, View, StyleSheet, Button } from 'react-native'
4import { AuthProvider, useAuth, AuthRef } from './Auth'
5
6// you can access to Auth action directly from AuthRef
7// AuthRef.current.signOut()
8
9const LogOutButton = () => {
10 const { signOut } = useAuth()
11 return <Button title="log Out" onPress={signOut} />
12}
13
14const LogInButton = () => {
15 const { signIn } = useAuth()
16 return <Button title="log IN" onPress={() => signIn('my_token')} />
17}
18const Main = () => {
19 const { status, userToken } = useAuth()
20
21 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}
34
35export default function App() {
36 return (
37 <AuthProvider>
38 <Main />
39 </AuthProvider>
40 )
41}

Wrap Up

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.

More articles from Obytes

Building a Full-Text Search App Using Django, Docker and Elasticsearch

In this article, I will be giving you brief information about Elasticsearch, its installation, and some examples of usage.

August 13th, 2020 · 2 min read

Sending notifications to Slack using AWS Chatbot.

A guide to set up AWS Chatbot service to send custom notifications to a Slack channel using Terraform.

August 3rd, 2020 · 2 min read

ABOUT US

Our mission and ambition is to challenge the status quo, by doing things differently we nurture our love for craft and technology allowing us to create the unexpected.