DEV Community

Christophe El-Khoury
Christophe El-Khoury

Posted on

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!

Top comments (15)

Collapse
 
pragathyt95 profile image
pragathyt95

sails generate policy isAuthenticated

When i tried the above command, i got the following error:

error: No generator called policy found.
Did you mean sails generate api policy?

Tip: Want to use a custom or community generator?
Add it to your app's .sailsrc file under modules:
You can use a relative path:

"modules": {
  "policy": "./generators/policy"
}

Or the name of an NPM package:

"modules": {
  "policy": "sails-generate-react-component"
}

For help, see:
sailsjs.com/docs/concepts/extendin...

Collapse
 
christopheek profile image
Christophe El-Khoury • Edited

Thank you for the feedback!
At the time of this article, I was relying on Sails' Documentation, which currently takes us to a dead link. So it could be that it's changed. I've posted a comment with the updates

Thank you again!

Collapse
 
christopheek profile image
Christophe El-Khoury

As per @pragathyt95 , sails generate policy [policy-name] is not working. After trying to navigate to Sails' Documentation, it led me to a dead link, which could mean that it's no longer there.

The concept and everything else will still work as they should! All what you need to do is manually creating the policy inside api/policies/<policy_name.js>

Collapse
 
dominuskelvin profile image
Kelvin Omereshone

Great article, in reference to community, there is a brewing community for Sails at the moment and a platform that will be launching soon to teach Sails sailscasts.com

You can join the community by following this invite

discord.gg/gbJZuNm

We also have a blog dedicated to learning Sails things

blog.sailscasts.com

Collapse
 
dominuskelvin profile image
Kelvin Omereshone

By the way I wrote a LogRocket article on building a web API with Sails packed with user registration, JWT authentication, email verification and more.

Check it out if you are interested in that sort of thing :)
blog.logrocket.com/building-a-node...

Collapse
 
pragathyt95 profile image
pragathyt95

how to run this?

Collapse
 
christopheek profile image
Christophe El-Khoury

How to run what exactly?

Collapse
 
pragathyt95 profile image
pragathyt95 • Edited

i got it cleared thank u!

Thread Thread
 
christopheek profile image
Christophe El-Khoury

Alright Prathika. If there's anything else I can do, let me know!

Collapse
 
navicsteinr profile image
Navicstein Rotciv

Thanks for the article, being a sails fan I've been looking for a built in way of automatically handling JWT tokens, this really helps 😀

Collapse
 
christopheek profile image
Christophe El-Khoury

If there's anything else I can help you with, please let me know!

Collapse
 
plamenh profile image
Plamen Hristov

Hello, there! Thanks for the quick solution.

However, a crucial part is missing here - how do we generate the web tokens at the user level?

Collapse
 
christopheek profile image
Christophe El-Khoury

Hey there Plamen!

Unfortunately, this article does not have the purpose of teaching how JWT Tokens are generated.

If you'd like, and other people would too, I can create an article that serves that purpose! Let me know and help me get supporters for the purpose!

Collapse
 
navicsteinr profile image
Navicstein Rotciv

Thanks for this article it really helped!

Collapse
 
christopheek profile image
Christophe El-Khoury

If you need anything else make sure to ping me!