From 0dd3a1431e02df62f72197b19846cbd9a8572258 Mon Sep 17 00:00:00 2001 From: mleanos Date: Sat, 9 Apr 2016 21:43:56 -0700 Subject: [PATCH] feat(core): JWT Authentication simplified Implements JWT Authentication, and removes dependency of session storage. Closes #389 --- config/env/default.js | 6 ++ config/lib/authorization.js | 35 +++++++++++ config/lib/express.js | 22 +++++-- config/lib/socket.io.js | 45 ++++---------- .../client/config/core.client.route-filter.js | 7 ++- .../auth-interceptor.client.service.js | 6 +- .../services/socket.io.client.service.js | 2 +- .../core/client/views/header.client.view.html | 2 +- .../authentication.client.controller.js | 4 +- .../controllers/password.client.controller.js | 2 +- .../services/authentication.client.service.js | 61 ++++++++++++++++++- .../client/services/users.client.service.js | 4 ++ modules/users/server/config/strategies/jwt.js | 32 ++++++++++ .../users/server/config/strategies/local.js | 4 +- .../server/config/users.server.config.js | 15 ----- .../users.authentication.server.controller.js | 40 ++++-------- .../users/users.password.server.controller.js | 29 +++------ .../users/users.profile.server.controller.js | 8 +-- package.json | 1 + 19 files changed, 202 insertions(+), 123 deletions(-) create mode 100644 config/lib/authorization.js create mode 100644 modules/users/server/config/strategies/jwt.js diff --git a/config/env/default.js b/config/env/default.js index a3e8c5e8cc..e51c783d81 100644 --- a/config/env/default.js +++ b/config/env/default.js @@ -46,5 +46,11 @@ module.exports = { fileSize: 1 * 1024 * 1024 // Max file size in bytes (1 MB) } } + }, + 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..39d1ce03b9 --- /dev/null +++ b/config/lib/authorization.js @@ -0,0 +1,35 @@ +'use strict'; + +var config = require('../config'), + jwt = require('jsonwebtoken'), + lodash = require('lodash'); + +var auth = { + signToken: signToken +}; + +// Export the token auth service +module.exports = auth; + +// 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 c5b98f6bfd..dcec5b9d4b 100644 --- a/config/lib/express.js +++ b/config/lib/express.js @@ -18,8 +18,8 @@ var config = require('../config'), flash = require('connect-flash'), hbs = require('express-hbs'), path = require('path'), - _ = require('lodash'), - lusca = require('lusca'); + lusca = require('lusca'), + passport = require('passport'); /** * Initialize local variables @@ -88,6 +88,21 @@ module.exports.initMiddleware = function (app) { // Add the cookie parser and flash middleware app.use(cookieParser()); app.use(flash()); + + // Authorize JWT + app.use(function (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); + }); }; /** @@ -237,9 +252,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..0a46149ae9 100644 --- a/config/lib/socket.io.js +++ b/config/lib/socket.io.js @@ -6,11 +6,9 @@ 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); + ExtractJwt = require('passport-jwt').ExtractJwt; // Define the Socket.io configuration method module.exports = function (app, db) { @@ -69,40 +67,21 @@ 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 - }); - // 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; - - if (!sessionId) return next(new Error('sessionId was not found in socket.request'), false); - - // 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.initialize()(socket.request, {}, function () { + passport.authenticate('jwt', { session: false }, function (err, user) { + if (err) { + return next(new Error(err)); + } - // Set the Socket.io session information - socket.request.session = session; + if (user) { + socket.request.user = user; + } - // 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); - } - }); - }); - }); + next(); + })(socket.request, socket.request.res, next); }); }); diff --git a/modules/core/client/config/core.client.route-filter.js b/modules/core/client/config/core.client.route-filter.js index a68b70231f..181d95e8b6 100644 --- a/modules/core/client/config/core.client.route-filter.js +++ b/modules/core/client/config/core.client.route-filter.js @@ -8,7 +8,12 @@ routeFilter.$inject = ['$rootScope', '$state', 'Authentication']; function routeFilter($rootScope, $state, Authentication) { - $rootScope.$on('$stateChangeStart', stateChangeStart); + + Authentication.ready + .then(function (auth) { + $rootScope.$on('$stateChangeStart', stateChangeStart); + }); + $rootScope.$on('$stateChangeSuccess', stateChangeSuccess); function stateChangeStart(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 2929402804..20f1fc8a03 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 }; @@ -19,7 +19,7 @@ switch (rejection.status) { 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..9d94e6f835 100644 --- a/modules/core/client/services/socket.io.client.service.js +++ b/modules/core/client/services/socket.io.client.service.js @@ -25,7 +25,7 @@ function connect() { // Connect only when authenticated if (Authentication.user) { - service.socket = io(); + service.socket = io('', { query: 'auth_token=' + Authentication.token }); } } diff --git a/modules/core/client/views/header.client.view.html b/modules/core/client/views/header.client.view.html index c09e9770a8..ba0f02535d 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/users/client/controllers/authentication.client.controller.js b/modules/users/client/controllers/authentication.client.controller.js index e2f820cf82..97493a2fa2 100644 --- a/modules/users/client/controllers/authentication.client.controller.js +++ b/modules/users/client/controllers/authentication.client.controller.js @@ -35,7 +35,7 @@ $http.post('/api/auth/signup', vm.credentials).success(function (response) { // If successful we assign the response to the global user model - vm.authentication.user = response; + Authentication.login(response.user, response.token); // And redirect to the previous or home page $state.go($state.previous.state.name || 'home', $state.previous.params); @@ -55,7 +55,7 @@ $http.post('/api/auth/signin', vm.credentials).success(function (response) { // If successful we assign the response to the global user model - vm.authentication.user = response; + Authentication.login(response.user, response.token); // 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 4162d903fb..fe66777bea 100644 --- a/modules/users/client/controllers/password.client.controller.js +++ b/modules/users/client/controllers/password.client.controller.js @@ -57,7 +57,7 @@ vm.passwordDetails = null; // Attach user profile - Authentication.user = response; + Authentication.login(response.user, response.token); // 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..0ced369661 100644 --- a/modules/users/client/services/authentication.client.service.js +++ b/modules/users/client/services/authentication.client.service.js @@ -7,13 +7,68 @@ .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) { + var readyPromise = $q.defer(); - function Authentication($window) { var auth = { - user: $window.user + user: null, + token: null, + login: login, + signout: signout, + refresh: refresh, + ready: readyPromise.promise }; + // 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 { + readyPromise.resolve(auth); + } + } + + function login(user, token) { + auth.user = user; + auth.token = token; + + localStorage.setItem('token', token); + $http.defaults.headers.common.Authorization = 'JWT ' + token; + + readyPromise.resolve(auth); + } + + function signout() { + localStorage.removeItem('token'); + auth.user = null; + auth.token = null; + + $state.go('home', { reload: true }); + } + + function refresh(requestFromServer, callback) { + readyPromise = $q.defer(); + + UsersService.me().$promise + .then(function (user) { + auth.user = user; + readyPromise.resolve(auth); + }) + .catch(function (errorResponse) { + readyPromise.reject(errorResponse); + }); + } } }()); diff --git a/modules/users/client/services/users.client.service.js b/modules/users/client/services/users.client.service.js index 8037afbf3f..1af4d4f377 100644 --- a/modules/users/client/services/users.client.service.js +++ b/modules/users/client/services/users.client.service.js @@ -12,6 +12,10 @@ return $resource('api/users', {}, { update: { method: 'PUT' + }, + me: { + method: 'GET', + url: 'api/users/me' } }); } diff --git a/modules/users/server/config/strategies/jwt.js b/modules/users/server/config/strategies/jwt.js new file mode 100644 index 0000000000..5a11f3551b --- /dev/null +++ b/modules/users/server/config/strategies/jwt.js @@ -0,0 +1,32 @@ +'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.versionOneCompatibility({ tokenQueryParameterName: 'auth_token' }), + secretOrKey: config.jwt.secret + // opts.issuer = "accounts.examplesoft.com", + // opts.audience = "yoursite.net" + }; + + passport.use(new JwtStrategy(opts, function (jwt_payload, done) { + User.findById({ _id: jwt_payload.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/strategies/local.js b/modules/users/server/config/strategies/local.js index 9caa9091ff..cf88d838f4 100644 --- a/modules/users/server/config/strategies/local.js +++ b/modules/users/server/config/strategies/local.js @@ -21,9 +21,9 @@ module.exports = function () { return done(err); } if (!user || !user.authenticate(password)) { - return done(null, false, { + return done({ message: 'Invalid username or password' - }); + }, false); } 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 56a962e8e9..0e87ebea66 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 }); } }); }; @@ -53,21 +49,16 @@ exports.signup = function (req, res) { * Signin after passport authentication */ exports.signin = function (req, res, next) { - passport.authenticate('local', function (err, user, info) { + passport.authenticate('local', function (err, user) { if (err || !user) { - res.status(400).send(info); + res.status(400).send(err); } else { // Remove sensitive data before login 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); }; @@ -111,12 +102,9 @@ 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 || sessionRedirectURL || '/'); + var token = authorization.signToken(user); + return res.redirect(info || sessionRedirectURL || '/'); }); })(req, res, next); }; @@ -229,13 +217,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 06197fba73..f1ea9c95e1 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); @@ -147,19 +148,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 { @@ -223,14 +218,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 99f8e7bc99..ef9fb97592 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 { diff --git a/package.json b/package.json index bcecd37843..a9754f5621 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "passport-facebook": "~2.1.0", "passport-github": "~1.1.0", "passport-google-oauth": "~1.0.0", + "passport-jwt": "~2.0.0", "passport-linkedin": "~1.0.0", "passport-local": "~1.0.0", "passport-paypal-openidconnect": "~0.1.1",