Social Login using Passport.js in Node.js.

What is Passport.js?

Passport is authentication middleware for Node.js. As it’s extremely flexible and modular, Passport can be unobtrusively dropped into any Express-based web application. A comprehensive set of strategies supports authentication using username and password, Facebook, Twitter and more.

The objective of this article

In this article, we will learn how to use passport.js for authentication and to Implement Multiple Social Logins using Passport.js.

We will create a project that will Authenticate/Register the user using Social Logins like Facebook, Twitter, and Google.

Setting up a Basic Express App.

As this article focuses only on authentication parts with passport.js. We would skip explaining this basic app. You can view the entire code and its structure here.

Run the below commands to create a node project and install few dependencies

npm init
npm i express mongoose passport session --save

App.js

var express = require('express');
var path = require('path')
var mongoose = require('mongoose');
var passport = require('passport');
var session = require('express-session');

var app = express();
const PORT = 3000;

mongoose.connect("mongodb://localhost/passport", {useNewUrlParser : true}).then(()=>{
    console.log("Connected to mongoDB");
})

app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');
app.use(session({
    secret: 'keyboard cat',
    resave: true,
    saveUninitialized: true,
}))

app.get('/', function (req, res){
    res.render('login');
})

var authRoutes = require('./routes/auth');

app.use('/auth', authRoutes)
app.listen(PORT,function(){
    console.log("SERVER STARTED")
})

The login page that is rendered on the home page simply contains links or buttons that users would click to trigger authentication. From here on we will majorly be working inside the authRoutes but before we start implementing passport lets start by creating a User model.

Login Page

<a href="/auth/google">Login with Google</a><br>
<a href="/auth/twitter">Login with Twitter</a><br>  
<a href="/auth/facebook">Login with Facebook</a>

User Model

var mongoose = require('mongoose');

var UserSchema = mongoose.Schema({
    provider:{type: String},
    providerid : {type: String}, 
    picture:{type:String},
    name: { type: String, required: true },
    email: { type: String, required: true },
});

module.exports = mongoose.model('User', UserSchema);

In the user model, I have decided to save the provider i.e., mode of authentication. Feel free to change the schema as per your requirements.

Auth.js

Our auth.js file will have all the authentication route and passport code that we would need to implement. There are only two routes that are needed

  • /:provider
    Provider here means the authentication strategy that is being used for example it could be google, Facebook, twitter etc. You could have implemented a different route for each strategy
  • ./:provider/callback
    The redirect URL each strategy will redirect to after the authentication.

Remember that any routes in this file will be appended to /auth route as we have specified it in our app.js file

Getting the KEYs and SECRETs For Authentication

Get the API key and secret for any login provider that you would want to use. We would be using Facebook, Google, and Twitter. To obtain keys go to the links provided below for each login provider. In all cases, you need to create an app.

Auth.js code

var express = require('express');
var router = express.Router();
var passport = require('passport');
var User  = require('../models/User');
require('dotenv').config()

passport.serializeUser(function (user, done) {
    done(null, user._id);
});
passport.deserializeUser(function (id, done) {
    User.findById(id, function(err, user) {
		done(err, user);
	});
});

var setAuthProvider = (req, res, next)=>{
    try{
        req.params.provider = req.params.provider.toLowerCase();
    if(req.params.provider == 'google'){
        var GoogleStrategy = require( 'passport-google-oauth2' ).Strategy;
        passport.use('google',new GoogleStrategy({
            clientID:  process.env.GOOGLE_CLIENTID   ,
            clientSecret: process.env.GOOGLE_CLIENTSECRET,
            callbackURL: "http://localhost:3000/auth/google/callback",
            passReqToCallback   : true
          },
           function(request, accessToken, refreshToken, profile, done) {
              console.log("PROFILE", profile)
              if(profile && profile.email){
                 checkUser(profile, done);
          }else{
              res.redirect('/')
          }
        }
        ));
        next()
    }else if(req.params.provider == 'twitter'){ 
        var TwitterStrategy = require('passport-twitter').Strategy;
        console.log(process.env.TWITTER_CONSUMERKEY)
        passport.use('twitter',new TwitterStrategy({
            consumerKey:  process.env.TWITTER_CONSUMERKEY   ,
            consumerSecret:process.env.TWITTER_CONSUMERSECRET,
            callbackURL: "http://localhost:3000/auth/twitter/callback",
            userProfileURL: "https://api.twitter.com/1.1/account/verify_credentials.json?include_email=true",
            passReqToCallback   : true
          },
           function(request, accessToken, refreshToken, profile, done) {
              console.log("PROFILE", profile)
              if(profile){
                 checkUser(profile, done);
          }else{
              res.redirect('/')
          }
        }
        ))
        next();
    }else if(req.params.provider == 'facebook'){ 
        var FacebookStrategy = require('passport-facebook').Strategy;
        passport.use('facebook',new FacebookStrategy({
            clientID:  process.env.FACEBOOK_CLIENTID   ,
            clientSecret:process.env.FACEBOOK_CLIENTSECRET,
            callbackURL: "http://localhost:3000/auth/facebook/callback",
            profileFields: ['id', 'displayName', 'profileUrl', 'name', 'gender', 'birthday', 'photos', 'emails'],
            passReqToCallback   : true
          },
           function(request, accessToken, refreshToken, profile, done) {
              console.log("PROFILE", profile)
              if(profile){
                 checkUser(profile, done);
          }else{
              res.redirect('/')
          }
        }
        ))
        next();

    }else {
        res.send(406)
    }

}catch(err){
    res.send(406, err)
}
}

