Authenticating Users in a Node App using Express & Passport (Part Two)

. Posted in Express, Web.
post-banner

In the last article I built a simple user authentication Node app using the Express framework and the Passport middleware service. This article is the second, and final, part where I will be adding user authentication from third party accounts, Facebook, Twitter and Google+.

You will need to have read the previous article to get started, or you could even clone the local branch from my GitHub repo.

Getting Started

To get started, cd into the directory you were working on from the last article.

$ mongod # This will start a MongoDB instance
# Open up a new terminal tab
$ node app

And then head over to http://localhost:3000 and the application should display the index screen that was created in the previous article.

index

Configuring Developer APIs

To be able to use the Passport strategies for social accounts, we need to configure the APIs for each of them to allow our app to access the necessary data. In my opinion, this was the most difficult part of creating the app. But once you have successfully navigated through their developer consoles to locate the 'create an app' section, it's fairly simple from then on.

First, let's create a new file /config/auth.js to store the credentials we need for logging in with these accounts.

module.exports = {  
  facebookAuth: {
    clientID: 'YOUR-FB-CLIENT-ID',
    clientSecret: 'YOUR-FB-CLIENT-SECRET',
    callbackURL: 'http://localhost:3000/auth/facebook/callback',
  },
  twitterAuth: {
    consumerKey: 'YOUR-TWITTER-CONSUMER-KEY',
    consumerSecret: 'YOUR-TWITTER-CONSUMER-SECRET',
    callbackURL: 'http://localhost:3000/auth/twitter/callback',
  },
  googleAuth: {
    clientID: 'YOUR-GOOGLE-CLIENT-ID',
    clientSecret: 'YOUR-GOOGLE-CLIENT-SECRET',
    callbackURL: 'http://localhost:3000/auth/google/callback',
  },
};

When we have successfully configured the app with the relevant APIs, we will be able to access the client id and client secret, which will be added to this file. Setting the callback URL's to http://localhost:8080/auth/<service>/callback will ensure the user is redirected back to our app when the user completes the OAuth authentication flow for that service. The callback URL is used to handle approved or rejected authentications, and make sure the service is redirected back to the correct place.

This stage is a bit long-winded, but important, so if you have any troubles feel free to leave a comment or tweet me and I'll get back to you.

Facebook Developer API

So let's start with the Facebook API. Navigate to https://developers.facebook.com/apps/ and click 'Add a New App'. You will need a registered Facebook account for this. You should then be presented with a list of app platforms, select 'website'.

fb-api

Next you will be asked to name your app, and then to select a category as well. Facebook will then present you with a quick start tutorial, but for now we can skip this by clicking the 'Skip Quick Start' button.

The next screen will present you with the credentials we need, so simply copy the relevant data into our config/auth.js file.

Twitter Developer API

The Twitter API is a little different. To be able to use it, you need to make sure your mobile number is registered to your Twitter account. To start, simply navigate to https://apps.twitter.com/ and click 'Create New App'.

twitter-api

You will be presented with a form, asking you to prove the name of your app, a simple description and the application's website. You can simply set a placeholder here, or even just your personal domain. You can also set the callback URL, which we've set in our config file to be http://localhost:3000/auth/twitter/callback. Twitter may have a problem with this callback URL, in which case you can simply use http://127.0.0.1:3000/auth/twitter/callback.

When you have completed the form, agree to the terms of use and submit your application. You should be displayed with the information relevant to your app. To find our Twitter consumer key and secret, click on the 'Keys and Access Tokens' tab, and copy them across to the config file to complete this section.

Google Developer API

So, last API to configure, Google. Navigate to https://console.developers.google.com/project and click the 'Create Project' button. Again, you will need a Google account to do so.

google-api

You will then be asked to name your project, and be redirected to the API dashboard. To find your credentials, click 'Enable and Manage APIs' and scroll down to the 'Google+ API'. Click 'Enable', navigate to 'Credentials' in the side bar and add 'OAuth 2.0 client ID'. Here you will be able to configure the screen users will be presented with as they log in, and find the credentials to copy over to the code.

Update User Schema

Now that we have the configurations out of the way, we can get back to the code. The next stage is to update the databases user schema to include the data we retrieve from logging in with a third party account.

Each service will provide us with slightly different information, which we need to take into account when setting the user schema. For example, Twitter doesn't provide users email addresses, where as Facebook and Google does.

Let's open up the /models/user.js file and update the schema.

...
var userSchema = mongoose.Schema({  
  local: {
    name: String,
    email: String,
    password: String,
  },
  facebook: {
    id: String,
    token: String,
    email: String,
    name: String,
    username: String,
  },
  twitter: {
    id: String,
    token: String,
    displayName: String,
    username: String,
  },
  google: {
    id: String,
    token: String,
    email: String,
    name: String,
  },
});
...

