Jay Gould

Using JSON Web Tokens and refresh tokens with React Native, Redux and Node JS - Part 1

July 18, 2017

This is Part 1 of a two part post about using JSON web tokens with a React Native app using Redux and a Node Express server. Part 2 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.

Updated June 2018 to update code snippets to bring up to date with the Git repo, and add in a small section for error handling on the server.

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

JSON Web Tokens

Introduction

JSON Web Tokens (JWT) are an authentication method which I’ve integrated into my React Native, Redux and Node server project recently so I wanted to share the result of my research as there were a few parts I struggled to find examples of when developing.

There are a fair few options for authentication when using web apps - one being the classic session based authentication used on many server-side rendered PHP websites. This method is great for standard websites, as the session token is checked on each page request to keep the user logged in, but it’s a little different when it comes to large scale or mobile apps.

JWTs shine when used in mobile apps or complex web apps as they provide “stateless authentication”, meaning the server doesn’t need to maintain the state of the logged in user. The JSON web tokens expire in a set amount of time and the user is required to log in again to get a new token. The protected areas of the server which require authorization use the JWTs to grant access, taking the load away from database auth checks. This is especially useful with apps that have large user base and/or many incoming connections, as there’s not a constant need to hit the database to authenticate a user/request.

Auth Token and Refresh Token

The stateless authentication aspect of the JWT is one of the main benefits of JSON web tokens, but mobile apps most often keep users logged in for a long amount of time (someones indefinitely). We may also want to revoke access and have some control over the authentication system, so stateless JWTs by themselves may not always be the best option. One way to get around this is by using refresh tokens.

Refresh tokens are stored in the database and the users device, alongside the auth token, and when validated against the database are used to generate a new auth JWT for the app. The auth token is sent to the server on each request and when that expires, the refresh token is sent to the server and if it matches refresh token in the database, the system will generate a new JWT. If the refresh token has expired though, the user should be logged out of their account and asked to log in again, gaining new tokens.

When the refresh token has expired, it’s also possible to simply give the user another refresh token, although this may be a bit of a security concern as anyone with access to a refresh token could keep logged in for as long as they want.

Refresh tokens therefore need have a longer expiration than the JWTs (obviously), and don’t necessarily need to be JSON web tokens (although mine are in this example). They act as a kind of session token, which means they can be revoked at any time, forcing the user to log in again, while still giving the user the benefits of the short lived JWT.

There’s an excellent Hacker News thread which covers the benefits of using both auth token and refresh token together if you want to read more.

Node server

Anyway, firstly let’s discuss the server side API.

I have recently updated this post to include some missing features and I’ve included as much new code here as I can such as validation, password hashing and better code structuring. I’ve also added Mongoose and separated the database models from the rest of the main code.

I have one package installed to assist with the creating and validating of the tokens - jsonwebtoken. When the user signs up to the app their details are added to the database. The authToken and refreshToken are also created and sent back to the app.

The majority of the code for the authentication on the server is split into 2 places:

  • controllers/auth.api.js which contains the endpoints for the API, and the top level logic.
  • controllers/auth.js which contains the associated functions to help with creating, validating, testing and refreshing the auth token, as well as database operations (reading/writng/updating).

Signup

// auth.api.js
...

const jwt = require('jsonwebtoken');

router.post('/signup', (req, res) => {
  auth
    .registerUser(
      req.body.first,
      req.body.last,
      req.body.email,
      req.body.password
    )
    .then(user => {
      auth.logUserActivity(user, 'signup');
    })
    .then(() => {
      res.send({
        success: true
      });
    })
    .catch(err => {
      return errors.errorHandler(res, err);
    });
});

// auth.js

let registerUser = (first, last, email, password) => {
  return new Promise((res, rej) => {
    if (!first || !last || !email || !password) {
      return rej('You must send all details.');
    }
    return Users.find({
        email: email
      })
      .then(user => {
        if (user.length > 0) {
          return rej('A user with that username already exists.');
        }
        let passwordHash = bcrypt.hashSync(password.trim(), 12);
        let newUser = {
          first,
          last,
          email
        };
        newUser.password = passwordHash;
        res(Users.create(newUser));
      })
      .catch(err => {
        rej(err);
      });
  });
};

let createToken = user => {
  return jwt.sign(_.omit(user.toObject(), 'password'), config.secret, {
    expiresIn: '10s' //lower value for testing
  });
};

let createRefreshToken = user => {
  //It doesn't always need to be in the /login endpoint route
  let refreshToken = jwt.sign({
    type: 'refresh'
  }, config.secret, {
    expiresIn: '20s' // 1 hour
  });
  return Users.findOneAndUpdate({
      email: user.email
    }, {
      refreshToken: refreshToken
    })
    .then(() => {
      return refreshToken;
    })
    .catch(err => {
      throw err;
    });
};

You’ll see the createRefreshToken() function saves the refresh token in the database so, as mentioned above, it can be later used to send the user a new auth token. These createToken() and createRefreshToken() functions are also used when the user logs in (below).

The jwt.sign method creates the JWT. The first parameter in my example allows information to be added to the token. This may include the user ID, email, or user level for example. As I have recently added Mongoose, I’ve used the toObject() function to turn the Mongoose user model into a usable JSON object so the password can be taken out of the token payload. This is important as without using this function, the token will be sent to the user will the hashed password.

The second parameter is the secret key, which actually validates the token once it’s used to gain access to a protected area. Once a token is generated, it can be decoded on the (JWT) website, and you’ll see an area to add a secret key which shows it’s validation state.

