Categories
How To Javascript Node JS Programming

[How To] Implement Passport.js Authentication with Sails.js

Here we intend to demonstrate the procedure of local authentication i.e. by using a username and password.

Step 1: Dependencies

  • passport
  • passport-local
  • bcrypt

List these dependencies inside application_directory/package.json  under dependencies.

//application_directory/package.json
{
...
  "dependencies": {
    ...
    "passport": "~0.1.16",
    "passport-local": "~0.1.6",
    "bcrypt": "~0.7.6"
  }
...
}

Step 2: Create user model

To create user model run the following command:

sails generate model user

Now the user model will be created inside the
/api/models/user.js
To add the necessary attributes and remove confidential attribute from view edit the newly created file user.js
Our final model user.js  will look like  the following

/**
 * User
 *
 * @module      :: Model
 * @description :: A short summary of how this model works and what it represents.
 *
 */
var bcrypt = require('bcrypt');

module.exports = {

  attributes: {
    username: {
      type: 'string',
      required: true,
      unique: true
    },
    password: {
      type: 'string',
      required: true
    },
    toJSON: function() {
      var obj = this.toObject();
      delete obj.password;
      return obj;
    }
  },

  beforeCreate: function(user, cb) {
    bcrypt.genSalt(10, function(err, salt) {
      bcrypt.hash(user.password, salt, function(err, hash) {
        if (err) {
          console.log(err);
          cb(err);
        }else{
          user.password = hash;
          cb(null, user);
        }
      });
    });
  }

};

Step 3: Create AuthController and login view

To create a controller in sails type the command

sails generate controller 

Now edit the newly generated AuthController  inside api/controllers/AuthController.js to add the action login and process
Action login is used to render the login form in GET request.
Action process is used to validate the login credentials using passport in POST request.
Now our AuthController will look like the following.

/**
 * AuthController
 *
 */
var passport = require('passport');
module.exports = {

  login: function (req, res) {
    res.view();
  },
  process: function(req, res){
    passport.authenticate('local', function(err, user, info) {
      if ((err) || (!user)) {
        return res.send({
        message: 'login failed'
        });
        res.send(err);
      }
      req.logIn(user, function(err) {
        if (err) res.send(err);
        return res.send({
          message: 'login successful'
        });
      });
    })(req, res);
  },
  logout: function (req,res){
    req.logout();
    res.send('logout successful');
  }
};

/**
 * Sails controllers expose some logic automatically via blueprints.
 *
 * Blueprints are enabled for all controllers by default, and they can be turned on or off
 * app-wide in `config/controllers.js`. The settings below are overrides provided specifically
 * for AuthController.
 *
 * NOTE:
 * REST and CRUD shortcut blueprints are only enabled if a matching model file
 * (`models/Auth.js`) exists.
 *
 * NOTE:
 * You may also override the logic and leave the routes intact by creating your own
 * custom middleware for AuthController's `find`, `create`, `update`, and/or
 * `destroy` actions.
 */

module.exports.blueprints = {

  // Expose a route for every method,
  // e.g.
  // `/auth/foo` => `foo: function (req, res) {}`
  actions: true,
  
  // Expose a RESTful API, e.g.
  // `post /auth` => `create: function (req, res) {}`
  rest: true,
  
  // Expose simple CRUD shortcuts, e.g.
  // `/auth/create` => `create: function (req, res) {}`
  // (useful for prototyping)
  shortcuts: true

};

Creating Login view

Create a view file  inside application_directory/views/auth/login.ejs
Add the following code to that

Step 4: Configuring the routes

We are going to render the login page in GET to /login and authenticate in POST using the action ‘process’
For that add the following code to application_directory/config/routes.js

module.exports.routes = {
  // (This would also work if you had a file at: `/views/home.ejs`)
  '/': {
    view: 'home/index'
  },
  
  '/login': {
    controller: 'AuthController',
    action: 'login'
  },
  '/logout': {
    controller: 'AuthController',
    action: 'logout'
  }
  ......
}

Step 5: Configuring passport to be used as a middleware

Inside application_directory/config create a file passport.js and add the following code to that

var passport = require('passport'),
LocalStrategy = require('passport-local').Strategy;
module.exports = {
  express: {
    customMiddleware: function(app){
      console.log('Express midleware for passport');
      app.use(passport.initialize());
      app.use(passport.session());
    }
  }
};

Step 6: Invoking passport local strategy for authentication

Basically in this step we have to invoke the authentication method of passport by passing the user provided details.
Inside /api/services/ create a file passport.js and add the following code to that.

var passport = require('passport'),
  LocalStrategy = require('passport-local').Strategy,
  bcrypt = require('bcrypt'); < /code>
//helper functions
function findById(id, fn) {
  User.findOne(id).done(function (err, user) {
    if (err) {
      return fn(null, null);
    } else {
      return fn(null, user);
    }
  });
}

function findByUsername(u, fn) {
  User.findOne({
    username: u
  }).done(function (err, user) {
    // Error handling
    if (err) {
      return fn(null, null);
      // The User was found successfully!
    } else {
      return fn(null, user);
    }
  });
}

// Passport session setup.
// To support persistent login sessions, Passport needs to be able to
// serialize users into and deserialize users out of the session. Typically,
// this will be as simple as storing the user ID when serializing, and finding
// the user by ID when deserializing.
passport.serializeUser(function (user, done) {
  done(null, user.id);
});