Depending on the users choice of authentication method, we will be storing the data to reflect that. We can then display this information in the 'profile' view to make sure that the authentication has worked correctly.

Add Passport Strategies

Now we can finally start to work with Passport again to use the services strategies for each method of authentication.

We will need to install the passport strategies for each service through npm and save them to the package.json.

$ npm install passport-facebook --save
$ npm install passport-twitter --save
$ npm install passport-google-oauth --save

Next, let's open the config/passport.js file and include these dependencies, along with config/auth.js at the top of the file. For Google we are going to the using the OAuth strategy, as Google no longer supports the passport-google plugin.

var LocalStrategy = require('passport-local').Strategy;  
var FacebookStrategy = require('passport-facebook').Strategy;  
var TwitterStrategy = require('passport-twitter').Strategy;  
var GoogleStrategy = require('passport-google-oauth').OAuth2Strategy;  
var User = require('../models/user');  
var configAuth = require('./auth');  

Once this is all setup we can move on to building the actual strategy code to authenticate a user. These are relatively similar to the functions created in the last article, but they will use the data from the relevant APIs.

...
passport.use(new FacebookStrategy({  
    clientID: configAuth.facebookAuth.clientID,
    clientSecret: configAuth.facebookAuth.clientSecret,
    callbackURL: configAuth.facebookAuth.callbackURL,
    profileFields: ['id', 'email', 'first_name', 'last_name'],
  },
  function(token, refreshToken, profile, done) {
    process.nextTick(function() {
      User.findOne({ 'facebook.id': profile.id }, function(err, user) {
        if (err)
          return done(err);
        if (user) {
          return done(null, user);
        } else {
          var newUser = new User();
          newUser.facebook.id = profile.id;
          newUser.facebook.token = token;
          newUser.facebook.name = profile.name.givenName + ' ' + profile.name.familyName;
          newUser.facebook.email = (profile.emails[0].value || '').toLowerCase();

          newUser.save(function(err) {
            if (err)
              throw err;
            return done(null, newUser);
          });
        }
      });
    });
  }));
...

This is the Facebook authentication strategy. It will retrieve the client ID, client secret and callback URL that we set in the configurations, and create a new user document in the database containing the user id, token, name and email from their Facebook account.

...
passport.use(new TwitterStrategy({  
    consumerKey: configAuth.twitterAuth.consumerKey,
    consumerSecret: configAuth.twitterAuth.consumerSecret,
    callbackURL: configAuth.twitterAuth.callbackURL,
  },
  function(token, tokenSecret, profile, done) {
    process.nextTick(function() {
      User.findOne({ 'twitter.id': profile.id }, function(err, user) {
        if (err)
          return done(err);
        if (user) {
          return done(null, user);
        } else {
          var newUser = new User();
          newUser.twitter.id          = profile.id;
          newUser.twitter.token       = token;
          newUser.twitter.username    = profile.username;
          newUser.twitter.displayName = profile.displayName;
          newUser.save(function(err) {
            if (err)
             throw err;
            return done(null, newUser);
          });
        }
      });
    });
  }));
...

The Twitter strategy is very similar, but instead it will retrieve the consumer key, consumer secret and callback URL that we have set for Twitter in the config. From Twitter we can store the user id, token, username and display name in the database.

...
passport.use(new GoogleStrategy({  
    clientID: configAuth.googleAuth.clientID,
    clientSecret: configAuth.googleAuth.clientSecret,
    callbackURL: configAuth.googleAuth.callbackURL,
  },
    function(token, refreshToken, profile, done) {
      process.nextTick(function() {
        User.findOne({ 'google.id': profile.id }, function(err, user) {
          if (err)
            return done(err);
          if (user) {
            return done(null, user);
          } else {
            var newUser = new User();
            newUser.google.id = profile.id;
            newUser.google.token = token;
            newUser.google.name = profile.displayName;
            newUser.google.email = profile.emails[0].value;
            newUser.save(function(err) {
              if (err)
                throw err;
              return done(null, newUser);
            });
          }
        });
      });
    }));

};

And finally the Google strategy, which allows us to retrieve the users id, token, name and email to store in the database when they are authenticated with their Google+ account.

Create New Routes

Now that everything is setup to allow authentication through Facebook, Twitter and Google, we can implement the routes to get the correct authentication strategy and redirect the user to the profile view. As before, the new routes will be defined in the /routes/index.js file below all of the currently configured routes and above the module.exports = router; line of code.

...
// Facebook routes
router.get('/auth/facebook', passport.authenticate('facebook', { scope: 'email' }));

router.get('/auth/facebook/callback', passport.authenticate('facebook', {  
  successRedirect: '/profile',
  failureRedirect: '/',
}));
...

This simple Express code will use the Passport Facebook strategy to authenticate a user, and then redirect them, using the callback URL to the profile view. The routing for Twitter and Google is basically the same, so I'll simply add them below.

