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"