passport.deserializeUser(function (id, done) {
  findById(id, function (err, user) {
    done(err, user);
  });
});

// Use the LocalStrategy within Passport.
// Strategies in passport require a `verify` function, which accept
// credentials (in this case, a username and password), and invoke a callback
// with a user object.
passport.use(new LocalStrategy(
  function (username, password, done) {
    // asynchronous verification, for effect...
    process.nextTick(function () {
      // Find the user by username. If there is no user with the given
      // username, or the password is not correct, set the user to `false` to
      // indicate failure and set a flash message. Otherwise, return the
      // authenticated `user`.
      findByUsername(username, function (err, user) {
        if (err)
          return done(null, err);
        if (!user) {
          return done(null, false, {
            message: 'Unknown user ' + username
          });
        }
        bcrypt.compare(password, user.password, function (err, res) {
          if (!res)
            return done(null, false, {
              message: 'Invalid Password'
            });
          var returnUser = {
            username: user.username,
            createdAt: user.createdAt,
            id: user.id
          };
          return done(null, returnUser, {
            message: 'Logged In Successfully'
          });
        });
      })
    });
  }
));

Step 7 : Modifying policy

Modify the authenticated.js file present inside /api/policies/

/**
 * Allow any authenticated user.
 */
module.exports = function (req, res, ok) {

  // User is allowed, proceed to controller
  var is_auth = req.isAuthenticated()
  if (is_auth) return next();
  // User is not allowed
  else return res.redirect("/login");
};

Step 8: Testing

npm install 
sails lift

First create a user by following the below url
http://localhost:1337/user/create?username=user&password=pass
and then goto http://localhost:1337/login in ur preferred browser and login

That’s all
Happy Coding !

References

15 replies on “[How To] Implement Passport.js Authentication with Sails.js”

Thanks for the write up.

“http://localhost:1337/user/create?username=user&password=pass”

In order for the API to work like this, there needs to be a controller as well. Because the tutorial creates just a user Model, this link will actually 404. I had to create an empty User controller for it to work, but it might make more sense to just rename the AuthController.

Thanks for this example, but I’m struggling with the following error:

Error: passport.initialize() middleware not in use

It appears Passport isn’t being initialised. I never see this message in the console when I start the app:

console.log(‘Express midleware for passport’);

Any suggestions very welcome. This is driving me mad. 🙁

It does seem to be authenticating users, btw. I’m getting that error when I call req.logIn in the controller.

.done in the passport.js service(line 18) will no longer work. It should be replaced with .exec instead.

Maybe this was a good example some time ago, but now it isn’t. It is extremely error-prone.
I used the working one from https://github.com/kasperisager/sails-generate-auth (thanks @Edygar), but made it simpler (I don’t need Facebook adapters or login with both username and e-mail).

Literally almost any piece of code I reused from this example had errors in it.

If you want to figure it out on your own, with invaluable educational experience, don’t read my comment any further, and try making it to work! 😉

First of all, in findById & findByUsername, you can’t do User.findOne(query).done() anymore, you have to do User.findOne(id, function (err, user) { … }); . In the case of findByUsername it trows an error, but findById fails silently (and you spend hours figuring out why session is not established after successful login).

Next,
bcrypt.genSalt(10, function(err, salt) {
bcrypt.hash(user.password, salt, function(err, hash) {…} });
doesn’t work anymore (undocumented change in api?), one has to do
bcrypt.hash(user.password, 10, function (err, hash) {…});

Then, to set up the local strategy one has to declare something like this in services/passport.js and add it to config/bootstrap.js (a “passport.loadStrategies();” line):

passport.loadStrategies = function () {
this.use(new LocalStrategy({ passReqToCallback: true, usernameField: ’email’ },
function (req, username, password, done) {

Note: passReqToCallback is useful when you want to localize messages returned, e.g. with i18n
return done(null, false, {message: sails.__.apply(req, [‘Error.Passport.Password.Wrong’])});
otherwise, there will be no req object inside.

Custom middleware is best inserted through an “authenticated” policy, which has to set up session variables (req.isAuthenticated() doesn’t work), see how it’s done e.g. like in https://github.com/kasperisager/sails-generate-auth, but the example there with “sessionAuth” policy is incomplete and “req.session.authenticated” variable doesn’t get set, so one has to add some extra code into api/policies/passport.js.
Inserting middleware Express-style through config/passport.js is obsolete (one has to use “http” variable, not “express”) and seems to break interoperability of other middleware inside Sails.
It is possible to set custom middleware config through config/http.js, but I don’t think it is good to do it this way.

And so on, because I think I might have omitted something.
If you don’t want any headache just use https://github.com/kasperisager/sails-generate-auth (don’t forget to set req.session.authenticated somewhere in api/policies/passport.js). This might not be the best code, this is how I do it:

file api/policies/passport.js:
module.exports = function (req, res, next) {
// Initialize Passport
passport.initialize()(req, res, function () {
// Use the built-in sessions
passport.session()(req, res, function () {
//passport.authenticate(‘session’)(req, res, function () {
// Make the user available throughout the frontend
console.log(“passport.session called, user is “, req.user);
res.locals.user = req.user;
if (typeof req.user != ‘undefined’) req.session.authenticated = true;
else req.session.authenticated = false;
next();
});
});
};