Protecting API routes in loklak web client using JWTs

This is a continuation from the previous post on how I used JWTs (JSON Web Tokens) for local authentication. 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.

[code language=”javascript”]
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);
[/code]

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.

[code language=”javascript”]
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([]);
}
});
}
}
[/code]

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.

[code language=”javascript”]

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

[/code]

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.

[code language=”javascript”]
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,

}]
}
});
[/code]

Client side – consuming the API.

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

[code language=”javascript”]
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’
}
}, …
[/code]

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

[code language=”javascript”]
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;
}
});
}
};
[/code]

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.

[code language=”javascript”]
function tokenInjectorService($window) {
var tokenInjector = {
request: function(config) {
config.headers[‘Authorization’] = ‘Bearer ‘+ $window.localStorage[‘jwt-token’];
return config;
}
};
return tokenInjector;
}
[/code]

[code language=”javascript”]
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’);
}
[/code]

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

 

Protecting API routes in loklak web client using JWTs