...
// Twitter routes
router.get('/auth/twitter', passport.authenticate('twitter'));

router.get('/auth/twitter/callback', passport.authenticate('twitter', {  
  successRedirect: '/profile',
  failureRedirect: '/',
}));

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

router.get('/auth/google/callback', passport.authenticate('google', {  
  successRedirect: '/profile',
  failureRedirect: '/',
}));
...

Update Profile Views

All that is left to do now is update the views to allow the user to click a button to authenticate themselves using the app. We can also add some new markup to the profile page, to display the users information as a visual verification that the app is working.

<!-- views/index.ejs -->  
...
<a href="/login" class="btn btn-default"><span class="fa fa-sign-in"></span> Login</a>  
        <a href="/signup" class="btn btn-default"><span class="fa fa-user"></span> Signup</a>
        <a href="/auth/facebook" class="btn btn-primary"><span class="fa fa-facebook"></span> Facebook</a>
        <a href="/auth/twitter" class="btn btn-info"><span class="fa fa-twitter"></span> Twitter</a>
        <a href="/auth/google" class="btn btn-danger"><span class="fa fa-google-plus"></span> Google</a>

This markup will add some new buttons to the index page, which the user will click to sign up through Facebook, Twitter or Google. It will also include icons from Font Awesome. If you reload the page at http://localhost:3000, you should see updated home page.

newindex

To finish up with, we can update the views/profile.ejs file to display the users data from their chosen authentication method.

<% if (user.local.email != null) { %>  
  <img src="https://avatars.io/gravatar/<%= user.local.email %>/140" class="img-circle"><br>
  <strong>email</strong>: <%= user.local.email %><br>
<% } %>

<% if (user.facebook.id != null) { %>  
  <h2 class="fa fa-facebook"> Facebook</h2><br>
  <img src="https://avatars.io/gravatar/<%= user.facebook.email %>/140" class="img-circle"><br>
  <strong>id</strong>: <%= user.facebook.id %><br>
  <strong>email</strong>: <%= user.facebook.email %><br>
  <strong>name</strong>: <%= user.facebook.name %>
<% } %>

<% if (user.twitter.id != null) { %>  
  <h2 class="fa fa-twitter"> Twitter</h2><br>
  <img src="https://avatars.io/twitter/<%= user.twitter.username %>" class="img-circle"><br>
  <strong>id</strong>: <%= user.twitter.id %><br>
  <strong>token</strong>: <%= user.twitter.token %><br>
  <strong>username</strong>: <%= user.twitter.username %><br>
  <strong>displayName</strong>: <%= user.twitter.displayName %>
<% } %>

<% if (user.google.id != null) { %>  
  <h2 class="fa fa-google-plus"> Google</h2><br>
  <img src="https://avatars.io/gravatar/<%= user.google.email %>/140" class="img-circle"><br>
  <strong>id</strong>: <%= user.google.id %><br>
  <strong>token</strong>: <%= user.google.token %><br>
  <strong>email</strong>: <%= user.google.email %><br>
  <strong>name</strong>: <%= user.google.name %>
<% } %>  

So, the app is finally complete and if you reload the page and attempt to sign up using a third party service, you should be redirected via the service and displayed with a user profile showing your data and avatar. For example, I signed up using my Twitter account:

twitter-auth

And now, if I make a MongoDB query inside the mongo shell, I will be displayed with the correct information from the database.

$ mongo
> db.users.find()
{ "_id" : ObjectId("565b18998ba4442b199b6a0e"), "facebook" : { "email" : "[email protected]", "name" : "Daniel Gynn", "token" : "CAAMvdS5yUNgBAA7XQDSkRWmBwFlpC9usGV7Gl3PzQ8YVnO1wyN8OJHClHIhXZCHq9TxYl1E2xPFtvDDONTZABQiXNWJCd5HAx1YK9jWtuRz9OW6A7CD8ZAyqWzgqtlTxNcUt1MhkKwfEIFmES2rsF284C8ybhRqL7wImq8EdvHEuAIFDd0gF1dRSubxMUsZD", "id" : "952710344775180" }, "__v" : 0 }
{ "_id" : ObjectId("565b18a28ba4442b199b6a0f"), "twitter" : { "displayName" : "Daniel", "username" : "danielgynn", "token" : "2483852947-nLwzZXwuXKMPpsZNApqaOQs8GThe5JbnbORlWXP", "id" : "2483852947" }, "__v" : 0 }

Conclusion

Hopefully this tutorial wasn't too long-winded and was simple enough to complete in around half hour. If you had any trouble throughout either part of this tutorial, feel free to leave a comment or tweet me.

You can find the source code for part 1 and part 2 on GitHub, as well as the full application.

comments powered by Disqus