This is Part 2 of a two part post about using JSON web tokens with a React Native app using Redux and a Node Express server. Part 1 can be read here.

Updated Oct 2017

Updated Jan 2018 which includes a solution for automatically getting auth token refreshed by using Redux middleware to hold a buffer of outgoing requests which can be used to re-send a failed request when a token becomes invalid. Also added bcryprt for password hashing and updated code structure.

Updated March 2018 to re-structure the directories. The structure of directories, components and screens make more sense, and also removed redundant import and init code from the boilerplate setup.

Full code can now be found on Github for my jwt-react-native-boilerplate.

JSON Web Tokens

The React Native app

Part 1 of this two part post covered the Node JS back end of creating and storing user information with JWT auth and refresh tokens, and using them to restrict access to back-end API endpoints. This part will cover what I ended up with on the front end using React Native, and how the JWT’s link in with the app.

One of the most fundemental parts of the app is it’s use of Redux. There’s a great article on CSS Tricks for some reading up on Redux if needed as it’s way out of scope for this post. At a top level, Redux is useful to keep the app logic and UI consistent throughout the app by having all events and data pass through actions and reducers which manipulate the state to be used by the app. It sounds like a bit of a hassle, but if you’re anything more than a very basic app, I think it’s extremely helpful.

Another building block of the app is React Native Nativation by the folks over at Wix. Again, a brilliant piece of software which, like Redux, allows us to stand on the shoulders of giants to achieve great quality app performance and stability with a lot of hard work already done.

React Native Navigation is one of the best truly native navigation options for React Native, and is constantly being contributed to, with a second version due to release soon which is exciting. An alternative to truly native nav solution is the official React Navigation, which is great to get up and running and maybe even on a small project, but it depends on your requirements!

The app’s foundation relies upon React Native Navigation, as mentioned above. I won’t go into the detail about this as the focus is authentication, but the whole boilerplate app can be found on Github at the bottom of the post.

The React Native authentication

With an overview of some of the important parts of the app out the way, I’ll explain how I added JWT authentication. Part 1 of this post showed that when the user logs in they receive a JWT auth token and refresh token, and a refresh token saves in a remote database to be used to generate a new auth token when the auth token expires. The app must now save the auth token to the device for future requests to restricted content.

Logging in

Here is the Login.js component which contains the login form. I have used Redux Form in the app, but I’ll leave this out of the code examples in the post and they can be viewed on Github.

//Login.js

class Login extends Component {
  constructor(props) {
    super(props);
  }

  render() {
    const { handleSubmit } = this.props;
    const submitForm = e => {
      this.props.login(e.email, e.password);
    };

    return (
      <View style={styles.container}>
        <FormLabel>Email</FormLabel>
        <Field name="email" />
        <FormLabel>Password</FormLabel>
        <Field name="password" />
        <Button
          onPress={handleSubmit(submitForm)}
          title={'Log in'}
        />
      </View>
    );
  }
}

...

function mapStateToProps(store, ownProps) {
  return {
    ...
  };
}
function mapDispatchToProps(dispatch) {
  return {
    login: (email, password) => {
      dispatch(login(email, password));
    }
  };
}
let LoginConnect = connect(mapStateToProps, mapDispatchToProps)(Login);

Redux and Async flow

Redux uses the mapDispatchToProps() to dispatch the login action. The actions normally get sent through to reducers right away which will manipulate state and update the rest of the app, but our situation calls for something more complex as we need to handle async flow (checking user details agaisnt database) and fun a few more functions during the login process, such as making a loading spinner and perhaps more importantly, saving our tokens.

To help with this async flow, I used Redux Thunk, which allows you to write action creators that return a function instead of an action. This means that we can access the store and dispatch actions from within a dispatched action.

Redux Thunk Inception

This login action gets sent through to auth.service.js (alongside all other authentication related actions):