The third parameter is the expiration time. In my example here it’s set to 1 minute, and the refresh token is set to 90 days.

Login

Once the user has signed up, the details will be stored in a database somewhere. I used Heroku and mLab for this project, but there are some other great alternatives.

The login process will generate another auth token and send back to the user. In my example I’ve added logic so if the user doesn’t have a refresh token, it will send one back, however they should always have one when logging in, valid or not, but this is just for testing so I’ve left it in as you may find it useful.

//auth.api.js

...
const jwt = require('jsonwebtoken');

router.post('/login', (req, res) => {
  auth
    .loginUser(req.body.email, req.body.password)
    .then(user => {
      let authToken = auth.createToken(user);
      let refreshToken = auth.createRefreshToken(user);
      let userActivityLog = auth.logUserActivity(user, 'login');
      return Promise.all([
        authToken,
        refreshToken,
        userActivityLog
      ]).then(tokens => {
        return {
          authToken: tokens[0],
          refreshToken: tokens[1]
        };
      });
    })
    .then(success => {
      res.send({
        success: true,
        authToken: success.authToken,
        refreshToken: success.refreshToken
      });
    })
    .catch(err => {
      return errors.errorHandler(res, err);
    });
});

//auth.js

let loginUser = (email, password) => {
  return new Promise((res1, rej1) => {
    if (!email || !password) {
      return rej1('You must send the username and the password.');
    }
    return Users.findOne({
        email: email
      })
      .then(user => {
        if (!user) return rej1('No matching user.');
        return new Promise((res2, rej2) => {
          bcrypt.compare(password, user.password, (err, success) => {
            if (err) {
              return rej2(
                'The has been an unexpected error, please try again later'
              );
            }
            if (!success) {
              return rej2('Your password is incorrect.');
            } else {
              res1(user);
            }
          });
        });
      })
      .catch(err => {
        rej1(err);
      });
  });
};
...

When the user logs in, the auth and refresh tokens will be sent back to the client. These must then be stored in the React Native app (which I’ll cover in my next post). The whole point of getting the auth token is so it can be used in a stateless way to grant a user access to restricted areas.

Restricting access

Ok so what about protecting an endpoint in our API? There will be a signup and login endpoint which are obviously not to be protected. However we may want to make an endpoint to get all users from the database, for example. That may look something like this:

// auth.api.js

// anything below this point will use the custom middleware to check the token. If invalid, it will return an error.

router.use((req, res, next) => {
  var token = req.headers["authorization"]
  token = token.replace("Bearer ", "")
  return jwt.verify(token, config.secret, (jwtErr) => {
    if (jwtErr) {
      return errors.errorHandler(
        res,
        "Your access token is invalid.",
        "invalidToken"
      )
    } else {
      next()
    }
  })
})

router.post("/getAll", (req, res) => {
  Users.find()
    .then((users) => {
      res.status(201).send({
        success: true,
        message: users,
      })
    })
    .catch((err) => {
      return errors.errorHandler(res, err)
    })
})

As shown here, the jsonwebtoken plugin is used to protect the routes below the middleware above using the stored secret key to validate the incoming token. The invalidToken parameter is passed to a new error handler function which is included in the repo, as this keeps the code nice and clean when dealing with error messages and updating error functionality at any time.

One final thing to mention is that the token is sent from the app in the Authorization header, which I’ll cover below. The token can be sent in the body of the request and validated using the jwt.verify() method manually, but the process outlined above seems better to me and separates the token nicely.

Refreshing the auth token

As discussed before, the auth token will expire after a set amount of time. The refresh token is then used to get a new auth token for future requests to the restricted area.

//auth.js

...

router.post('/refreshToken', (req, res) => {
  auth
    .validateRefreshToken(req.body.refreshToken)
    .then(tokenResponse => {
      return auth.createToken(tokenResponse);
    })
    .then(authToken => {
      res.status(200).send({
        success: true,
        authToken: authToken
      });
    })
    .catch(err => {
      if (err.code) {
        return errors.errorHandler(res, err.message, err.code);
      } else {
        return errors.errorHandler(res, err.message);
      }
    });
});

let validateRefreshToken = refreshToken => {
  if (refreshToken != '') {
    return new Promise((res, rej) => {
      jwt.verify(refreshToken, config.secret, err => {
        if (err) {
          rej({
            code: 'refreshExpired',
            message: 'Refresh token expired - session ended.'
          });
        } else {
          Users.findOne({
              refreshToken: refreshToken
            })
            .then(user => {
              res(user);
            })
            .catch(err => {
              rej(err);
            });
        }
      });
    });
  } else {
    throw 'There is no refresh token to check.';
  }
};

The auth token will be used on the app until it expires. Once expired, the refresh token will be used on the server to send a new auth token back to the user.

Handling errors

Finally, I have recently updated my boilerplate repo to include a more elegant error handling solution. All errors from .catch() blocks returned from promises pass through an error handling function:

//error.js

const errorHandler = (res, errorMessage, errorCode) => {
  if (errorCode === "invalidToken" || errorCode === "refreshExpired") {
    return res.status(403).send({
      success: false,
      message: errorMessage,
      code: errorCode,
    })
  } else {
    return res.status(400).send({
      success: false,
      message: errorMessage,
    })
  }
}

The error handling allows all error types to be caught in one place and a response formulated there.

To be continued…

Ok that covers the first part of this two part post. The next one will discuss the React Native and Redux part of using the JWTs for authentication.


Senior Engineer at Haven

© Jay Gould 2023, Built with love and tequila.