Sails and Facebook Login using Passport.

Sails is pretty awesome at scaffolding and organizing your NodeJS project. But, when it comes to using some packages it can get messy with all that middleware pieces in between. In this post we'll look at how to configure your Sails app to use Passport for third party authentication. We'll use Facebook as an example and will leverage MongoDB for persistence.

This post is an adaptation of these two sources:

http://www.geektantra.com/2013/08/implement-passport-js-authentication-with-sails-js/

https://github.com/stefanbuck/sails-social-auth-example/tree/master/config

About Sails.

Sails is a framework that "makes it easy to build custom, enterprise-grade Node.js apps." It is designed emulate MVC design patterns common in Ruby on Rails, but with a strong focus on data-driven applications. Creating an application is as easy as running "sails new ApplicationName", which will create a well organized project structure. It does come with a lot of configuration aspects that are unnecessary for small prototype projects, and might introduce complexity to small performance-driven micro-services. That being said, it's pretty awesome. If you haven't tried it, I really recommend it.

About Passport

Passport is a NodeJS strategy-modelled package that simplifies the authentication task in your application. It supports many common strategies; like Facebook, Twitter, Github, Google; and some for services you've probably never heard of.

What day is it today? This is probably outdated.

The NodeJS-verse moves quick. Sometimes too quick; you blink and the code you committed is already deprecated. So if you are following this guide remember that the following might already be too old. This post was written and tested on 2015-10-30. And I used Sails v0.11.2, Passport v0.3.0, and Node v4.2.1.

About MongoDb

We will use MongoDB to persist our data. MongoDB is a non-relational document database designed for fast and easy development and even easier scaling.

Install NodeJS and MongoDB.

If you're starting from scratch you should install NodeJS, and install MongoDB.

Create a new Sails Application.

First, install sails with NPM:

sudo npm -g install sails

Once installed you can use Sails's generators to quickly create files needed for a project. Running "sails new testProject" will create the folder structure and files following Sails anatomy; if you're new to Sails, this is a great start to understand what each folder and file does. Finally you can start your application with "sails lift":

sails new testProject
cd testProject
sails lift

Configure and install dependencies for MongoDB and Passport.

package.json

Edit the package.json file on the root of your application and add the following dependencies:

  "dependencies": {
    ...

    "passport": "^0.3.*",
    "passport-facebook": "^2.*",
    "sails-mongo": "^0.11.*",
    "connect-mongo": "^0.8.*"

    ...
  }

Now, install the dependencies, in the project folder run::

npm install

This will install the dependencies you just specified in the package.json file.

Configure the MongoDB connection and persist Sessions

config/connections.js

We will configure the MongoDB connection in the config/connections.js file. Add or uncomment the following lines. If you have a different MongoDB host, or require a username/password, here is the place to configure it:

module.exports.connections = {
  ...
  mongo: {
    adapter: 'sails-mongo',
    host: 'localhost',
    port: 27017,
    // user: 'username',
    // password: 'password',
    database: 'sails-passport-example'
  },
  ...
}

config/sessions.js

Let's now configure sessions to be stored in MongoDB. We will tell Sails to use the adapter mongo, and to connect to a database called 'sails-passport-example'; we will also tell it to use a collection 'sessions'. Edit the config/sessions.js file and add the following lines:

module.exports.session = {
  ...
  adapter: 'mongo',
  host: 'localhost',
  port: 27017,
  db: 'sails-passport-example',
  collection: 'sessions',
  ...
}

Create a User Model

Let's now create a User model. In Sails that's too easy:

api/models/User.js

We will create the appropriate file with:

sails generate model user

This will create the file api/models/User.js. Let's edit that file and add the appropriate attributes and connection to mongo:

module.exports = {

  connection: 'mongo',

  attributes: {
    provider: 'STRING',
    uid: 'STRING',
    name: 'STRING',
    email: 'STRING',
    firstname: 'STRING',
    lastname: 'STRING'
  }
};

That's it for creating and persisting the User model. Sails will take care of the rest.

/api/controllers/AuthController.js:

The same way we generated the above file we will create the Authorization controller with:

sails generate controller auth

Which will create the /api/controllers/AuthController.js file. Let's edit it:

var passport = require('passport');

module.exports = {

  // login action will render the login view
  login: function (req, res) {
    res.view();
  },

  // logout action will logout using Passport
  // and redirect
  logout: function(req, res) {
    req.logout();
    res.redirect('/home');
  },

  // Here is were we specify our facebook strategy.
  // https://developers.facebook.com/docs/
  // https://developers.facebook.com/docs/reference/login/
  facebook: function(req, res) {
    passport.authenticate('facebook', 
                          { failureRedirect: '/login', scope: ['email'] }, 
                          function(err, user) {
      req.logIn(user, function(err) {
        if (err) {
          console.log(err);
          res.view('500');
          return;
        }
        res.redirect('/home');
        return;
      });
    })(req, res);
  }

};

We have to now bind this controller to URL patterns, let's do that in the routes file..

/config/routes.js

We will add the routes to "/login", "/logout" and "/home". You'll notice we haven't created HomeController, we'll do that later.