//auth.service.js
import * as AuthReducer from './auth.reducer';
import AuthApi from './auth.api';

...

export const login = (email, password) => {
  return dispatch => {
    dispatch(AuthReducer.setLoginPending());
    return AuthApi.login(email, password)
      .then(response => {
        if (response.success) {
          dispatch(
            AuthReducer.setLoginSuccess(
              response.authToken,
              response.refreshToken
            )
          );
          _saveItem('authToken', response.authToken)
            .then(resp => {
              _saveItem('refreshToken', response.refreshToken)
              ...
            })
        }
      })
  };
};

const _saveItem = async (item, selectedValue) => {
  try {
    await AsyncStorage.setItem(item, selectedValue);
  } catch (error) {
    throw error;
  }
};

Quite a few things going on here. Firstly as mentioned above, Redux Thunk allows us to dispatch actions from within actions. The first action we dispatch inside the login function is setLoginPending(), which will update the auth reducer to give us the ability to create soe sort of loading spinner.

On login success, another action is dispatched to the auth reducer which marks to login as sucesfull, storing the auth token and refresh token in the global store, as well as turning the loading spinner off.

A final function is executed (this time, not a Redux action) but a normal function. _saveItem() uses the React Native feature for saving data to a device, similar to web localStorage specification. This is needed so the app can get the token and refresh token from the device to keep the user logged in when they close the app.

The login function uses the AuthReducer module to keep the code nice and clean. The files are quite large and are worth looking at on Github if you’re interested, but I feel are out of scope of this post.

Using the stored JSON Web Tokens

The tokens are now stored in the apps global Redux store and on the device using AsyncStorage. They now need to be sent to the server to be used to gain access to restricted areas.

//auth.api.js
import config from '../../config';

class AuthApi {

  ...

  static checkAuthTest(token) {
    return fetch(`${config.url}/api/auth/getAll`, {
      method: 'POST',
      headers: {
        ...config.configHeaders,
        Authorization: 'Bearer ' + token
      }
    })
      .then(response => {
        return response.json();
      })
  }
}
export default AuthApi;

I made a checkAuthTest() function which hits the protected API endpoint /getAll, which was set up on Part 1 of this post. The key point here is that we are sending the token via an Authorization header, which is automatically validated using the jsonwebtoken package.

When I first looked at this I wondered why not just send the token over a query string parameter instead of over an Authoriztion header, alongside other data which is sent to the server in a normal request. The answer is not so black and white, as many people prefer one or the other.

Using the Authorization header combined with jsonwebtoken makes it nice and simple though, and doesn’t leave validation code littered throughout your server code.

Security plays a huge part here. Make sure you are only comminucating over an SSL connection as without this people can intercept the auth token (or even worse, the refresh token) and gain access to your account until the auth token expires, or the refresh token is refreshed/revoked.

Refreshing the auth token

Part 1 discussed how to refresh the auth token from the server, but I made the app handle an important part of this. I was torn between 2 options:

Option 1

One option I found was to refresh the token when the server received an invalid auth token. This way though, there would be the following process:

  • User sends invalid auth token
  • Server notices invalid auth token, and requires a refresh token to get a new one
  • Server sends request for refresh token back to the app
  • The app (still in the middle of the initial server request) sends the refresh token to the server
  • The server refreshes the token and sends back a new auth token
  • The app completes the initial request with the new auth token

This seems like a lot of steps. The tricky part here is sending the original failed request when the auth token has been refreshed.

Option 2

The other option for was to have the app control when the token is refreshed:

  • User goes to send invalid auth token
  • App notices the auth token is expired by decoding the JWT
  • App sends request for a new auth token by sending the refresh token
  • Once app receives the new auth token, the initial request is initiated
  • (The server will need to have some sort of fallback in case the app fails to recognise the expired token)

This second option is easy to implement in React Native when using Redux using Middleware.

