Protecting API routes using JWTs

Signing in locally is great but what we want is to make sure the right person has access to the right routes. This was done for the loklak user walls, as we have to check if the user is signed in and is authorized to see the current walls, and edit them.

Server side - protecting routes

To verify the identity of the logged in user we have to check for the JWT. To setup and use the route authentication we use express-jwt as middleware, which checks if the JWT is present before going to the route.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var jwt = require('express-jwt');
var config = require('../../custom_configFile.json');

var auth = jwt({
secret: config.jwtsecret,
userProperty: 'payload'
});

var ctrlAuth = require('../controllers/authentication');
var ctrlMailer = require('../controllers/email');
var ctrlWalls = require('../controllers/walls');

// WALL API
router.get ('/:user/:app/:id', ctrlWalls.getWallById);
router.get ('/:user/:app', auth, ctrlWalls.getUserWalls);
router.post ('/:user/:app', auth, ctrlWalls.createWall);
router.put ('/:user/:app/:id', auth, ctrlWalls.updateWall);
router.delete('/:user/:app/:id', auth, ctrlWalls.deleteWall);

Notice that we do not use authentication for the wall route as we want anyone to be able to access the wall.

The route controllers then handle manipulating the mongoose user models. We check for the JWT payload in the getUserWalls controller but not the getWallById controller.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
module.exports.getWallById = function (req, res) {
User
.findById(req.params.user)
.exec(function(err, user) {
if (user.apps[req.params.app]) {
for (i = 0; i < user.apps[req.params.app].length; i = i + 1) {
if (user.apps[req.params.app][i].id === req.params.id) {
return res.jsonp(user.apps[req.params.app][i]);
}
}
res.jsonp({});
} else {
res.jsonp({});
}
});
}

module.exports.getUserWalls = function (req, res) {
// If no user ID exists in the JWT return a 401
if (!req.payload._id) {
res.status(401).json({
message : UnauthorizedError: private wall page
});
} else {
User
.findById(req.params.user)
.exec(function(err, user) {
if (user.apps && user.apps[req.params.app]) {
res.jsonp(user.apps[req.params.app]);
} else {
res.jsonp([]);
}
});
}
}

One error I got stuck on was the Mongoose: TypeError: doc.validate is not a function, in the update and delete methods. Mutating the array with splice instead of assigning it a new object solves this.

1
2
3
4
5
// x
// appData[req.params.app][i] = req.body;

// √
appData[req.params.app].splice(i, 1, req.body);

Model Schema

I have chosen to embed the wall options into the User model schema itself instead of creating another collection, as it is one user-few walls, so there would be lesser queries to the API as there’s mostly reading wall options, and relatively less updating of wall options.

1
2
3
4
5
6
7
8
9
10
11
12
13
var UserSchema = new Schema({
email: { type: String, unique: true, required: true },
name: { type: String, required: true },
hash: String,
salt: String,
isVerified: { type: Boolean, required: true },
apps: {
wall: [{
profanity: Boolean,
...
}]
}
});

Client side - consuming the API.

We use $resource, a factory built on $http, to interact with the routes, through an angular service.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function AppsService($q, $http, $resource, AppSettings, AuthService) {
return $resource('/api/:user/:app/:id', {
user: '@user',
app: '@app',
id: '@id'
}, {
query: {
method: 'GET',
isArray: true
},
save: {
method: 'POST',
transformRequest: function(data) {
delete data.user;
delete data.app;
delete data.showLoading;
return JSON.stringify(data);
},
params: {
user: '@user',
app: '@app',
id: '@id'
}
}, ...

Then in our controller, WallCtrl, we use this service, for eg:

1
2
3
4
5
6
7
8
9
10
11
12
var init = function() {
if ($scope.isLoggedIn) {
$scope.userWalls = AppsService.query({
user: $scope.currentUser._id,
app: 'wall'
}, function(result) {
if ($scope.userWalls.length === 0) {
$scope.wallsPresent = false;
}
});
}
};

To attach the JWT in the header we will get the JWT from local storage and attach it using an httpInterceptor through the client side routes which uses UI-router. If you do not do this you will get the UnauthorizedError, as no token is found.

1
2
3
4
5
6
7
8
9
function tokenInjectorService($window) {
var tokenInjector = {
request: function(config) {
config.headers['Authorization'] = 'Bearer '+ $window.localStorage['jwt-token'];
return config;
}
};
return tokenInjector;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
function Routes($stateProvider, $locationProvider, $httpProvider) {
$locationProvider.html5Mode(true);

$stateProvider
.state('Home', {
url: '/',
controller: 'MapCtrl as map',
templateUrl: 'home.html',
title: 'Home'
}) // and other routes

$httpProvider.interceptors.push('tokenInjector');
}

Hope that helps anyone who has trouble with their MEAN app and JWT auth.