What I learned on my own | Implementing JWT Authentication on SailsJS (v1)
Christophe El-Khoury
Posted on August 2, 2019
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'
}
}
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()
}
})
}
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()
}
}
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 thetoken
, and runjsonwebtoken
'sverify
function. This function will return apayload
. - A
sub
property inside thepayload
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!
Posted on August 2, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.