diff --git a/config/env/default.js b/config/env/default.js
index 35d1948bd2..d154797ff6 100644
--- a/config/env/default.js
+++ b/config/env/default.js
@@ -63,6 +63,11 @@ module.exports = {
minPhraseLength: 20,
minOptionalTestsToPass: 4
}
+ },
+ jwt: {
+ secret: process.env.JWT_SECRET || 'M3@N_R0CK5',
+ options: {
+ expiresIn: process.env.JWT_EXPIRES_IN || '1d'
+ }
}
-
};
diff --git a/config/lib/authorization.js b/config/lib/authorization.js
new file mode 100644
index 0000000000..d97f499ba7
--- /dev/null
+++ b/config/lib/authorization.js
@@ -0,0 +1,51 @@
+'use strict';
+
+var config = require('../config'),
+ jwt = require('jsonwebtoken'),
+ lodash = require('lodash'),
+ passport = require('passport');
+
+var auth = {
+ signToken: signToken,
+ authorize: authorizeRequest
+};
+
+// Export the token auth service
+module.exports = auth;
+
+function authorizeRequest(req, res, next) {
+ passport.authenticate('jwt', { session: false }, function (err, user) {
+ if (err) {
+ return next(new Error(err));
+ }
+
+ if (user) {
+ req.user = user;
+ }
+
+ next();
+ })(req, res, next);
+}
+
+// Sign the Token
+function signToken(user, options) {
+ var payload,
+ token,
+ jwtOptions;
+
+ if (!user || !user._id) {
+ return null;
+ }
+
+ options = options || {};
+
+ payload = {
+ user: user._id.toString()
+ };
+
+ jwtOptions = lodash.merge(config.jwt.options, options);
+
+ token = jwt.sign(payload, config.jwt.secret, jwtOptions);
+
+ return token;
+}
diff --git a/config/lib/express.js b/config/lib/express.js
index 93303517b6..bcd322dbe7 100644
--- a/config/lib/express.js
+++ b/config/lib/express.js
@@ -19,7 +19,8 @@ var config = require('../config'),
hbs = require('express-hbs'),
path = require('path'),
_ = require('lodash'),
- lusca = require('lusca');
+ lusca = require('lusca'),
+ authorization = require('./authorization');
/**
* Initialize local variables
@@ -89,6 +90,9 @@ module.exports.initMiddleware = function (app) {
// Add the cookie parser and flash middleware
app.use(cookieParser());
app.use(flash());
+
+ // Authorize Request
+ app.use(authorization.authorize);
};
/**
@@ -238,9 +242,6 @@ module.exports.init = function (db) {
// Initialize modules static client routes, before session!
this.initModulesClientRoutes(app);
- // Initialize Express session
- this.initSession(app, db);
-
// Initialize Modules configuration
this.initModulesConfiguration(app);
diff --git a/config/lib/socket.io.js b/config/lib/socket.io.js
index 0050f4fb86..ee7f10fdef 100644
--- a/config/lib/socket.io.js
+++ b/config/lib/socket.io.js
@@ -6,11 +6,8 @@ var config = require('../config'),
fs = require('fs'),
http = require('http'),
https = require('https'),
- cookieParser = require('cookie-parser'),
passport = require('passport'),
- socketio = require('socket.io'),
- session = require('express-session'),
- MongoStore = require('connect-mongo')(session);
+ socketio = require('socket.io');
// Define the Socket.io configuration method
module.exports = function (app, db) {
@@ -69,49 +66,42 @@ module.exports = function (app, db) {
// Create a new Socket.io server
var io = socketio.listen(server);
- // Create a MongoDB storage object
- var mongoStore = new MongoStore({
- mongooseConnection: db.connection,
- collection: config.sessionCollection
+ // Configure SocketIO Authentication
+ require('socketio-auth')(io, {
+ authenticate: authenticate,
+ postAuthenticate: postAuthenticate,
+ timeout: 1000
});
- // Intercept Socket.io's handshake request
- io.use(function (socket, next) {
- // Use the 'cookie-parser' module to parse the request cookies
- cookieParser(config.sessionSecret)(socket.request, {}, function (err) {
- // Get the session id from the request cookies
- var sessionId = socket.request.signedCookies ? socket.request.signedCookies[config.sessionKey] : undefined;
+ return server;
- if (!sessionId) return next(new Error('sessionId was not found in socket.request'), false);
+ // Handler for authenticating the SocketIO connection
+ function authenticate(socket, data, callback) {
+ // Set the Authorization header using the provided token
+ socket.request.headers.authorization = 'JWT ' + data.token;
- // Use the mongoStorage instance to get the Express session information
- mongoStore.get(sessionId, function (err, session) {
- if (err) return next(err, false);
- if (!session) return next(new Error('session was not found for ' + sessionId), false);
+ // Use Passport to populate the user details
+ passport.authenticate('jwt', { session: false }, function (err, user) {
+ if (err) {
+ return callback(new Error(err));
+ }
- // Set the Socket.io session information
- socket.request.session = session;
+ if (!user) {
+ return callback(new Error('User not found'));
+ }
- // Use Passport to populate the user details
- passport.initialize()(socket.request, {}, function () {
- passport.session()(socket.request, {}, function () {
- if (socket.request.user) {
- next(null, true);
- } else {
- next(new Error('User is not authenticated'), false);
- }
- });
- });
- });
- });
- });
+ // Set the socket user
+ socket.request.user = user;
+
+ return callback(null, true);
+ })(socket.request, socket.request.res, callback);
+ }
- // Add an event listener to the 'connection' event
- io.on('connection', function (socket) {
+ // Handler for post-Authentication
+ function postAuthenticate(socket, data) {
+ // Configure the server-side Socket listeners
config.files.server.sockets.forEach(function (socketConfiguration) {
require(path.resolve(socketConfiguration))(io, socket);
});
- });
-
- return server;
+ }
};
diff --git a/modules/articles/tests/client/admin.articles.client.controller.tests.js b/modules/articles/tests/client/admin.articles.client.controller.tests.js
index 32964a8fe3..04eee9ae52 100644
--- a/modules/articles/tests/client/admin.articles.client.controller.tests.js
+++ b/modules/articles/tests/client/admin.articles.client.controller.tests.js
@@ -91,6 +91,7 @@
it('should send a POST request with the form input values and then locate to new object URL', inject(function (ArticlesService) {
// Set POST response
$httpBackend.expectPOST('/api/articles', sampleArticlePostData).respond(mockArticle);
+ $httpBackend.when('GET', '/api/users/me').respond(200, 'Fred');
// Run controller functionality
$scope.vm.save(true);
@@ -107,6 +108,7 @@
$httpBackend.expectPOST('/api/articles', sampleArticlePostData).respond(400, {
message: errorMessage
});
+ $httpBackend.when('GET', '/api/users/me').respond(200, 'Fred');
$scope.vm.save(true);
$httpBackend.flush();
@@ -124,6 +126,7 @@
it('should update a valid article', inject(function (ArticlesService) {
// Set PUT response
$httpBackend.expectPUT(/api\/articles\/([0-9a-fA-F]{24})$/).respond();
+ $httpBackend.when('GET', '/api/users/me').respond(200, 'Fred');
// Run controller functionality
$scope.vm.save(true);
@@ -140,6 +143,7 @@
$httpBackend.expectPUT(/api\/articles\/([0-9a-fA-F]{24})$/).respond(400, {
message: errorMessage
});
+ $httpBackend.when('GET', '/api/users/me').respond(200, 'Fred');
$scope.vm.save(true);
$httpBackend.flush();
@@ -159,6 +163,7 @@
spyOn(window, 'confirm').and.returnValue(true);
$httpBackend.expectDELETE(/api\/articles\/([0-9a-fA-F]{24})$/).respond(204);
+ $httpBackend.when('GET', '/api/users/me').respond(200, 'Fred');
$scope.vm.remove();
$httpBackend.flush();
diff --git a/modules/articles/tests/client/admin.list.articles.client.controller.tests.js b/modules/articles/tests/client/admin.list.articles.client.controller.tests.js
index 90bd19f1a6..16cdf66b43 100644
--- a/modules/articles/tests/client/admin.list.articles.client.controller.tests.js
+++ b/modules/articles/tests/client/admin.list.articles.client.controller.tests.js
@@ -81,7 +81,7 @@
it('should send a GET request and return all articles', inject(function (ArticlesService) {
// Set POST response
$httpBackend.expectGET('/api/articles').respond(mockArticleList);
-
+ $httpBackend.when('GET', '/api/users/me').respond(200, 'Fred');
$httpBackend.flush();
diff --git a/modules/articles/tests/client/articles.client.controller.tests.js b/modules/articles/tests/client/articles.client.controller.tests.js
index 966d9c4309..aacd083bd3 100644
--- a/modules/articles/tests/client/articles.client.controller.tests.js
+++ b/modules/articles/tests/client/articles.client.controller.tests.js
@@ -46,6 +46,8 @@
Authentication = _Authentication_;
ArticlesService = _ArticlesService_;
+ $httpBackend.whenGET('/api/users/me').respond({});
+
// create mock article
mockArticle = new ArticlesService({
_id: '525a8422f6d0f87f0e407a33',
diff --git a/modules/articles/tests/client/articles.client.routes.tests.js b/modules/articles/tests/client/articles.client.routes.tests.js
index 7b724a5c53..8fe68252c9 100644
--- a/modules/articles/tests/client/articles.client.routes.tests.js
+++ b/modules/articles/tests/client/articles.client.routes.tests.js
@@ -18,6 +18,10 @@
ArticlesService = _ArticlesService_;
}));
+ afterEach(inject(function (Authentication) {
+ Authentication.signout();
+ }));
+
describe('Route Config', function () {
describe('Main Route', function () {
var mainstate;
diff --git a/modules/articles/tests/client/list-articles.client.controller.tests.js b/modules/articles/tests/client/list-articles.client.controller.tests.js
index b5e37c9f62..8486bede31 100644
--- a/modules/articles/tests/client/list-articles.client.controller.tests.js
+++ b/modules/articles/tests/client/list-articles.client.controller.tests.js
@@ -67,6 +67,10 @@
spyOn($state, 'go');
}));
+ afterEach(inject(function (Authentication) {
+ Authentication.signout();
+ }));
+
describe('Instantiate', function () {
var mockArticleList;
diff --git a/modules/articles/tests/server/admin.article.server.routes.tests.js b/modules/articles/tests/server/admin.article.server.routes.tests.js
index 0322dd14bc..ebe6dff3c8 100644
--- a/modules/articles/tests/server/admin.article.server.routes.tests.js
+++ b/modules/articles/tests/server/admin.article.server.routes.tests.js
@@ -74,6 +74,7 @@ describe('Article Admin CRUD tests', function () {
// Save a new article
agent.post('/api/articles')
+ .set('Authorization', 'JWT ' + signinRes.body.token)
.send(article)
.expect(200)
.end(function (articleSaveErr, articleSaveRes) {
@@ -119,6 +120,7 @@ describe('Article Admin CRUD tests', function () {
// Save a new article
agent.post('/api/articles')
+ .set('Authorization', 'JWT ' + signinRes.body.token)
.send(article)
.expect(200)
.end(function (articleSaveErr, articleSaveRes) {
@@ -132,6 +134,7 @@ describe('Article Admin CRUD tests', function () {
// Update an existing article
agent.put('/api/articles/' + articleSaveRes.body._id)
+ .set('Authorization', 'JWT ' + signinRes.body.token)
.send(article)
.expect(200)
.end(function (articleUpdateErr, articleUpdateRes) {
@@ -169,6 +172,7 @@ describe('Article Admin CRUD tests', function () {
// Save a new article
agent.post('/api/articles')
+ .set('Authorization', 'JWT ' + signinRes.body.token)
.send(article)
.expect(422)
.end(function (articleSaveErr, articleSaveRes) {
@@ -196,6 +200,7 @@ describe('Article Admin CRUD tests', function () {
// Save a new article
agent.post('/api/articles')
+ .set('Authorization', 'JWT ' + signinRes.body.token)
.send(article)
.expect(200)
.end(function (articleSaveErr, articleSaveRes) {
@@ -206,6 +211,7 @@ describe('Article Admin CRUD tests', function () {
// Delete an existing article
agent.delete('/api/articles/' + articleSaveRes.body._id)
+ .set('Authorization', 'JWT ' + signinRes.body.token)
.send(article)
.expect(200)
.end(function (articleDeleteErr, articleDeleteRes) {
@@ -243,6 +249,7 @@ describe('Article Admin CRUD tests', function () {
// Save a new article
agent.post('/api/articles')
+ .set('Authorization', 'JWT ' + signinRes.body.token)
.send(article)
.expect(200)
.end(function (articleSaveErr, articleSaveRes) {
@@ -253,6 +260,7 @@ describe('Article Admin CRUD tests', function () {
// Get the article
agent.get('/api/articles/' + articleSaveRes.body._id)
+ .set('Authorization', 'JWT ' + signinRes.body.token)
.expect(200)
.end(function (articleInfoErr, articleInfoRes) {
// Handle article error
diff --git a/modules/articles/tests/server/article.server.routes.tests.js b/modules/articles/tests/server/article.server.routes.tests.js
index 6f35907e6a..a831c27451 100644
--- a/modules/articles/tests/server/article.server.routes.tests.js
+++ b/modules/articles/tests/server/article.server.routes.tests.js
@@ -70,6 +70,7 @@ describe('Article CRUD tests', function () {
}
agent.post('/api/articles')
+ .set('Authorization', 'JWT ' + signinRes.body.token)
.send(article)
.expect(403)
.end(function (articleSaveErr, articleSaveRes) {
@@ -101,6 +102,7 @@ describe('Article CRUD tests', function () {
}
agent.post('/api/articles')
+ .set('Authorization', 'JWT ' + signinRes.body.token)
.send(article)
.expect(403)
.end(function (articleSaveErr, articleSaveRes) {
@@ -181,6 +183,7 @@ describe('Article CRUD tests', function () {
}
agent.post('/api/articles')
+ .set('Authorization', 'JWT ' + signinRes.body.token)
.send(article)
.expect(403)
.end(function (articleSaveErr, articleSaveRes) {
@@ -252,6 +255,7 @@ describe('Article CRUD tests', function () {
// Save a new article
agent.post('/api/articles')
+ .set('Authorization', 'JWT ' + signinRes.body.token)
.send(article)
.expect(200)
.end(function (articleSaveErr, articleSaveRes) {
@@ -279,6 +283,7 @@ describe('Article CRUD tests', function () {
// Get the article
agent.get('/api/articles/' + articleSaveRes.body._id)
+ .set('Authorization', 'JWT ' + res.body.token)
.expect(200)
.end(function (articleInfoErr, articleInfoRes) {
// Handle article error
@@ -359,6 +364,7 @@ describe('Article CRUD tests', function () {
// Save a new article
agent.post('/api/articles')
+ .set('Authorization', 'JWT ' + signinRes.body.token)
.send(article)
.expect(200)
.end(function (articleSaveErr, articleSaveRes) {
@@ -384,6 +390,7 @@ describe('Article CRUD tests', function () {
// Get the article
agent.get('/api/articles/' + articleSaveRes.body._id)
+ .set('Authorization', 'JWT ' + res.body.token)
.expect(200)
.end(function (articleInfoErr, articleInfoRes) {
// Handle article error
diff --git a/modules/core/client/config/core.client.route-filter.js b/modules/core/client/config/core.client.route-filter.js
index 4d7c605492..85f3b89214 100644
--- a/modules/core/client/config/core.client.route-filter.js
+++ b/modules/core/client/config/core.client.route-filter.js
@@ -5,36 +5,43 @@
.module('core')
.run(routeFilter);
- routeFilter.$inject = ['$rootScope', '$state', 'Authentication'];
+ routeFilter.$inject = ['$rootScope', '$state', 'Authentication', '$log'];
+
+ function routeFilter($rootScope, $state, Authentication, $log) {
- function routeFilter($rootScope, $state, Authentication) {
$rootScope.$on('$stateChangeStart', stateChangeStart);
$rootScope.$on('$stateChangeSuccess', stateChangeSuccess);
function stateChangeStart(event, toState, toParams, fromState, fromParams) {
- // Check authentication before changing state
- if (toState.data && toState.data.roles && toState.data.roles.length > 0) {
- var allowed = false;
-
- for (var i = 0, roles = toState.data.roles; i < roles.length; i++) {
- if ((roles[i] === 'guest') || (Authentication.user && Authentication.user.roles !== undefined && Authentication.user.roles.indexOf(roles[i]) !== -1)) {
- allowed = true;
- break;
- }
- }
-
- if (!allowed) {
- event.preventDefault();
- if (Authentication.user !== null && typeof Authentication.user === 'object') {
- $state.transitionTo('forbidden');
- } else {
- $state.go('authentication.signin').then(function () {
- // Record previous state
- storePreviousState(toState, toParams);
- });
+ Authentication.ready.promise
+ .then(function () {
+ // Check authentication before changing state
+ if (toState.data && toState.data.roles && toState.data.roles.length > 0) {
+ var allowed = false;
+
+ for (var i = 0, roles = toState.data.roles; i < roles.length; i++) {
+ if ((roles[i] === 'guest') || (Authentication.user && Authentication.user.roles !== undefined && Authentication.user.roles.indexOf(roles[i]) !== -1)) {
+ allowed = true;
+ break;
+ }
+ }
+
+ if (!allowed) {
+ event.preventDefault();
+ if (Authentication.user !== null && typeof Authentication.user === 'object') {
+ $state.transitionTo('forbidden');
+ } else {
+ $state.go('authentication.signin').then(function () {
+ // Record previous state
+ storePreviousState(toState, toParams);
+ });
+ }
+ }
}
- }
- }
+ })
+ .catch(function (errorResponse) {
+ $log.error(errorResponse.data);
+ });
}
function stateChangeSuccess(event, toState, toParams, fromState, fromParams) {
diff --git a/modules/core/client/services/interceptors/auth-interceptor.client.service.js b/modules/core/client/services/interceptors/auth-interceptor.client.service.js
index 89bae6f023..37aa6212db 100644
--- a/modules/core/client/services/interceptors/auth-interceptor.client.service.js
+++ b/modules/core/client/services/interceptors/auth-interceptor.client.service.js
@@ -5,9 +5,9 @@
.module('core')
.factory('authInterceptor', authInterceptor);
- authInterceptor.$inject = ['$q', '$injector', 'Authentication'];
+ authInterceptor.$inject = ['$q', '$injector'];
- function authInterceptor($q, $injector, Authentication) {
+ function authInterceptor($q, $injector) {
var service = {
responseError: responseError
};
@@ -22,7 +22,7 @@
break;
case 401:
// Deauthenticate the global user
- Authentication.user = null;
+ $injector.get('Authentication').user = null;
$injector.get('$state').transitionTo('authentication.signin');
break;
case 403:
diff --git a/modules/core/client/services/socket.io.client.service.js b/modules/core/client/services/socket.io.client.service.js
index a00299ae8a..666b54a02a 100644
--- a/modules/core/client/services/socket.io.client.service.js
+++ b/modules/core/client/services/socket.io.client.service.js
@@ -26,6 +26,7 @@
// Connect only when authenticated
if (Authentication.user) {
service.socket = io();
+ service.socket.emit('authentication', { token: Authentication.token });
}
}
diff --git a/modules/core/client/views/header.client.view.html b/modules/core/client/views/header.client.view.html
index d35f961dc5..1a7c1a4919 100644
--- a/modules/core/client/views/header.client.view.html
+++ b/modules/core/client/views/header.client.view.html
@@ -41,7 +41,7 @@
- Signout
+ Signout
diff --git a/modules/core/tests/client/socket.io.client.service.tests.js b/modules/core/tests/client/socket.io.client.service.tests.js
index 02def5d065..587b9342fd 100644
--- a/modules/core/tests/client/socket.io.client.service.tests.js
+++ b/modules/core/tests/client/socket.io.client.service.tests.js
@@ -24,6 +24,10 @@
function connect() {
io.socket = {};
+
+ // Mock authentication
+ io.on('authentication', function (msg, data) {
+ });
}
function emit(msg, data) {
diff --git a/modules/users/client/controllers/authentication.client.controller.js b/modules/users/client/controllers/authentication.client.controller.js
index 23379ce384..29764ad108 100644
--- a/modules/users/client/controllers/authentication.client.controller.js
+++ b/modules/users/client/controllers/authentication.client.controller.js
@@ -66,8 +66,8 @@
// Authentication Callbacks
function onUserSignupSuccess(response) {
- // If successful we assign the response to the global user model
- vm.authentication.user = response;
+ // If successful we login the user client-side using the JWT token
+ Authentication.login(response.user, response.token);
Notification.success({ message: ' Signup successful!' });
// And redirect to the previous or home page
$state.go($state.previous.state.name || 'home', $state.previous.params);
@@ -78,9 +78,9 @@
}
function onUserSigninSuccess(response) {
- // If successful we assign the response to the global user model
- vm.authentication.user = response;
- Notification.info({ message: 'Welcome ' + response.firstName });
+ // If successful we login the user client-side using the JWT Token
+ Authentication.login(response.user, response.token);
+ Notification.info({ message: 'Welcome ' + response.user.firstName });
// And redirect to the previous or home page
$state.go($state.previous.state.name || 'home', $state.previous.params);
}
diff --git a/modules/users/client/controllers/password.client.controller.js b/modules/users/client/controllers/password.client.controller.js
index 856d87f7ff..c82ce8425b 100644
--- a/modules/users/client/controllers/password.client.controller.js
+++ b/modules/users/client/controllers/password.client.controller.js
@@ -66,8 +66,8 @@
// If successful show success message and clear form
vm.passwordDetails = null;
- // Attach user profile
- Authentication.user = response;
+ // Login the user
+ Authentication.login(response.user, response.token);
Notification.success({ message: ' Password reset successful!' });
// And redirect to the index page
$location.path('/password/reset/success');
diff --git a/modules/users/client/services/authentication.client.service.js b/modules/users/client/services/authentication.client.service.js
index 9e6b83d663..904cbdd125 100644
--- a/modules/users/client/services/authentication.client.service.js
+++ b/modules/users/client/services/authentication.client.service.js
@@ -7,13 +7,71 @@
.module('users.services')
.factory('Authentication', Authentication);
- Authentication.$inject = ['$window'];
+ Authentication.$inject = ['$window', '$state', '$http', '$location', '$q', 'UsersService'];
+
+ function Authentication($window, $state, $http, $location, $q, UsersService) {
- function Authentication($window) {
var auth = {
- user: $window.user
+ user: null,
+ token: null,
+ login: login,
+ signout: signout,
+ refresh: refresh,
+ ready: $q.defer()
};
+ // Initialize service
+ init();
+
return auth;
+
+ function init() {
+ var token = localStorage.getItem('token') || $location.search().token || null;
+ // Remove the token from the URL if present
+ $location.search('token', null);
+
+ if (token) {
+ auth.token = token;
+ $http.defaults.headers.common.Authorization = 'JWT ' + token;
+
+ refresh();
+ } else {
+ auth.ready.resolve();
+ }
+ }
+
+ function login(user, token) {
+ auth.user = user;
+ auth.token = token;
+
+ localStorage.setItem('token', token);
+ $http.defaults.headers.common.Authorization = 'JWT ' + token;
+
+ auth.ready.resolve();
+ }
+
+ function signout() {
+ localStorage.removeItem('token');
+ auth.user = null;
+ auth.token = null;
+
+ $state.go('home', { reload: true });
+ }
+
+ function refresh() {
+ UsersService.me().$promise
+ .then(function (user) {
+ if (!user || !user.roles || !user.roles.length) {
+ signout();
+ return auth.ready.resolve();
+ }
+
+ auth.user = user;
+ auth.ready.resolve();
+ })
+ .catch(function (errorResponse) {
+ auth.ready.reject(errorResponse);
+ });
+ }
}
}());
diff --git a/modules/users/client/services/users.client.service.js b/modules/users/client/services/users.client.service.js
index dd51119448..0b839074a5 100644
--- a/modules/users/client/services/users.client.service.js
+++ b/modules/users/client/services/users.client.service.js
@@ -13,6 +13,10 @@
update: {
method: 'PUT'
},
+ me: {
+ method: 'GET',
+ url: '/api/users/me'
+ },
updatePassword: {
method: 'POST',
url: '/api/users/password'
diff --git a/modules/users/server/config/strategies/jwt.js b/modules/users/server/config/strategies/jwt.js
new file mode 100644
index 0000000000..96093897d4
--- /dev/null
+++ b/modules/users/server/config/strategies/jwt.js
@@ -0,0 +1,35 @@
+'use strict';
+
+/**
+ * Module dependencies
+ */
+var passport = require('passport'),
+ JwtStrategy = require('passport-jwt').Strategy,
+ ExtractJwt = require('passport-jwt').ExtractJwt,
+ User = require('mongoose').model('User');
+
+module.exports = function (config) {
+ var opts = {
+ jwtFromRequest: ExtractJwt.fromExtractors([
+ ExtractJwt.fromUrlQueryParameter('token'),
+ ExtractJwt.fromAuthHeader()
+ ]),
+ secretOrKey: config.jwt.secret
+ // opts.issuer = "accounts.examplesoft.com",
+ // opts.audience = "yoursite.net"
+ };
+
+ passport.use(new JwtStrategy(opts, function (jwtPayload, done) {
+ User.findById({ _id: jwtPayload.user }, '-salt -password', function (err, user) {
+ if (err) {
+ return done(err, false);
+ }
+
+ if (!user) {
+ return done('User not found');
+ }
+
+ return done(null, user);
+ });
+ }));
+};
diff --git a/modules/users/server/config/users.server.config.js b/modules/users/server/config/users.server.config.js
index 2bfedceb01..0f5811bc45 100644
--- a/modules/users/server/config/users.server.config.js
+++ b/modules/users/server/config/users.server.config.js
@@ -12,20 +12,6 @@ var passport = require('passport'),
* Module init function
*/
module.exports = function (app, db) {
- // Serialize sessions
- passport.serializeUser(function (user, done) {
- done(null, user.id);
- });
-
- // Deserialize sessions
- passport.deserializeUser(function (id, done) {
- User.findOne({
- _id: id
- }, '-salt -password', function (err, user) {
- done(err, user);
- });
- });
-
// Initialize strategies
config.utils.getGlobbedPaths(path.join(__dirname, './strategies/**/*.js')).forEach(function (strategy) {
require(path.resolve(strategy))(config);
@@ -33,5 +19,4 @@ module.exports = function (app, db) {
// Add passport's middleware
app.use(passport.initialize());
- app.use(passport.session());
};
diff --git a/modules/users/server/controllers/users/users.authentication.server.controller.js b/modules/users/server/controllers/users/users.authentication.server.controller.js
index 75373b470d..3c606eb8d0 100644
--- a/modules/users/server/controllers/users/users.authentication.server.controller.js
+++ b/modules/users/server/controllers/users/users.authentication.server.controller.js
@@ -7,7 +7,8 @@ var path = require('path'),
errorHandler = require(path.resolve('./modules/core/server/controllers/errors.server.controller')),
mongoose = require('mongoose'),
passport = require('passport'),
- User = mongoose.model('User');
+ User = mongoose.model('User'),
+ authorization = require(path.resolve('./config/lib/authorization'));
// URLs for which user can't be redirected on signin
var noReturnUrls = [
@@ -38,13 +39,8 @@ exports.signup = function (req, res) {
user.password = undefined;
user.salt = undefined;
- req.login(user, function (err) {
- if (err) {
- res.status(400).send(err);
- } else {
- res.json(user);
- }
- });
+ var token = authorization.signToken(user);
+ res.json({ user: user, token: token });
}
});
};
@@ -61,13 +57,8 @@ exports.signin = function (req, res, next) {
user.password = undefined;
user.salt = undefined;
- req.login(user, function (err) {
- if (err) {
- res.status(400).send(err);
- } else {
- res.json(user);
- }
- });
+ var token = authorization.signToken(user);
+ res.json({ user: user, token: token });
}
})(req, res, next);
};
@@ -86,7 +77,7 @@ exports.signout = function (req, res) {
exports.oauthCall = function (strategy, scope) {
return function (req, res, next) {
if (req.query && req.query.redirect_to)
- req.session.redirect_to = req.query.redirect_to;
+ req.redirect_to = req.query.redirect_to;
// Authenticate
passport.authenticate(strategy, scope)(req, res, next);
@@ -107,13 +98,15 @@ exports.oauthCallback = function (strategy) {
if (!user) {
return res.redirect('/authentication/signin');
}
- req.login(user, function (err) {
- if (err) {
- return res.redirect('/authentication/signin');
- }
- return res.redirect(info.redirect_to || '/');
- });
+ var token = authorization.signToken(user);
+ var redirect = info.redirect_to || '/';
+
+ if (token) {
+ redirect += '?token=' + token;
+ }
+
+ return res.redirect(redirect);
})(req, res, next);
};
};
@@ -127,8 +120,8 @@ exports.saveOAuthUserProfile = function (req, providerUserProfile, done) {
// Set redirection path on session.
// Do not redirect to a signin or signup page
- if (noReturnUrls.indexOf(req.session.redirect_to) === -1)
- info.redirect_to = req.session.redirect_to;
+ if (noReturnUrls.indexOf(req.redirect_to) === -1)
+ info.redirect_to = req.redirect_to;
if (!req.user) {
// Define a search query fields
@@ -237,13 +230,7 @@ exports.removeOAuthProvider = function (req, res, next) {
message: errorHandler.getErrorMessage(err)
});
} else {
- req.login(user, function (err) {
- if (err) {
- return res.status(400).send(err);
- } else {
- return res.json(user);
- }
- });
+ return res.json(user);
}
});
};
diff --git a/modules/users/server/controllers/users/users.password.server.controller.js b/modules/users/server/controllers/users/users.password.server.controller.js
index 7f3153ba1f..0465aa1111 100644
--- a/modules/users/server/controllers/users/users.password.server.controller.js
+++ b/modules/users/server/controllers/users/users.password.server.controller.js
@@ -10,7 +10,8 @@ var path = require('path'),
User = mongoose.model('User'),
nodemailer = require('nodemailer'),
async = require('async'),
- crypto = require('crypto');
+ crypto = require('crypto'),
+ authorization = require(path.resolve('./config/lib/authorization'));
var smtpTransport = nodemailer.createTransport(config.mailer.options);
@@ -145,19 +146,13 @@ exports.reset = function (req, res, next) {
message: errorHandler.getErrorMessage(err)
});
} else {
- req.login(user, function (err) {
- if (err) {
- res.status(400).send(err);
- } else {
- // Remove sensitive data before return authenticated user
- user.password = undefined;
- user.salt = undefined;
+ user.password = undefined;
+ user.salt = undefined;
- res.json(user);
+ var token = authorization.signToken(user);
+ res.json({ user: user, token: token });
- done(err, user);
- }
- });
+ done(err, user);
}
});
} else {
@@ -221,14 +216,8 @@ exports.changePassword = function (req, res, next) {
message: errorHandler.getErrorMessage(err)
});
} else {
- req.login(user, function (err) {
- if (err) {
- res.status(400).send(err);
- } else {
- res.send({
- message: 'Password changed successfully'
- });
- }
+ res.send({
+ message: 'Password changed successfully'
});
}
});
diff --git a/modules/users/server/controllers/users/users.profile.server.controller.js b/modules/users/server/controllers/users/users.profile.server.controller.js
index 52fc23cdc7..51d37f4948 100644
--- a/modules/users/server/controllers/users/users.profile.server.controller.js
+++ b/modules/users/server/controllers/users/users.profile.server.controller.js
@@ -35,13 +35,7 @@ exports.update = function (req, res) {
message: errorHandler.getErrorMessage(err)
});
} else {
- req.login(user, function (err) {
- if (err) {
- res.status(400).send(err);
- } else {
- res.json(user);
- }
- });
+ res.json(user);
}
});
} else {
@@ -68,7 +62,6 @@ exports.changeProfilePicture = function (req, res) {
uploadImage()
.then(updateUser)
.then(deleteOldImage)
- .then(login)
.then(function () {
res.json(user);
})
@@ -124,18 +117,6 @@ exports.changeProfilePicture = function (req, res) {
}
});
}
-
- function login () {
- return new Promise(function (resolve, reject) {
- req.login(user, function (err) {
- if (err) {
- res.status(400).send(err);
- } else {
- resolve();
- }
- });
- });
- }
};
/**
diff --git a/modules/users/tests/client/authentication.client.controller.tests.js b/modules/users/tests/client/authentication.client.controller.tests.js
index f2e99e1fec..e3fc18337b 100644
--- a/modules/users/tests/client/authentication.client.controller.tests.js
+++ b/modules/users/tests/client/authentication.client.controller.tests.js
@@ -51,6 +51,8 @@
$httpBackend.whenGET('/modules/core/client/views/home.client.view.html').respond(200);
$httpBackend.whenGET('/modules/core/client/views/400.client.view.html').respond(200);
+ $httpBackend.when('GET', '/api/users/me').respond(200, {});
+
// Initialize the Authentication controller
AuthenticationController = $controller('AuthenticationController as vm', {
$scope: scope
@@ -62,7 +64,7 @@
$templateCache.put('/modules/core/client/views/home.client.view.html', '');
// Test expected GET request
- $httpBackend.when('POST', '/api/auth/signin').respond(200, { username: 'Fred' });
+ $httpBackend.when('POST', '/api/auth/signin').respond(200, { user: { username: 'Fred' }, token: 'somejwttoken' });
scope.vm.signin(true);
$httpBackend.flush();
@@ -75,7 +77,8 @@
it('should login with a correct email and password', inject(function ($templateCache) {
$templateCache.put('/modules/core/client/views/home.client.view.html', '');
// Test expected GET request
- $httpBackend.when('POST', '/api/auth/signin').respond(200, { email: 'Fred@email.com' });
+ $httpBackend.when('POST', '/api/auth/signin').respond(200, { user: { email: 'Fred@email.com' }, token: 'somejwttoken' });
+ $httpBackend.when('GET', '/api/users/me').respond(200, 'Fred');
scope.vm.signin(true);
$httpBackend.flush();
@@ -100,7 +103,8 @@
spyOn($state, 'go');
// Test expected GET request
- $httpBackend.when('POST', '/api/auth/signin').respond(200, 'Fred');
+ $httpBackend.when('POST', '/api/auth/signin').respond(200, { user: 'Fred' });
+ $httpBackend.when('GET', '/api/users/me').respond(200, { user: 'Fred' });
scope.vm.signin(true);
$httpBackend.flush();
@@ -148,7 +152,8 @@
// Test expected GET request
scope.vm.authentication.user = 'Fred';
- $httpBackend.when('POST', '/api/auth/signup').respond(200, { username: 'Fred' });
+ $httpBackend.when('POST', '/api/auth/signup').respond(200, { user: { username: 'Fred' }, token: 'somejwttoken' });
+ $httpBackend.when('GET', '/api/users/me').respond(200, { user: 'Fred' });
scope.vm.signup(true);
$httpBackend.flush();
@@ -181,6 +186,8 @@
$location = _$location_;
$location.path = jasmine.createSpy().and.returnValue(true);
+ $httpBackend.when('GET', '/api/users/me').respond(200, {});
+
// Mock logged in user
_Authentication_.user = {
username: 'test',
diff --git a/modules/users/tests/client/edit-profile.client.controller.tests.js b/modules/users/tests/client/edit-profile.client.controller.tests.js
index e608a1bb45..cd516e0ddb 100644
--- a/modules/users/tests/client/edit-profile.client.controller.tests.js
+++ b/modules/users/tests/client/edit-profile.client.controller.tests.js
@@ -62,12 +62,18 @@
roles: ['user']
};
+ $httpBackend.whenGET('/api/users/me').respond(Authentication.user);
+
// Initialize the Articles controller.
EditProfileController = $controller('EditProfileController as vm', {
$scope: $scope
});
}));
+ afterEach(inject(function (Authentication) {
+ Authentication.signout();
+ }));
+
describe('Update User Profile', function () {
it('should have user context', inject(function (UsersService) {
diff --git a/modules/users/tests/client/password-validator.client.directive.tests.js b/modules/users/tests/client/password-validator.client.directive.tests.js
index c6f318349c..65cb0a2cfd 100644
--- a/modules/users/tests/client/password-validator.client.directive.tests.js
+++ b/modules/users/tests/client/password-validator.client.directive.tests.js
@@ -7,15 +7,19 @@
var scope,
element,
$compile,
+ $httpBackend,
form;
// Load the main application module
beforeEach(module(ApplicationConfiguration.applicationModuleName));
- beforeEach(inject(function(_$rootScope_, _$compile_) {
+ beforeEach(inject(function(_$rootScope_, _$compile_, _$httpBackend_) {
// Set a new global scope
scope = _$rootScope_.$new();
$compile = _$compile_;
+ $httpBackend = _$httpBackend_;
+
+ $httpBackend.whenGET('/api/users/me').respond({});
scope.passwordMock = {
password: 'P@ssw0rd!!'
diff --git a/modules/users/tests/client/password-verify.client.directive.tests.js b/modules/users/tests/client/password-verify.client.directive.tests.js
index 24b41dcd5a..046abe07c6 100644
--- a/modules/users/tests/client/password-verify.client.directive.tests.js
+++ b/modules/users/tests/client/password-verify.client.directive.tests.js
@@ -7,15 +7,19 @@
var scope,
element,
$compile,
+ $httpBackend,
form;
// Load the main application module
beforeEach(module(ApplicationConfiguration.applicationModuleName));
- beforeEach(inject(function(_$rootScope_, _$compile_) {
+ beforeEach(inject(function(_$rootScope_, _$compile_, _$httpBackend_) {
// Set a new global scope
scope = _$rootScope_.$new();
$compile = _$compile_;
+ $httpBackend = _$httpBackend_;
+
+ $httpBackend.whenGET('/api/users/me').respond({});
scope.passwordMock = {
newPassword: 'P@ssw0rd!!',
diff --git a/modules/users/tests/client/password.client.controller.tests.js b/modules/users/tests/client/password.client.controller.tests.js
index 3bb87cc588..42d7067843 100644
--- a/modules/users/tests/client/password.client.controller.tests.js
+++ b/modules/users/tests/client/password.client.controller.tests.js
@@ -6,6 +6,7 @@
// Initialize global variables
var PasswordController,
scope,
+ Authentication,
$httpBackend,
$stateParams,
$location,
@@ -35,6 +36,7 @@
scope = $rootScope.$new();
// Point global variables to injected services
+ Authentication = _Authentication_;
$stateParams = _$stateParams_;
$httpBackend = _$httpBackend_;
$location = _$location_;
@@ -42,9 +44,10 @@
// Ignore parent template gets on state transition
$httpBackend.whenGET('/modules/core/client/views/404.client.view.html').respond(200);
+ $httpBackend.whenGET('/api/users/me').respond({ user: { username: 'test', roles: ['user'] } });
// Mock logged in user
- _Authentication_.user = {
+ Authentication.user = {
username: 'test',
roles: ['user']
};
@@ -55,17 +58,22 @@
});
}));
+ afterEach(inject(function (Authentication) {
+ Authentication.signout();
+ }));
+
it('should redirect logged in user to home', function() {
expect($location.path).toHaveBeenCalledWith('/');
});
});
describe('Logged out user', function() {
- beforeEach(inject(function($controller, $rootScope, _$window_, _$stateParams_, _$httpBackend_, _$location_, _Notification_) {
+ beforeEach(inject(function($controller, $rootScope, _$window_, _$stateParams_, _$httpBackend_, _$location_, _Notification_, _Authentication_) {
// Set a new global scope
scope = $rootScope.$new();
// Point global variables to injected services
+ Authentication = _Authentication_;
$stateParams = _$stateParams_;
$httpBackend = _$httpBackend_;
$location = _$location_;
@@ -77,6 +85,10 @@
spyOn(Notification, 'error');
spyOn(Notification, 'success');
+ Authentication.user = null;
+
+ $httpBackend.whenGET('/api/users/me').respond({ user: null });
+
// Ignore parent template gets on state transition
$httpBackend.whenGET('/modules/core/client/views/404.client.view.html').respond(200);
$httpBackend.whenGET('/modules/core/client/views/400.client.view.html').respond(200);
@@ -87,6 +99,10 @@
});
}));
+ afterEach(inject(function (Authentication) {
+ Authentication.signout();
+ }));
+
it('should not redirect to home', function() {
expect($location.path).not.toHaveBeenCalledWith('/');
});
@@ -168,7 +184,7 @@
username: 'test'
};
beforeEach(function() {
- $httpBackend.when('POST', '/api/auth/reset/' + token, passwordDetails).respond(user);
+ $httpBackend.when('POST', '/api/auth/reset/' + token, passwordDetails).respond({ user: user });
scope.vm.resetUserPassword(true);
$httpBackend.flush();
diff --git a/modules/users/tests/e2e/users.e2e.tests.js b/modules/users/tests/e2e/users.e2e.tests.js
index 02d27ede02..4907b2df18 100644
--- a/modules/users/tests/e2e/users.e2e.tests.js
+++ b/modules/users/tests/e2e/users.e2e.tests.js
@@ -22,6 +22,8 @@ describe('Users E2E Tests:', function () {
browser.get('http://localhost:3001/authentication/signout');
// Delete all cookies
browser.driver.manage().deleteAllCookies();
+ // Clear local storage
+ browser.executeScript('localStorage.clear();');
};
describe('Signup Validation', function () {
diff --git a/modules/users/tests/server/user.server.routes.tests.js b/modules/users/tests/server/user.server.routes.tests.js
index 6a51d63362..be1e8ccbed 100644
--- a/modules/users/tests/server/user.server.routes.tests.js
+++ b/modules/users/tests/server/user.server.routes.tests.js
@@ -79,13 +79,13 @@ describe('User CRUD tests', function () {
return done(signupErr);
}
- signupRes.body.username.should.equal(_user.username);
- signupRes.body.email.should.equal(_user.email);
+ signupRes.body.user.username.should.equal(_user.username);
+ signupRes.body.user.email.should.equal(_user.email);
// Assert a proper profile image has been set, even if by default
- signupRes.body.profileImageURL.should.not.be.empty();
+ signupRes.body.user.profileImageURL.should.not.be.empty();
// Assert we have just the default 'user' role
- signupRes.body.roles.should.be.instanceof(Array).and.have.lengthOf(1);
- signupRes.body.roles.indexOf('user').should.equal(0);
+ signupRes.body.user.roles.should.be.instanceof(Array).and.have.lengthOf(1);
+ signupRes.body.user.roles.indexOf('user').should.equal(0);
return done();
});
});
@@ -103,6 +103,7 @@ describe('User CRUD tests', function () {
// Logout
agent.get('/api/auth/signout')
+ .set('Authorization', 'JWT ' + signinRes.body.token)
.expect(302)
.end(function (signoutErr, signoutRes) {
if (signoutErr) {
@@ -170,6 +171,7 @@ describe('User CRUD tests', function () {
// Request list of users
agent.get('/api/users')
+ .set('Authorization', 'JWT ' + signinRes.body.token)
.expect(403)
.end(function (usersGetErr, usersGetRes) {
if (usersGetErr) {
@@ -197,6 +199,7 @@ describe('User CRUD tests', function () {
// Request list of users
agent.get('/api/users')
+ .set('Authorization', 'JWT ' + signinRes.body.token)
.expect(200)
.end(function (usersGetErr, usersGetRes) {
if (usersGetErr) {
@@ -228,6 +231,7 @@ describe('User CRUD tests', function () {
// Get single user information from the database
agent.get('/api/users/' + user._id)
+ .set('Authorization', 'JWT ' + signinRes.body.token)
.expect(200)
.end(function (userInfoErr, userInfoRes) {
if (userInfoErr) {
@@ -267,6 +271,7 @@ describe('User CRUD tests', function () {
};
agent.put('/api/users/' + user._id)
+ .set('Authorization', 'JWT ' + signinRes.body.token)
.send(userUpdate)
.expect(200)
.end(function (userInfoErr, userInfoRes) {
@@ -302,6 +307,7 @@ describe('User CRUD tests', function () {
}
agent.delete('/api/users/' + user._id)
+ .set('Authorization', 'JWT ' + signinRes.body.token)
.expect(200)
.end(function (userInfoErr, userInfoRes) {
if (userInfoErr) {
@@ -496,6 +502,7 @@ describe('User CRUD tests', function () {
// Change password
agent.post('/api/users/password')
+ .set('Authorization', 'JWT ' + signinRes.body.token)
.send({
newPassword: '1234567890Aa$',
verifyPassword: '1234567890Aa$',
@@ -525,6 +532,7 @@ describe('User CRUD tests', function () {
// Change password
agent.post('/api/users/password')
+ .set('Authorization', 'JWT ' + signinRes.body.token)
.send({
newPassword: '1234567890Aa$',
verifyPassword: '1234567890-ABC-123-Aa$',
@@ -554,6 +562,7 @@ describe('User CRUD tests', function () {
// Change password
agent.post('/api/users/password')
+ .set('Authorization', 'JWT ' + signinRes.body.token)
.send({
newPassword: '1234567890Aa$',
verifyPassword: '1234567890Aa$',
@@ -583,6 +592,7 @@ describe('User CRUD tests', function () {
// Change password
agent.post('/api/users/password')
+ .set('Authorization', 'JWT ' + signinRes.body.token)
.send({
newPassword: '',
verifyPassword: '',
@@ -632,6 +642,7 @@ describe('User CRUD tests', function () {
// Get own user details
agent.get('/api/users/me')
+ .set('Authorization', 'JWT ' + signinRes.body.token)
.expect(200)
.end(function (err, res) {
if (err) {
@@ -682,6 +693,7 @@ describe('User CRUD tests', function () {
};
agent.put('/api/users')
+ .set('Authorization', 'JWT ' + signinRes.body.token)
.send(userUpdate)
.expect(200)
.end(function (userInfoErr, userInfoRes) {
@@ -724,6 +736,7 @@ describe('User CRUD tests', function () {
};
agent.put('/api/users')
+ .set('Authorization', 'JWT ' + signinRes.body.token)
.send(userUpdate)
.expect(200)
.end(function (userInfoErr, userInfoRes) {
@@ -781,6 +794,7 @@ describe('User CRUD tests', function () {
};
agent.put('/api/users')
+ .set('Authorization', 'JWT ' + signinRes.body.token)
.send(userUpdate)
.expect(422)
.end(function (userInfoErr, userInfoRes) {
@@ -833,6 +847,7 @@ describe('User CRUD tests', function () {
};
agent.put('/api/users')
+ .set('Authorization', 'JWT ' + signinRes.body.token)
.send(userUpdate)
.expect(422)
.end(function (userInfoErr, userInfoRes) {
@@ -874,6 +889,7 @@ describe('User CRUD tests', function () {
// Get own user details
agent.put('/api/users')
+ .set('Authorization', 'JWT ' + signinRes.body.token)
.send(userUpdate)
.expect(200)
.end(function (err, res) {
@@ -953,6 +969,7 @@ describe('User CRUD tests', function () {
}
agent.post('/api/users/picture')
+ .set('Authorization', 'JWT ' + signinRes.body.token)
.attach('newProfilePicture', './modules/users/client/img/profile/default.png')
.expect(200)
.end(function (userInfoErr, userInfoRes) {
@@ -981,6 +998,7 @@ describe('User CRUD tests', function () {
}
agent.post('/api/users/picture')
+ .set('Authorization', 'JWT ' + signinRes.body.token)
.attach('fieldThatDoesntWork', './modules/users/client/img/profile/default.png')
.send(credentials)
.expect(422)
@@ -1001,6 +1019,7 @@ describe('User CRUD tests', function () {
}
agent.post('/api/users/picture')
+ .set('Authorization', 'JWT ' + signinRes.body.token)
.attach('newProfilePicture', './modules/users/tests/server/img/text-file.txt')
.send(credentials)
.expect(422)
@@ -1014,13 +1033,14 @@ describe('User CRUD tests', function () {
agent.post('/api/auth/signin')
.send(credentials)
.expect(200)
- .end(function (signinErr) {
+ .end(function (signinErr, signinRes) {
// Handle signin error
if (signinErr) {
return done(signinErr);
}
agent.post('/api/users/picture')
+ .set('Authorization', 'JWT ' + signinRes.body.token)
.attach('newProfilePicture', './modules/users/tests/server/img/too-big-file.png')
.send(credentials)
.expect(422)
diff --git a/package.json b/package.json
index 05802e9320..0cf89f803c 100644
--- a/package.json
+++ b/package.json
@@ -51,6 +51,7 @@
"glob": "~7.1.0",
"helmet": "~2.3.0",
"jasmine-core": "~2.5.0",
+ "jsonwebtoken": "~5.5.4",
"lodash": "~4.16.2",
"lusca": "~1.4.1",
"method-override": "~2.3.5",
@@ -64,6 +65,7 @@
"passport-facebook": "~2.1.0",
"passport-github": "~1.1.0",
"passport-google-oauth": "~1.0.0",
+ "passport-jwt": "~2.2.1",
"passport-linkedin": "~1.0.0",
"passport-local": "~1.0.0",
"passport-paypal-openidconnect": "~0.1.1",
@@ -71,6 +73,7 @@
"phantomjs-prebuilt": "~2.1.4",
"serve-favicon": "~2.3.0",
"socket.io": "^1.4.8",
+ "socketio-auth": "0.0.5",
"validator": "~6.0.0",
"winston": "^2.2.0",
"wiredep": "~4.0.0"