├── .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 | [](https://travis-ci.org/adcentury/socketio-jwt-auth) [](https://coveralls.io/github/adcentury/socketio-jwt-auth) [](https://www.npmjs.com/package/socketio-jwt-auth) [](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 |
--------------------------------------------------------------------------------