├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── lib └── index.js ├── package.json └── test ├── authenticate.test.js ├── authenticateWithoutToken.test.js ├── bootstrap.js ├── init.test.js ├── server ├── allowNoToken.js └── index.js └── testdata.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | coverage/ 3 | lib-cov/ 4 | coverage.json 5 | npm-debug.log 6 | package-lock.json 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "8" 4 | cache: 5 | directories: 6 | - "node_modules" 7 | after_success: 8 | - npm run coveralls -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Lei Lei 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SocketIO JWT Auth 2 | 3 | [![Travis](https://img.shields.io/travis/adcentury/socketio-jwt-auth.svg)](https://travis-ci.org/adcentury/socketio-jwt-auth) [![Coveralls github](https://img.shields.io/coveralls/github/adcentury/socketio-jwt-auth.svg)](https://coveralls.io/github/adcentury/socketio-jwt-auth) [![npm](https://img.shields.io/npm/dm/socketio-jwt-auth.svg)](https://www.npmjs.com/package/socketio-jwt-auth) [![GitHub license](https://img.shields.io/github/license/adcentury/socketio-jwt-auth.svg)](https://github.com/adcentury/socketio-jwt-auth/blob/master/LICENSE) 4 | 5 | > Socket.io authentication middleware using Json Web Token 6 | 7 | Work with [socket.io](http://socket.io/) >= 1.0 8 | 9 | ## Installation 10 | 11 | ``` 12 | npm install socketio-jwt-auth 13 | ``` 14 | 15 | ## Usage 16 | 17 | ### Register the middleware with socket.io 18 | 19 | __socketio-jwt-auth__ has only one method `authenticate(options, verify)`. 20 | 21 | `options` is an object literal that contains options: 22 | 23 | * `secret` a secret key, 24 | * `algorithm`, defaults to HS256, and 25 | * `succeedWithoutToken`, which, if `true` tells the middleware not to fail if no token is suppled. Defaults to`false`. 26 | 27 | `verify` is a function with two args `payload`, and `done`: 28 | 29 | * `payload` is the decoded JWT payload, and 30 | * `done` is an error-first callback with three args: `done(err, user, message)` 31 | 32 | ```javascript 33 | var io = require('socket.io')(); 34 | var jwtAuth = require('socketio-jwt-auth'); 35 | 36 | // using middleware 37 | io.use(jwtAuth.authenticate({ 38 | secret: 'Your Secret', // required, used to verify the token's signature 39 | algorithm: 'HS256' // optional, default to be HS256 40 | }, function(payload, done) { 41 | // done is a callback, you can use it as follows 42 | User.findOne({id: payload.sub}, function(err, user) { 43 | if (err) { 44 | // return error 45 | return done(err); 46 | } 47 | if (!user) { 48 | // return fail with an error message 49 | return done(null, false, 'user does not exist'); 50 | } 51 | // return success with a user info 52 | return done(null, user); 53 | }); 54 | })); 55 | ``` 56 | 57 | ### Connecting without a token 58 | 59 | There are times when you might wish to successfully connect the socket but indentify the connection as being un-authenticated. For example when a user connects as a guest, before supplying login credentials. In this case you must supply the option `succeedWithoutToken`, as follows: 60 | 61 | ```javascript 62 | var io = require('socket.io')(); 63 | var jwtAuth = require('socketio-jwt-auth'); 64 | 65 | // using middleware 66 | io.use(jwtAuth.authenticate({ 67 | secret: 'Your Secret', // required, used to verify the token's signature 68 | algorithm: 'HS256', // optional, default to be HS256 69 | succeedWithoutToken: true 70 | }, function(payload, done) { 71 | // you done callback will not include any payload data now 72 | // if no token was supplied 73 | if (payload && payload.sub) { 74 | User.findOne({id: payload.sub}, function(err, user) { 75 | if (err) { 76 | // return error 77 | return done(err); 78 | } 79 | if (!user) { 80 | // return fail with an error message 81 | return done(null, false, 'user does not exist'); 82 | } 83 | // return success with a user info 84 | return done(null, user); 85 | }); 86 | } else { 87 | return done() // in your connection handler user.logged_in will be false 88 | } 89 | })); 90 | ``` 91 | 92 | ### Access user info 93 | ```javascript 94 | io.on('connection', function(socket) { 95 | console.log('Authentication passed!'); 96 | // now you can access user info through socket.request.user 97 | // socket.request.user.logged_in will be set to true if the user was authenticated 98 | socket.emit('success', { 99 | message: 'success logged in!', 100 | user: socket.request.user 101 | }); 102 | }); 103 | 104 | io.listen(9000); 105 | ``` 106 | 107 | ### Client Side 108 | 109 | ```javascript 110 | 127 | ``` 128 | 129 | If your client [support](https://socket.io/docs/client-api/#With-extraHeaders), you can also choose to pass the auth token in headers. 130 | 131 | ```javascript 132 | 149 | ``` 150 | 151 | ## Tests 152 | 153 | ``` 154 | npm install 155 | npm test 156 | ``` 157 | 158 | ## Change Log 159 | 160 | ### 0.2.1 161 | 162 | * Fix a bug caused by undefined 163 | 164 | ### 0.2.0 165 | 166 | * Add auth handshake for Socket.IO v3 167 | 168 | ### 0.1.0 169 | 170 | * Add support for passing auth token with `extraHeaders` 171 | 172 | ### 0.0.6 173 | 174 | * Fix an api bug of `node-simple-jwt` 175 | 176 | ### 0.0.5 177 | 178 | * Add an option (`succeedWithoutToken`) to allow guest connection 179 | 180 | ## License 181 | 182 | [The MIT License](http://opensource.org/licenses/MIT) 183 | 184 | Copyright (c) 2015 Lei Lei 185 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | var xtend = require('xtend'); 2 | var jwt = require('jwt-simple'); 3 | 4 | var defaultOptions = { 5 | algorithm: 'HS256', 6 | succeedWithoutToken: false 7 | }; 8 | 9 | function authenticate(options, verify) { 10 | var _this = this; 11 | this.options = xtend(defaultOptions, options); 12 | if (!this.options.secret) { 13 | throw new TypeError('SocketioJwtAuth requires a secret'); 14 | } 15 | 16 | this.verify = verify; 17 | if (!this.verify) { 18 | throw new TypeError('SocketioJwtAuth requires a verify callback'); 19 | } 20 | 21 | this.success = function(next) { 22 | next(); 23 | } 24 | 25 | this.fail = function(err, next) { 26 | var error; 27 | // Legacy support for users who handle the errors as strings 28 | // Should be removed on next release. 29 | if (typeof(err) === 'string') { 30 | error = new Error(err); 31 | } else { 32 | // Socket IO parser only parses object if there is a data field 33 | // This allows for better error handling by checking name/type 34 | error = err; 35 | error.data = { name: err.name, message: err.message }; 36 | } 37 | next(error); 38 | } 39 | 40 | return function(socket, next) { 41 | var token = socket.handshake.headers['x-auth-token']; 42 | 43 | if (!token) { 44 | token = socket.handshake.query ? socket.handshake.query.auth_token : null; 45 | } 46 | 47 | if (!token) { 48 | token = socket.handshake.auth ? socket.handshake.auth.auth_token : null; 49 | } 50 | 51 | var verified = function(err, user, message) { 52 | if (err) { 53 | return _this.fail(err, next); 54 | } else if (!user) { 55 | if (!_this.options.succeedWithoutToken) return _this.fail(message, next); 56 | socket.request.user = {logged_in: false}; 57 | return _this.success(next) 58 | } else { 59 | user.logged_in = true; 60 | socket.request.user = user; 61 | return _this.success(next); 62 | } 63 | }; 64 | try { 65 | var payload = {}; 66 | if (!token) { 67 | if (!_this.options.succeedWithoutToken) { 68 | return _this.fail('No auth token', next); 69 | } 70 | } else { 71 | payload = jwt.decode(token, _this.options.secret, false, _this.options.algorithm); 72 | } 73 | _this.verify(payload, verified); 74 | } catch (ex) { 75 | _this.fail(ex, next); 76 | } 77 | } 78 | } 79 | 80 | exports.authenticate = authenticate; 81 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "socketio-jwt-auth", 3 | "version": "0.2.1", 4 | "description": "Socket.io authentication middleware using Json Web Token", 5 | "keywords": [ 6 | "socket.io", 7 | "socket.io middleware", 8 | "authenticate", 9 | "authentication", 10 | "authorize", 11 | "authorization", 12 | "auth", 13 | "jwt", 14 | "Json Web Token", 15 | "Socket.io JWT Auth" 16 | ], 17 | "main": "./lib", 18 | "scripts": { 19 | "test": "mocha", 20 | "cover": "istanbul cover ./node_modules/mocha/bin/_mocha", 21 | "coveralls": "npm run cover -- --report lcovonly && cat ./coverage/lcov.info | coveralls" 22 | }, 23 | "author": { 24 | "name": "Lei Lei", 25 | "email": "adcentury100@gmail.com" 26 | }, 27 | "contributors": [ 28 | "Dave Sag " 29 | ], 30 | "repository": { 31 | "type": "git", 32 | "url": "https://github.com/adcentury/socketio-jwt-auth.git" 33 | }, 34 | "license": "MIT", 35 | "dependencies": { 36 | "jwt-simple": "^0.5", 37 | "xtend": "^4.0.0" 38 | }, 39 | "devDependencies": { 40 | "chai": "^4.1.2", 41 | "coveralls": "^3.0.0", 42 | "istanbul": "^0.4.5", 43 | "mocha": "^5.0.0", 44 | "mocha-lcov-reporter": "^1.3.0", 45 | "socket.io": "^1.3.7", 46 | "socket.io-client": "^1.3.7" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /test/authenticate.test.js: -------------------------------------------------------------------------------- 1 | var server = require('./server'); 2 | var io = require('socket.io-client'); 3 | 4 | var data = require('./testdata'); 5 | 6 | describe('authenticate', function() { 7 | 8 | var socket; 9 | 10 | // start and stop the server 11 | before(server.start); 12 | after(server.stop); 13 | 14 | afterEach(function() { 15 | socket.disconnect(); 16 | }); 17 | 18 | describe('when user connect to server', function() { 19 | it('should emit error when auth_token is missing', function(done) { 20 | socket = io('http://localhost:9000', {'force new connection': true}); 21 | socket.on('error', function(err) { 22 | expect(err).to.equal('No auth token'); 23 | done(); 24 | }) 25 | }); 26 | 27 | it('should emit error when auth_token is syntactically invalid', function(done) { 28 | socket = io('http://localhost:9000', {query: 'auth_token=blabla', 'force new connection': true}); 29 | socket.on('error', function(err) { 30 | expect(err).to.be.a('object'); 31 | expect(err.message).to.be.a('string') 32 | expect(err.message).to.equal('Not enough or too many segments'); 33 | expect(err.name).to.equal('Error') 34 | 35 | done(); 36 | }); 37 | }); 38 | 39 | it('should emit error when auth_token has the wrong signature', function(done) { 40 | socket = io('http://localhost:9000', {query: 'auth_token=' + data.valid_jwt_with_another_secret.token, 'force new connection': true}); 41 | socket.on('error', function(err) { 42 | expect(err).to.be.a('object'); 43 | expect(err.message).to.be.a('string') 44 | expect(err.message).to.equal('Signature verification failed'); 45 | expect(err.name).to.equal('Error') 46 | done(); 47 | }); 48 | }); 49 | 50 | it('should add user info to socket.request when authenticated', function(done) { 51 | socket = io('http://localhost:9000', {query: 'auth_token=' + data.valid_jwt.token, 'force new connection': true}); 52 | socket.on('success', function(user) { 53 | expect(user).to.be.an('object'); 54 | expect(user.name).to.equal(data.user.name); 55 | expect(user.email).to.equal(data.user.email); 56 | expect(user.logged_in).to.be.true; 57 | done(); 58 | }); 59 | }); 60 | 61 | it('should support auth token being passed in with extraHeaders', function(done) { 62 | socket = io('http://localhost:9000', { 63 | extraHeaders: { 64 | 'x-auth-token': data.valid_jwt.token 65 | }, 66 | transportOptions: { 67 | polling: { 68 | extraHeaders: { 69 | 'x-auth-token': data.valid_jwt.token 70 | } 71 | } 72 | }, 73 | 'force new connection': true 74 | }); 75 | socket.on('success', function(user) { 76 | expect(user).to.be.an('object'); 77 | expect(user.name).to.equal(data.user.name); 78 | expect(user.email).to.equal(data.user.email); 79 | expect(user.logged_in).to.be.true; 80 | done(); 81 | }); 82 | }); 83 | }); 84 | 85 | }); 86 | -------------------------------------------------------------------------------- /test/authenticateWithoutToken.test.js: -------------------------------------------------------------------------------- 1 | var server = require('./server/allowNoToken'); 2 | var io = require('socket.io-client'); 3 | 4 | var data = require('./testdata'); 5 | 6 | describe('authenticate without token if succeedWithoutToken is true', function() { 7 | 8 | var socket; 9 | 10 | // start and stop the server 11 | before(server.start); 12 | after(server.stop); 13 | 14 | afterEach(function() { 15 | socket.disconnect(); 16 | }); 17 | 18 | describe('when guest connects to server', function() { 19 | 20 | it('should succeed but the user should not be logged in', function(done) { 21 | socket = io('http://localhost:9000', {'force new connection': true}); 22 | socket.on('success', function(user) { 23 | console.log('got user', user) 24 | expect(user).to.be.an('object'); 25 | expect(user.logged_in).to.be.false; 26 | done(); 27 | }); 28 | }); 29 | 30 | }); 31 | 32 | }); 33 | -------------------------------------------------------------------------------- /test/bootstrap.js: -------------------------------------------------------------------------------- 1 | var chai = require('chai'); 2 | global.expect = chai.expect; 3 | -------------------------------------------------------------------------------- /test/init.test.js: -------------------------------------------------------------------------------- 1 | var socketIoJwtAuth = require('../lib'); 2 | 3 | describe('authenticate function init', function() { 4 | it('should throw if called without a secret arg', function() { 5 | expect(function() { 6 | socketIoJwtAuth.authenticate() 7 | }).to.throw(TypeError, 'SocketioJwtAuth requires a secret'); 8 | expect(function() { 9 | socketIoJwtAuth.authenticate({blabla: 'blabla'}) 10 | }).to.throw(TypeError, 'SocketioJwtAuth requires a secret'); 11 | expect(function() { 12 | socketIoJwtAuth.authenticate({secret: null}) 13 | }).to.throw(TypeError, 'SocketioJwtAuth requires a secret'); 14 | }); 15 | 16 | it('should throw if called without a verify callback', function() { 17 | expect(function() { 18 | socketIoJwtAuth.authenticate({secret: 'secret'}) 19 | }).to.throw(TypeError, 'SocketioJwtAuth requires a verify callback'); 20 | }); 21 | }); 22 | 23 | 24 | -------------------------------------------------------------------------------- /test/server/allowNoToken.js: -------------------------------------------------------------------------------- 1 | var io = require('socket.io')(); 2 | var socketIoJwtAuth = require('../../lib'); 3 | 4 | var data = require('../testdata'); 5 | 6 | exports.start = function() { 7 | io.use(socketIoJwtAuth.authenticate({ 8 | secret: data.valid_jwt.secret, 9 | succeedWithoutToken: true 10 | }, function(_payload, done) { 11 | // ignore payload 12 | return done(null, null); 13 | })); 14 | 15 | io.on('connection', function(socket) { 16 | socket.emit('success', socket.request.user); 17 | }); 18 | 19 | io.listen(9000); 20 | } 21 | 22 | exports.stop = function() { 23 | io.close(); 24 | } 25 | -------------------------------------------------------------------------------- /test/server/index.js: -------------------------------------------------------------------------------- 1 | var io = require('socket.io')(); 2 | var socketIoJwtAuth = require('../../lib'); 3 | 4 | var data = require('../testdata'); 5 | 6 | exports.start = function() { 7 | io.use(socketIoJwtAuth.authenticate({ 8 | secret: data.valid_jwt.secret, 9 | algorithm: 'HS256' 10 | }, function(payload, done) { 11 | var id = payload.sub; 12 | if (!id) { 13 | return done('error happened'); 14 | } 15 | if (id !== '1') { 16 | return done(null, false, 'user not exist'); 17 | } 18 | return done(null, { 19 | name: data.user.name, 20 | email: data.user.email 21 | }); 22 | })); 23 | 24 | io.on('connection', function(socket) { 25 | socket.emit('success', socket.request.user); 26 | }); 27 | 28 | io.listen(9000); 29 | } 30 | 31 | exports.stop = function() { 32 | io.close(); 33 | } 34 | -------------------------------------------------------------------------------- /test/testdata.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | valid_jwt: { 3 | token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxIn0.8qZF8vbN3UpcanXFc-mPXJkOPN01-bRch8XX3rToP1U', 4 | payload: { 5 | sub: '1' 6 | }, 7 | secret: 'secret' 8 | }, 9 | valid_jwt_with_another_secret: { 10 | token: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIn0.rbRUk2g1Rifi3klfYOV6Z1unf3xfRWMRP8JBIHVDYzw', 11 | payload: { 12 | 'sub': '1' 13 | }, 14 | secret: 'anothersecret' 15 | }, 16 | user: { 17 | name: 'leilei', 18 | email: 'adcentury100@gmail.com' 19 | } 20 | }; 21 | --------------------------------------------------------------------------------