module.exports.routes = {
  ...

  '/login': {
    controller: 'AuthController',
    action: 'login'
  },
  '/logout': {
    controller: 'AuthController',
    action: 'logout'
  },
  '/home': {
    controller: 'HomeController',
    action: 'index'
  }
  ...
};

Create your Facebook App.

That's a lot of configuring and typing right there. Since we're half-way let's take a break for Soda and Popcorn. We will need to create a Facebook Application. Visit https://developers.facebook.com/ to create an application. I just create an advanced application and modify its settings.

facebook

We will need to add a "Platform", choose "Web" and add the following url: "http://localhost:1337/". This will be used by Facebook to authorize and redirect to when doing the "OAuth dance". Note the App ID and App Secret, we will use them later when configuring our application.

Back to Sails

Let's continue with our Sails application.

config/express.js.

Now it's time to configure Passport to be used as Authorization middleware. This is the exciting part. We will create a new file "config/express.js" that we will use to configure the Facebook strategy:

var passport = require('passport'),
    FacebookStrategy = require('passport-facebook').Strategy
    //Other strategies go here
;

var verifyHandler = function(token, tokenSecret, profile, done) {
  process.nextTick(function() {

    User.findOne({uid: profile.id}, function(err, user) {
      if (user) {
        return done(null, user);
      } else {

        var data = {
          provider: profile.provider,
          uid: profile.id,
          name: profile.displayName
        };

        if (profile.emails && profile.emails[0] && profile.emails[0].value) {
          data.email = profile.emails[0].value;
        }
        if (profile.name && profile.name.givenName) {
          data.firstname = profile.name.givenName;
        }
        if (profile.name && profile.name.familyName) {
          data.lastname = profile.name.familyName;
        }

        User.create(data, function(err, user) {
          return done(err, user);
        });
      }
    });
  });
};

passport.serializeUser(function(user, done) {
  done(null, user.uid);
});

passport.deserializeUser(function(uid, done) {
  User.findOne({uid: uid}, function(err, user) {
    done(err, user);
  });
});

module.exports.http = {

  customMiddleware: function(app) {

    passport.use(new FacebookStrategy({
      clientID: "YOUR_CLIENT_ID", // Use your Facebook App Id
      clientSecret: "YOUR_CLIENT_SECRET", // Use your Facebook App Secret
      callbackURL: "http://localhost:1337/auth/facebook/callback"
    }, verifyHandler));

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

Remember to configure your client id and secret from Facebook, in "YOUR_CLIENT_ID" and "YOUR_CLIENT_SECRET" above.

api/policies/sessionAuth.js

Time to tell Sails how to handle when a user is authenticated or not. Edit the "api/policies/sessionAuth.js" file. We will tell Sails that if a request is authenticated proceed to the next policy or, if being the last policy, the controller. If the request is not authenticated we will redirect it to the '/login' route we configured above.

module.exports = function(req, res, next) {

  // User is allowed, proceed to the next policy,
  // or if this is the last policy, the controller
  if (req.isAuthenticated()) {
    return next();
  }

  // User is not allowed
  // (default res.forbidden() behavior can be overridden in `config/403.js`)
  return res.redirect('/login');
};

views/auth/login.ejs

If you look closely in the AuthController code we told Sails to respond with the login view (res.view();), but we don't have a login view yet. So we will create the views/auth/login.ejs file and put a link to the actual Facebook Authentication url (/auth/facebook) expected by the Passport middleware:

<a href="/auth/facebook/" class="btn">Login With Facebook</a>

api/controllers/HomeController.js

Let's create the HomeController we talked about above. This will be a private controller, only accessible to authenticated users. First, let's generate the file using a Sails generator:

sails generate controller home

Edit the file api/controllers/HomeController.js. It will be quite simple. It will respond with the index view:

module.exports = {
  index: function(req,res){
    res.view({
      user: req.user
    });
  }
};

views/home/index.js

Let's create the folder /views/home and the file index.ejs. It will be a fairly simple view, showing the content of the User Model:

<h1 id="main-title">Hi <%= user.name %>,</h1>

<p>You've successfully logged in.</p>
<a class="btn" href="/logout">Logout</a>

<h2>User Model</h2>
<table class="table">
  <% for(key in user.toJSON() ) { if (user.hasOwnProperty(key)) { %>
  <tr>
    <td><%= key %>:</td>
    <td class="boxy"><%= user[key] %></td>
  </tr>
  <% }} %>
</table>

However, the file is still publicly accessible. We will need to change the policies.

config/policies.js

Edit the config/policies.js file. We will tell Sails that every file will be public ('*': true) and tell it that the controller HomeController will require authentication using the sessionAuth policy in api/policies/sessionAuth.js:

module.exports.policies = {
  ...
  '*': true,
  'HomeController': {
    '*': 'sessionAuth'
  }
  ...
}

Done!

Sails lift

Let's start our application (in verbose moode) with:

sails lift --verbose

Sails will start your application. You should now be able to visit http://localhost:1337/home, be able to authenticate with Facebook and then redirected back to home to see the user model. Logout will redirect you back to the login page.

What next?

You can check out many of the strategies Passport supports. With the skeleton you've built above you can plug in any more you want.

Did I miss something, shout it out in the comments, and code on!

  • slenkar

    There doesnt seem to be any callback code?

  • Prince PK

    good podst

    Passports