Caching Google and Facebook Login Authentication Data Locally in a React-Native & Redux Application

Part 4 of Implementing an Authentication Flow in React Native

Introduction

This article is part 4 in a series on adding authentication to a react-native with redux application built in Expo.io that communicates to a server built on AWS API Gateway and Lambda functions.

If you’ve been following along so far, we’ve created an application that a user can log in to using Facebook or Google login. The user can then send HTTP requests to the serverless server built using API Gateway and Lambda. The application then receives a response from the server which is displayed to the user.

However, there is one problem. The user has to log in every time they restart the application. This inconvenience is easily avoidable by saving credential data to a persistent data store on the client device. Saving the authentication data to a persistent store allows the application to retrieve the credential data and use it authentication and authorization.

Design

Relative to previous posts, this one will be pretty simple. The react-native-storage library provides an easy to use interface for saving and loading data to the device’s persistent storage. The data is stored in key value pairs which makes it easy to save entire chunks of the redux store, and retrieve data for insertion into the redux store.

Data is persisted to the store at key points in the application’s life cycle. An application state transitions from the foreground to the background could lead to application shutdown so the data should be persisted then. React-native’s AppState method can alert our app to state transitions.

The data is retrieved and loaded into the store when the application’s top-most component mounts.

Additionally, data should be removed from the store whenever the user logs out of the account, or the user logs in using another account.

Implementation

First, I added an event listener that to call functions when the application changed to the background. Since I anticipated that I may want to do multiple things during the componentWillMount of my top-most component, App, I created an initApplication function to call this potential multitude of functions.

componentWillMount() {
// initialize application
this.initApplication();
}
initApplication() {
// add event listeners
AppState.addEventListener('change', this.handleAppStateChange);
AppState.addEventListener('memoryWarning', this.handleMemoryWarning);
this.addBackButtonListener();
// intialize the storage system
let initedStorage = storageUtils.initStorage();
this.props.dispatch(actions.appStateActions.saveStorage(initedStorage));
}

The two portions that are relevant to storage are adding the event listener for application state changes, and storageUtils.initStorage() which retrieves credential information from storage to be added to the redux store.

First, I’ll discuss the functionality that activates on application state changes. The handleAppStateChange function determines if the app has moved from the foreground to the background. If that is the case, the function storeState is called.

handleAppStateChange = (nextAppState) =>{
if (this.state.appState.match(/inactive|background/) && nextAppState === 'active'){
console.log('App has come to the foreground!')
}
else if (this.state.appState === 'active' && nextAppState.match(/inactive|background/)){
console.log('App has gone to background!');
// save stuff to storagethis.storeState();
}
this.setState({ appState: nextAppState }); }

The function storeState determines if the user is logged in. If they are, it dispatches an action that saves credential information to storage. If not, it dispatches an action the removes any credential information that may have been store previously. This way, no credential information is stored if the application closes while the user is logged out.

storeState(){
// if the user is signed in, store their authentication information
// otherwise remove any authentication information from the store
if(this.props.authentication.signedIn){
storageUtils.saveToStorage(this.props.appState.storage, 'authentication', this.props.authentication);
}
else{
storageUtils.removeFromStorage(this.props.appState.storage, 'authentication');
}
}

The saveToStorage function is very simple. It takes the storage item that was initialized during the App component’s componentWillMount function, the key the data will be saved as, and the data itself. react-native-storage expects to work with JSON objects, so it is particularly suitable for handling items from the redux-store.

export const saveToStorage = (storage, key, data) => {
storage.save({
key, // Note: Do not use underscore("_") in key!
data
});
}

The removeFromStorage function is similarly easy to use; all it requires is the storage item and the key to be removed:

export const removeFromStorage = (storage, key) =>{
storage.remove({
key
});
}

The loadFromStorage function is a bit more involved. react-native-storage loads data asynchronously, so I use the same promise, redux-saga, react-native-middleware combination that I’ve discussed in multiple previous posts to load data from the devices storage into the redux store.

export const loadFromStorage = (storage, key) => {
return storage.load({
key,
// autoSync(default true) means if data not found or expired,
// then invoke the corresponding sync method
autoSync:true, // syncInBackground(default true) means if data expired,
// return the outdated data first while invoke the sync method.
// It can be set to false to always return data provided by sync method when expired.(Of course it's slower)
syncInBackground:true
})
.then(ret => {
console.log(ret)
return({
type: 'success',
item: ret
})
})
.catch(err => {
console.log(err.message);
return({
type: 'error',
error: err
})
})
}

loadFromStorage always resolves successfully resulting in redux-promis-middleware dispatching a LOAD_AUTHENTICATION_FROM_STORAGE_FULFILLED action which calls a reducer that checks to make sure the action’s payload type is “success”, and sets the credential information in the store if it is. It also sets a flag in the store denoting that the device’s storage has already been checked for credentials. Without this flag, the application would check the device for credential information every time it became active.

One final check is to ensure the token that is loaded is still valid. This may involve something as simple as making sure the token hasn’t expired. All tokens access tokens have a limited lifetime. Unfortunately, not all tokens convey that lifetime in the same way. For example, one service may provide the date the token expires, while another service may provide a countdown to expiration. Bottom line is to read the documentation to view the format used by the login service you are using.

Alternatively, one could be more thorough and make a call to the service’s API to verify the token is still considered valid; and if not, obtain a new token or make the user log in again.

Either way, invalid tokens should be removed from the store.

Conclusion

That’s it. That’s all there is to caching the authentication information. Now, when the user opens the app, they will be logged into the same account that was logged in when they last closed it.

The same technique can be used to store any information that you want persisted across invocations of your application.

In the next article, I will go over adding user name and password authentication using the Auth0 authentication as a service.

Reginald Johnson has maintained his passion for coding throughout his 20+ year career as an Officer in the United States Navy. He enjoys applying his training and experience in programming, Systems Engineering, and Operational Planning towards programming. Follow him Twitter @reginald3.