var checkUser = function(profile, done){
    User.findOne({email:profile.email},function(err, user){
        if(err){
            throw new Error(err);
        }
        if(user){
            console.log(user)
            return done(err, user)
        }else{
        var newUser = new User({
            provider:profile.provider,
            providerid:profile.id,
            picture: profile.photos[0].value ||  profile._json.picture ,
            name: profile._json.name ,
            email: profile._json.email
        })
        newUser.save(function(err){
            if(err){
                throw new Error(err);
            }else{
                 return done(err, newUser);
            }
        })
    }
    })
}
var providerCallback = function(req, res, next){
    passport.authenticate(req.params.provider,{ successRedirect: '/profile', failureRedirect: '/' })(req, res, next); 
}
router.get('/:provider', setAuthProvider, function(req, res){
    var scopes = [];
    if(req.params.provider == 'google'){
     scopes =  [ 'https://www.googleapis.com/auth/userinfo.profile', 'https://www.googleapis.com/auth/userinfo.email' ]
    } 
    if(req.params.provider == 'facebook'){
        scopes = ['email']
    }
    passport.authenticate(req.params.provider, { scope: scopes })(req, res);
})
router.get('/:provider/callback', providerCallback)
module.exports  = router;

Understanding Auth.js

When the user clicks on one of the links in login page. It results in the execution of our passport authenticate middleware now in our setAuthProvider middleware we define all the strategies that we would use. The authenticate function uses the provider parameter present in params to decide which strategy to use(Reason why we decided to have a single route).

The CheckUser function does two things, First, it checks whether the user received from the authentication strategy exists or not. if it does then simply call the done(an internal passport function that in case a user is received attaches that user to the req object) with that user.If the user does not exist it First created that user and then calls done.

In the callback, we have a middleware that might look exactly the same as the one we initially set before Both of these calls the passport.authenticate method The difference between the two is that the first one just checks whether it has that strategy that is specified as the first parameter if not then it will throw an Unknown strategy error else would continue with our flow. While the passport.authenticate in the callback function uses the code it received from the provider to get the OAuth tokens.

Understanding Passport Strategies

Strategies are used to authenticate requests. Each of these Strategies is a different node package altogether for example In this tutorial we have used Facebook, Google and Twitter as login Provider. So we have different strategies for each of these login providers and you need to install each of these packages. Each of these strategies needs to be configured before they could be used. for example, In the case of Google, we only needed to give it the Client Id and Secret that you created and the callback URL which google will return to after authentication.

in case of Facebook, we have an additional profile field which basically is the scope of authentication i.e., what data from the user would we want to get(keep in mind user will be shown whatever scope you mention before the authentication).

Twitter Strategy is no different with exception that it has a field called userProfileURL. which we have used to get the email address of the user.

Understanding Passport Serialize and Deserialize and Done function

The Serialize and Deserialize methods are used to set id as a cookie in the user’s browser and to retrieve the id from the cookie when it then used to get user info in a callback.

The done function is an internal function of passport.js and the user id provided as the second argument of the done function is saved in the session which is used to get the whole object using deserialize function.

Passport middleware in App.js

Add the code below the session middleware in the app.js

app.use(passport.initialize());
app.use(passport.session());

passport.initialize method as the name suggests is a middle-ware that initializes the Passport authentication module.

passport.session middleware is responsible for altering the request object and changing the ‘user’ value that is currently the session id into the deserialized user object.

You can get the entire code here.

Leave a Reply