What I learned on my own | Implementing JWT Authentication on SailsJS (v1)

christopheek

Christophe El-Khoury

Posted on August 2, 2019

What I learned on my own | Implementing JWT Authentication on SailsJS (v1)

What this article will teach you:

  • How to apply JWT Authentication in your SailsJS v1 backend.

What this article will NOT teach you:

  • How to create a SailsJS application.
  • How to understand the mechanisms behind these functions.

The reason why I won't go in-depth with the mechanisms of some functions and how they work in the background is because there's a whole lot of articles and documentations out there, written by people who are much much more qualified than I am.

Who is this article intended for? Anyone who just needs to implement some JWT Authentication into SailsJS because they have a deadline and have no time to do prolonged research.

If you've been developing with Sails, you're going to realize that you won't be able to get all the answers you want, mainly because of Sails' migration from v0 to v1, their not-so-laravelish-documentation and the rather small community accompanying Sails.

Enough with the small talk. Let me have a smoke and I'll come back to the technicalities.


Disclaimer: some of this code is inspired from this repo written by Corey Birnbaum. So props for this guy.

The way this is going to be implemented is as follows, in a general, non-sails-terms sentences:

  • HTTP Request (requires authentication) hits your backend
  • The Request is intercepted by middleware
  • Authentication Valid -- Redirect to controller
  • Authentication invalid -- 401

Okay great, but how do we implement this in Sails?
For this, I'm going to assume you've already set up your routes and actions.

In simple terms, a middleware in Sails is called a policy. Which means, you're going to assign to a controller/action a set of rules, which basically tells that controller/action "Hey! I know you're expecting this HTTP Request to be redirected to you, but first I need to make a few checks. If these checks pass, I'll send it over. If not, I'm taking that piece of candy away from ya."

Prerequisite: for JWT Authentication here, I'm using a package called jsonwebtoken. Make sure to install it in your project directory.

So start by defining that policy by going to config/policies.js and adding the following line within the module.exports.policies body:

{
   controllerName: {
      'action-name': 'isAuthenticated'
   }
}
Enter fullscreen mode Exit fullscreen mode

That way, you told action-name that isAuthenticated will make a few checks on HTTP Requests directed to you before it decides whether or not the action can act upon that request.

Next up, you'll have to actually write that policy. Sails' CLI provides a command to generate just about anything, including policies. So run the following command in your command line:
sails generate policy isAuthenticated
if all goes well, you will see a isAuthenticated.js file inside api/policies/ directory.

Write the following code inside the isAuthenticated.js policy:

CODE

/**
 * isAuthenticated
 *
 * @module      :: Policy
 * @description :: Simple policy to require an authenticated user, or else redirect to login page
 *                 Looks for an Authorization header bearing a valid JWT token
 * @docs        :: http://sailsjs.org/#!documentation/policies
 *
 */

module.exports = async function (req, res, next) {
    sails.helpers.verifyJwt.with({
        req: req,
        res: res
    })
        .switch({
            error: function (err) {
                return res.serverError(err)
            },
            invalid: function (err) {
                // if this is not an HTML-wanting browser, e.g. AJAX/sockets/cURL/etc.,
                // send a 401 response letting the user agent know they need to login to
                // access this endpoint.
                if (req.wantsJSON) {
                    return res.sendStatus(401)
                }
                // otherwise if this is an HTML-wanting browser, do a redirect.
                return res.redirect('/login')
            },
            success: function () {
                // user has been attached to the req object (ie logged in) so we're set, they may proceed
                return next()
            }
        })
}

Enter fullscreen mode Exit fullscreen mode

EXPLANATION
First off, this policy is getting some help from a helper called verifyJwt, which we'll write in a minute. It's also giving it two parameters. The request req and response res. At first I was confused as to how am I going to pass these parameters to isAuthenticated from my policies.js definition? As it turns out, sails takes care of that automatically, since policies are intended, by nature, to take req and res, and they are only needed for HTTP Requests.

verifyJwt will return either error, or invalid, or success. Each one of these possible returns has its own handling.
If an error is returned, bad news. You've got a problem in your code or in your request.
If an invalid is returned, good news for you. Your code's working, but the request won't be forwarded to your action because the request is not authenticated.
If a success is returned, the user is authenticated, the request is forwarded to your action and everybody's happy.

Now onto the verifyJwt helper function. This is going to be the bulk of your authentication logic.

To do that, we'll have to create a helper
sails generate helper verify-jwt.

inside api/helpers/verify-jwt.js, we will write
CODE

var jwt = require('jsonwebtoken')

module.exports = {
  friendlyName: 'Verify JWT',
  description: 'Verify a JWT token.',
  inputs: {
    req: {
      type: 'ref',
      friendlyName: 'Request',
      description: 'A reference to the request object (req).',
      required: true
    },
    res: {
      type: 'ref',
      friendlyName: 'Response',
      description: 'A reference to the response object (res).',
      required: false
    }
  },
  exits: {
    invalid: {
      description: 'Invalid token or no authentication present.',
    }
  },
  fn: function (inputs, exits) {
    var req = inputs.req
    var res = inputs.res
    if (req.header('authorization')) {
      // if one exists, attempt to get the header data
      var token = req.header('authorization').split('Bearer ')[1]
      // if there's nothing after "Bearer", no go
      if (!token) return exits.invalid()
      // if there is something, attempt to parse it as a JWT token
      return jwt.verify(token, process.env.JWT_KEY, async function (err, payload) {
        if (err || !payload.sub) return exits.invalid()
        var user = await User.findOne(payload.sub)
        if (!user) return exits.invalid()
        // if it got this far, everything checks out, success
        req.user = user
        return exits.success(user)
      })
    }
    return exits.invalid()
  }
}
Enter fullscreen mode Exit fullscreen mode

EXPLANATION

  • Firstly, the helper is checking for an authorization header inside the request. If it doesn't exist, then the request is unauthenticated and will `return exits.invalid();
  • If an authorization header does exist, we extract the token, and run jsonwebtoken's verify function. This function will return a payload.
  • A sub property inside the payload should exist, as per the JWT Anatomy
  • If it does, it will most likely hold a key identifier for the user (e.g. ID).
  • Once we have that sub, we need to use it to try to find a user, to whom this JWT belongs.
  • If this user is found, assign it to your req. The reason why we're doing that is, if, for some reason in our code, we need to verify that a certain operation is being requested by userA, who indeed is authenticated, on some belongings to userB. So even though userA is authenticated, they should not be allowed to perform any actions concerning userB (such as, editing a userB's blog post).

And voilà. You've got authentication up and running.

If you have any questions, feedback, corrections from any misinformation I might have provided (including typos), my direct messages are open for anyone but I strongly urge you to post them in the comments section for everyone to benefit from them.

Happy coding fellas!

💖 💪 🙅 🚩
christopheek
Christophe El-Khoury

Posted on August 2, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related