From e094d231b25f03486b48a92735aa99ff43fee9ab Mon Sep 17 00:00:00 2001 From: Damian Fortuna Date: Wed, 18 Nov 2015 17:36:24 -0300 Subject: [PATCH] Add ability to generate secret dynamically This allow you to pass a function instead of an string in order to generate secret based on the new connection features. --- README.md | 25 ++++++ lib/index.js | 66 +++++++++++++-- test/authorizer_secret_function_noqs.test.js | 77 +++++++++++++++++ test/authorizer_secret_function_qs.test.js | 73 ++++++++++++++++ test/fixture/secret_function.js | 89 ++++++++++++++++++++ 5 files changed, 321 insertions(+), 9 deletions(-) create mode 100644 test/authorizer_secret_function_noqs.test.js create mode 100644 test/authorizer_secret_function_qs.test.js create mode 100644 test/fixture/secret_function.js diff --git a/README.md b/README.md index 4906f2e..be41081 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,31 @@ socket.on("error", function(error) { console.log("User's token has expired"); } }); +``` +## Getting the secret dynamically +You can pass a function instead of an string when configuring secret. +This function receives the request, the decoded token and a callback. This +way, you are allowed to use a different secret based on the request and / or +the provided token. + +__Server side__: + +```javascript +var SECRETS = { + 'user1': 'secret 1', + 'user2': 'secret 2' +} + +io.use(socketioJwt.authorize({ + secret: function(request, decodedToken, callback) { + // SECRETS[decodedToken.userId] will be used a a secret or + // public key for connection user. + + callback(null, SECRETS[decodedToken.userId]); + }, + handshake: false +})); + ``` ## Contribute diff --git a/lib/index.js b/lib/index.js index 48da0bd..41d951b 100644 --- a/lib/index.js +++ b/lib/index.js @@ -40,19 +40,19 @@ function noQsMethod(options) { return; // stop logic, socket will be close on next tick } }; - + if(typeof data.token !== "string") { return onError({message: 'invalid token datatype'}, 'invalid_token'); } - - jwt.verify(data.token, options.secret, options, function(err, decoded) { + + var onJwtVerificationReady = function(err, decoded) { if (err) { return onError(err, 'invalid_token'); } // success handler - var onSuccess = function(){ + var onSuccess = function() { socket.decoded_token = decoded; socket.emit('authenticated'); if (server.$emit) { @@ -73,9 +73,18 @@ function noQsMethod(options) { } else { onSuccess(); } - }); - }); + }; + var onSecretReady = function(err, secret) { + if (err || !secret) { + return onError(err, 'invalid_secret'); + } + + jwt.verify(data.token, secret, options, onJwtVerificationReady); + }; + + getSecret(socket.request, options.secret, data.token, onSecretReady); + }); }; } @@ -140,18 +149,57 @@ function authorize(options, onConnection) { return auth.fail(error, data, accept); } - jwt.verify(token, options.secret, options, function(err, decoded) { + var onJwtVerificationReady = function(err, decoded) { if (err) { - error = new UnauthorizedError('invalid_token', err); + error = new UnauthorizedError(err.code || 'invalid_token', err); return auth.fail(error, data, accept); } data.decoded_token = decoded; return auth.success(data, accept); - }); + }; + + var onSecretReady = function(err, secret) { + if (err) { + error = new UnauthorizedError(err.code || 'invalid_secret', err); + return auth.fail(error, data, accept); + } + + jwt.verify(token, secret, options, onJwtVerificationReady); + }; + + getSecret(req, options.secret, token, onSecretReady); }; } +function getSecret(request, secret, token, callback) { + if (typeof secret === 'function') { + if (!token) { + return callback({ code: 'invalid_token', message: 'jwt must be provided' }); + } + + var parts = token.split('.'); + + if (parts.length < 3) { + return callback({ code: 'invalid_token', message: 'jwt malformed' }); + } + + if (parts[2].trim() === '') { + return callback({ code: 'invalid_token', message: 'jwt signature is required' }); + } + + var decodedToken = jwt.decode(token); + + if (!decodedToken) { + return callback({ code: 'invalid_token', message: 'jwt malformed' }); + } + + secret(request, decodedToken, callback); + } else { + callback(null, secret); + } +}; + exports.authorize = authorize; diff --git a/test/authorizer_secret_function_noqs.test.js b/test/authorizer_secret_function_noqs.test.js new file mode 100644 index 0000000..912eed0 --- /dev/null +++ b/test/authorizer_secret_function_noqs.test.js @@ -0,0 +1,77 @@ +var fixture = require('./fixture/secret_function'); +var request = require('request'); +var io = require('socket.io-client'); + +describe('authorizer with secret function', function () { + + //start and stop the server + before(function (done) { + fixture.start({ + handshake: false + } , done); + }); + + after(fixture.stop); + + describe('when the user is not logged in', function () { + + describe('and when token is not valid', function() { + beforeEach(function (done) { + request.post({ + url: 'http://localhost:9000/login', + json: { username: 'invalid_signature', password: 'Pa123' } + }, function (err, resp, body) { + this.invalidToken = body.token; + done(); + }.bind(this)); + }); + + it('should emit unauthorized', function (done){ + var socket = io.connect('http://localhost:9000', { + 'forceNew':true, + }); + + var invalidToken = this.invalidToken; + socket.on('unauthorized', function() { + done(); + }); + + socket.on('connect', function(){ + socket + .emit('authenticate', { token: invalidToken + 'ass' }) + }); + }); + }); + + }); + + describe('when the user is logged in', function() { + + beforeEach(function (done) { + request.post({ + url: 'http://localhost:9000/login', + json: { username: 'valid_signature', password: 'Pa123' } + }, function (err, resp, body) { + this.token = body.token; + done(); + }.bind(this)); + }); + + it('should do the handshake and connect', function (done){ + var socket = io.connect('http://localhost:9000', { + 'forceNew':true, + }); + var token = this.token; + socket.on('connect', function(){ + socket.on('echo-response', function () { + socket.close(); + done(); + }).on('authenticated', function () { + socket.emit('echo'); + }) + .emit('authenticate', { token: token }) + }); + }); + }); + +}); diff --git a/test/authorizer_secret_function_qs.test.js b/test/authorizer_secret_function_qs.test.js new file mode 100644 index 0000000..0d3467e --- /dev/null +++ b/test/authorizer_secret_function_qs.test.js @@ -0,0 +1,73 @@ +var fixture = require('./fixture/secret_function'); +var request = require('request'); +var io = require('socket.io-client'); + +describe('authorizer with secret function', function () { + + //start and stop the server + before(fixture.start); + after(fixture.stop); + + describe('when the user is not logged in', function () { + + it('should emit error with unauthorized handshake', function (done){ + var socket = io.connect('http://localhost:9000?token=boooooo', { + 'forceNew': true + }); + + socket.on('error', function(err){ + err.message.should.eql("jwt malformed"); + err.code.should.eql("invalid_token"); + socket.close(); + done(); + }); + }); + + }); + + describe('when the user is logged in', function() { + + beforeEach(function (done) { + request.post({ + url: 'http://localhost:9000/login', + json: { username: 'valid_signature', password: 'Pa123' } + }, function (err, resp, body) { + this.token = body.token; + done(); + }.bind(this)); + }); + + it('should do the handshake and connect', function (done){ + var socket = io.connect('http://localhost:9000', { + 'forceNew':true, + 'query': 'token=' + this.token + }); + socket.on('connect', function(){ + socket.close(); + done(); + }).on('error', done); + }); + }); + + describe('unsigned token', function() { + beforeEach(function () { + this.token = 'eyJhbGciOiJub25lIiwiY3R5IjoiSldUIn0.eyJuYW1lIjoiSm9obiBGb28ifQ.'; + }); + + it('should not do the handshake and connect', function (done){ + var socket = io.connect('http://localhost:9000', { + 'forceNew':true, + 'query': 'token=' + this.token + }); + socket.on('connect', function () { + socket.close(); + done(new Error('this shouldnt happen')); + }).on('error', function (err) { + socket.close(); + err.message.should.eql("jwt signature is required"); + done(); + }); + }); + }); + +}); diff --git a/test/fixture/secret_function.js b/test/fixture/secret_function.js new file mode 100644 index 0000000..48badd0 --- /dev/null +++ b/test/fixture/secret_function.js @@ -0,0 +1,89 @@ +var express = require('express'); +var http = require('http'); + +var socketIo = require('socket.io'); +var socketio_jwt = require('../../lib'); + +var jwt = require('jsonwebtoken'); + +var xtend = require('xtend'); +var bodyParser = require('body-parser'); + +var server, sio; +var enableDestroy = require('server-destroy'); + +exports.start = function (options, callback) { + var SECRETS = { + 123: 'aaafoo super sercret', + 555: 'other' + }; + + if(typeof options == 'function'){ + callback = options; + options = {}; + } + + options = xtend({ + secret: function(request, decodedToken, callback) { + callback(null, SECRETS[decodedToken.id]); + }, + timeout: 1000, + handshake: true + }, options); + + var app = express(); + + app.use(bodyParser.json()); + + app.post('/login', function (req, res) { + var profile = { + first_name: 'John', + last_name: 'Doe', + email: 'john@doe.com', + id: req.body.username === 'valid_signature' ? 123 : 555 + }; + + // We are sending the profile inside the token + var token = jwt.sign(profile, SECRETS[123], { expiresIn: 60*60*5 }); + + res.json({token: token}); + }); + + server = http.createServer(app); + + sio = socketIo.listen(server); + + if (options.handshake) { + sio.use(socketio_jwt.authorize(options)); + + sio.sockets.on('echo', function (m) { + sio.sockets.emit('echo-response', m); + }); + } else { + sio.sockets + .on('connection', socketio_jwt.authorize(options)) + .on('authenticated', function (socket) { + socket.on('echo', function (m) { + socket.emit('echo-response', m); + }); + }); + } + + server.__sockets = []; + server.on('connection', function (c) { + server.__sockets.push(c); + }); + + server.listen(9000, callback); + enableDestroy(server); +}; + +exports.stop = function (callback) { + sio.close(); + try { + server.destroy(); + } catch (er) {} + + callback(); +}; +