Auth and OAuth in Node/React App

This is a high-level summary of how I got auth and oauth working on my node/react app, Quizipedia. My goal was to offer a local login system (where a user could use their email and a custom password) and also a social login system (where a user could use their google and/or facebook credentials to log in – aka OAuth).

Quick summary of the app. There are 2.5 parts. First, there is a Node API sitting at the backend which connects with a postgres database. From an auth point of view, this is where user info gets stored, including hashed passwords, and it can be accessed using various calls to the API. This is also where all the calls and logic for the rest of the app functionality lives, but we aren’t concerned with that today. Above the API is the node-react-app. This was created with create-react-app, which is why it counts for 1.5 parts: It has both server code and client code, but it is in the same project, and when it is deployed to production (using Heroku), they get combined into one piece. Ergo 1.5 parts. In this post I will be discussing the server parts.

Backend API

The auth-related code in the backend API is probably the simplest to understand. It is just a set of various calls to get and post information about a user, such as:

  • Get user by id (This is a unique GUID generated in the code)
  • Get user by email
  • Get user by social network and token (More on this later)
  • Post login credentials in an attempt to login
  • Post a new user
  • Post a new social user

So there are two resources working here: the user and the social user. Someone may only want to use the local login system without any ties to a social network. This means they will just get an entry in the ‘users’ table, which contains the unique GUID, their email, a hashed password, and join_date – fairly minimal for now. If they decide later that they want to attach a social network, then they will get an entry in the ‘social_users’ table, which contains a foreign key for the user’s unique id, the name of the social network (facebook or google), and the social network token. This token is provided by the social network itself, and uniquely identifies the person’s social network account. I will go into more detail in the next section. So the API itself is fairly straightforward. It is mostly CRUD operations with a small amount of business logic. This includes verifying if a user already exists, as well as verifying login credentials.

React app – Server part

Note that this part does not contain any React code. All the React code is in the client part of the React App. The server part is similar to the backend API in that it uses Node and Express. The difference is that the backend API uses ES6 conventions and uses Babel to transpile it. I did not do the same on the Server part of the React app due to the extra complexity that would be involved in fiddling with some of the built-in functionality that comes with react-app. This was fine because, as a whole, there is much less code in this part than the backend API. In fact, the only major code in here relates to auth itself, and there are three parts. The handlers, the thin library layer, and the passporting. The handlers represent the auth endpoints that can be called right from the app, such as ‘login’, ‘signup’, ‘social login’, and ‘logout’. It also contains a few special handlers for dealing with the social logins, which we’ll get to. The thin library is essentially a wrapper for calling the backend API – For each auth-related API call mentioned above, there is a function in the library to just call the api and return the data – no mapping or filtering – just calls the API. The interesting section is the passporting. This file contains the main auth logic, for both local login and social login. The passport.js documentation is invaluable here, so I won’t repeat any of it. The basic idea is that once you add passporting to your project (done in the index.js file of this part of the project), you just define various middleware authentication functions that get called on the auth handlers.

For example, here is what the login function looks like:

Here is the handler:

app.post(
'/auth/login',
passport.authenticate('login', {
  failureRedirect: '/auth/loginfail',
  failureFlash: true,
}), (req, res) => {
  res.status(200).send(req.user);
});

So the passport ‘login’ function acts as a middleware for the handler. If the login is successful, it just returns the user, which gets added to the req object. If it fails, then it sends a message (using the connect-flash library) and redirects to a failure route. Here is the middleware function:

const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;
const keys = require('../config/keys');
const authLib = require('./authLib');

...

passport.use('login', new LocalStrategy(
{
  usernameField: 'email',
  passwordField: 'password',
  passReqToCallback: true,
},
(req, email, password, done) => {
  return authLib.getUserByEmail(email).then((user) => {
    authLib.login(email, password).then((login) => {
      if (login) {
        return done(null, user);
      }
      return done(null, false, { message: 'Incorrect password' });
    }).catch((err) => { throw err; })
  }).catch(err => done(null, false, { message: 'Email address not found' }));
}));

So this ‘use’ function acts as a middleware when the login handler is called. The LocalStrategy is another library that must be imported from passport.js. So as you might expect with a login call, the first step is to check if the user’s email exists. This is done by the backend API via the auth lib. If it does not, then return a message saying it does not exist. This will be displayed to the user. If the email does exist, then the login credentials are again sent to the backend API, where the password is hashed and compared with the stored hashed value (uses the bcrypt library). Again, if this fails, the appropriate message is sent to the user. If it succeeds, then the user gets returned in the ‘done’ function, which is a convention of passport.js.

The social logins are a little bit trickier. We’ll use Google as an example. In order to get an app to use Google’s authentication, you need to add your app on Google’s developer console. The first step is to define a handler that will redirect to Google’s login acceptance page. You also need to specify the scope of the data you want. All I care about for this application is the profile and the email address. The profile is required to get the unique profile id for a user so that it can be stored on our db to make sure that we know the user has used google’s social login on the app before. We also need to define a callback handler, which is what we tell Google to redirect to once the user has accepted on the Google login page. The handlers themselves are very straightforward:

app.get('/auth/google', passport.authenticate(GOOGLE, {
  scope: ['profile', 'email'],
}));

app.get('/auth/google/callback', passport.authenticate(GOOGLE), (req, res) => {
  res.redirect('/');
});

The authentication middleware for the Google login is a bit more involved:

passport.use(GOOGLE, new GoogleStrategy(
{
  clientID: keys.googleClientID,
  clientSecret: keys.googleClientSecret,
  callbackURL: '/auth/google/callback',
},
(accessToken, refreshToken, profile, done) => {
  const token = profile.id;
  const email = profile.emails[0].value;
  return socialStrategy(GOOGLE, token, email, done);
}));

The Google strategy from passport.js requires the app Id and secret, both of which are created when you add your app to the Google developer console. The callback URL is the one we mentioned above. So this function will redirect your browser to Google’s login screen, where the user confirms their login with a Google account. It returns some standard data, but we’re interested in the profile, which contains the unique Id and the email address, both of which we will store. The socialStrategy function is used so that we can reuse functionality for the Facebook login. Here it is:

function socialStrategy(network, token, email, done) {
  let userId;
  let message;
  return authLib.getUserBySocialNetworkAndToken(network, token)
    .then(existingUser => done(null, existingUser))
    .catch((err) => {
      if (err.response.status !==404) {
        return done(err);
      }

      // if email already exists, then associate; Otherwise add new
      return authLib.getUserByEmail(email).then((checkEmail) => {
        ({ userId } = checkEmail);
        message = 'associated existing';
        return authLib.linkSocial(userId, { network, token }).then(() => {
          const result = {
            userId, network, token, message,
          };
          return done(null, result);
        });
      }).catch((err) => {
      if (err.response.status !==404) {
        return done(err);
      }
      return authLib.createSocialUser({ network, token, email })
        .then((newUser) => {
          ({ userId } = newUser);
          message = 'created new';
          const result = { userId, network, token, message };
          return done(null, result);
        }).catch(err => done(err));
      });
   });
}
This checks if the user has previously logged in with Google by comparing the token and network with what is in the db. If it’s found, then we’re done. If not, then we check if the user has a local login with the app. If a user has previously signed up locally with the same email that is tied to their social account, then there will be a conflict. This ensures that the social login is just LINKED to their local login user id, rather than create a new one. So if they have an email, then the account is linked. If they do not, then it creates a brand new user.
Once all of this is done, passport adds the user to the req object, and redirects to the specified callback. In our case, this just redirects to the root path. So the user is now logged in!
Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s