In Jan 2018 I updated this post so instead of using the second option above, I have moved over to the first option. Although this was a little tougher to develop, it paid off as the user does not notice any token refreshing being done at all.

I’ll include the old route and the new route below:

// middleware.js - OLD SOLUTION

import { isTokenExpired, refreshToken } from '../modules/auth/auth.service';
import * as AuthReducer from '../modules/auth/auth.reducer';

export const jwt = store => next => action => {
  if (typeof action === 'function') {
    var theState = store.getState();
    if (
      theState.auth &&
      theState.auth.authToken &&
      isTokenExpired(theState.auth.authToken)
    ) {
      if (!theState.auth.tokenRefreshing) {
        store.dispatch({ type: 'REFRESHING_TOKEN' });
        store.dispatch(refreshToken(theState.auth.refreshToken));
      } else {
        ... //Queue up requests and perhaps action them once the token is refreshed
      }
    }
  }
  return next(action);
};

// middleware.js - NEW SOLUTION

import { refreshToken } from '../modules/auth/auth.service';
import * as AuthReducer from '../modules/auth/auth.reducer';

let buffer = [];

export const jwt = store => next => action => {
  buffer.push(action);
  if (action.type === 'INVALID_TOKEN') {
    let theStore = store.getState();
    if (theStore.auth && theStore.auth.authToken) {
      if (!theStore.auth.pendingRefreshingToken) {
        store.dispatch({ type: 'REFRESHING_TOKEN' });
        let pos = buffer.map(e => e.type).indexOf('INVALID_TOKEN') - 1;
        let previousRequest = buffer[pos];
        store
        .dispatch(refreshToken(theStore.auth.refreshToken, previousRequest))
        .then(resp => {
          buffer = [];
        });
      }
    }
  } else {
    if (buffer.length > 20) {
      //remove all items but keep the last 20 which forms the buffer
      buffer.splice(0, buffer.length - 20);
    }
    return next(action);
  }
};

As mentioned above, this was a little more difficult than the second option which I’d gone with previously as when a request fails due to an invalid token, the request needs to be sent again after the token is refreshed. This is difficult because using Redux middleware many async actions are sent in between the original request and getting the new token back.

A solution is to create a buffer array which holds the last 20 actions. Then once the token is refreshed, the buffer is used to get the action which is in the array position before the INVALID_TOKEN action type, and re-send this.

Improvements and considerations

The project is by no means a fully functioning and complete setup. In the posts I’ve left out a lot of error handling and other supporting functionality, but all this extra code can be viewed on Github as I’ve mentioned.

There are some parts worth thinking about though:

Storing secure data on device

The refresh token and auth token are both stored on your device using React Native AsyncStorage. This is not recomended in production as the data is stored on a part of the device which can be accessed by any other app, or people who reverse engineer the app. With these tokens, people can access the server on your behalf until you log in again or revoke the access token.

Expiring refresh tokens

Currently the refresh token expiration, although set, doesn’t expire. It’s used as a DB reference (as shown in Part 1 of this post) to simply check it’s there, and if it is the system will generate a new auth token.

Revoking refresh token

Similarly, one of the benefits of the JWT/refresh token combination is having the ability to revoke access when required. Say for example if the user wants to sign out of all devices, there would need to be functionality to remove the refresh token from the database so the auth token can’t be refreshed.

Stateless authentication is not for every situation

If the user revokes the refresh token, or user resets their password for example, this will invalidate the refresh token so a new auth token can’t be refreshed. However, the initial auth token (being stateless) will still gain the user access to the system until it expires. This is why a lower expiration time for auth tokens are preferred, because it limits the time that the token will be used if access is revoked.

It is however great if you’re trying to limit the number of authentication related database requests, but there are alternatives.

Alternatives

I found a great post which explains alternatives if the JWT/refresh route is not your cup of tea for what project you’re working on.

Conclusion

Thanks for reading! Please send through any questions or if you spot a mistake somewhere let me know.