├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── lib ├── UnauthorizedError.js └── index.js ├── package.json ├── test ├── authorizer.test.js ├── authorizer_namespaces.test.js ├── authorizer_noqs.test.js ├── authorizer_secret_function_noqs.test.js ├── authorizer_secret_function_qs.test.js ├── fixture │ ├── index.js │ ├── namespace.js │ └── secret_function.js └── mocha.opts └── types └── index.d.ts /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules/* -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.12" 4 | - "0.11" 5 | - "0.10" -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [4.0.1] - 2015-04-22 2 | - [4cf0651] Minor. replace regexp for native comparisoon (`Nikita Gusakov`) 3 | 4 | ## [4.0.0] - 2015-04-22 5 | - [7e53470] Updated to jsonwebtoken@5.0.0 (`Alberto Pose`) 6 | 7 | ## [3.0.0] - 2015-03-16 8 | - [089bfe5] update jsonwebtoken dependency (`José F. Romaniello`) 9 | - [dbb4e95] Merge pull request #21 from kennyki/patch-1 (`José F. Romaniello`) 10 | - [b1e5530] Added example of handling token expiration (`Kenny Ki`) 11 | 12 | ## [2.3.5] - 2014-09-05 13 | - [f357dd7] update jsonwebtoken (`José F. Romaniello`) 14 | 15 | ## [2.3.4] - 2014-07-16 16 | - [2490770] Merge pull request #18 from yads/master (`José F. Romaniello`) 17 | - [cae2123] test fixes (`Vadim Kazakov`) 18 | 19 | ## [2.3.3] - 2014-07-16 20 | - [55d5e43] Merge branch 'yads-master' (`José F. Romaniello`) 21 | - [2897f90] merge (`José F. Romaniello`) 22 | - [1398434] add data to UnauthorizedError so that more information can be returned to client (`Vadim Kazakov`) 23 | 24 | ## [2.3.2] - 2014-07-16 25 | - [9d5abf9] update jsonwebtoken module to fix security issue (`José F. Romaniello`) 26 | - [870a274] update example (`José F. Romaniello`) 27 | - [e9b8ea4] fix readme (`José F. Romaniello`) 28 | - [7ea32e5] Merge pull request #9 from zudio/master (`José F. Romaniello`) 29 | - [e6ea64d] Update README.md (`Mark Rendle`) 30 | 31 | ## [2.3.1] - 2014-06-09 32 | - [fe39d2c] Merge pull request #12 from otothea/master (`José F. Romaniello`) 33 | - [f072f91] update readme and fix #11 (`José F. Romaniello`) 34 | - [29b3882] Make it look for both kinds of query (`Oscar`) 35 | - [452cc19] req._query is now req.query (`Oscar`) 36 | 37 | ## [2.3.0] - 2014-06-05 (YANKED) 38 | ## [2.2.0] - 2014-06-05 (YANKED) 39 | 40 | ## [2.1.0] - 2014-06-03 41 | - [e8380c1] add support for socket.io 1.0 (`José F. Romaniello`) 42 | - [0577d07] missing parenthesis closes #7 (`José F. Romaniello`) 43 | 44 | ## [2.0.2] - 2014-03-20 45 | - [9a9f7d0] added newest xtend to prevent deprecation warning from object-keys (`kjellski`) 46 | - [9a58d94] add license, close #2 (`José F. Romaniello`) 47 | - [8e567b9] fix #3 (`José F. Romaniello`) 48 | 49 | ## [2.0.1] - 2014-01-23 50 | - [54a33c2] change user to decoded_token (`José F. Romaniello`) 51 | - [e626188] add example (`José F. Romaniello`) 52 | 53 | ## [2.0.0] - 2014-01-14 54 | - [b292ab7] change the API (`José F. Romaniello`) 55 | - [b0f4354] add noqs method (`José F. Romaniello`) 56 | - [14a34ae] initial commit after fork of passport-socketio (`José F. Romaniello`) 57 | - [aa678b4] move request to devDeps, closes #44 (`José F. Romaniello`) 58 | - [a2c4ad3] version push (`Screeny`) 59 | - [95fb0fb] emit error on store-error (`Screeny`) 60 | - [c5303a3] fixes #40 (`Screeny`) 61 | - [4a50d9e] Merge pull request #39 from rickheere/master (`José F. Romaniello`) 62 | - [efa4838] Corrected a minor error in the documentation. (`Rick Heere`) 63 | - [2bf410f] Merge pull request #37 from ceojasonnichols/patch-1 (`José F. Romaniello`) 64 | - [49f35c3] Missing Paren in Example (`Jason Nichols`) 65 | 66 | ## [2.2.1] - 2014-01-13 67 | - [efbef7a] move request to devDeps, closes #44 (`José F. Romaniello`) 68 | 69 | ## [2.2.0] - 2013-11-21 70 | - [bd0980e] Merge pull request #36 from TeamSynergy/cors_workaround (`José F. Romaniello`) 71 | - [1a3b3e1] step 2, updated readme (`Screeny`) 72 | - [f31dc4a] step 1 (`Screeny`) 73 | 74 | ## [2.1.2] - 2013-11-18 75 | - [599a614] fixed a security issue (`Amir`) 76 | - [91750bb] Merge pull request #33 from TeamSynergy/master (`José F. Romaniello`) 77 | - [2d257bf] Update README.md (`Screeny`) 78 | - [b111b3f] Update README.md (`Screeny`) 79 | - [f5adaa7] Merge pull request #1 from TeamSynergy/close_default (`Screeny`) 80 | - [df32515] version push (`Screeny`) 81 | - [3c9f23e] a little simpler (`Screeny`) 82 | - [fd4214e] close socket.io by default (`Screeny`) 83 | - [2a3cfdb] thanks to @chill117 (`Screeny`) 84 | - [928cd50] major changes (`Screeny`) 85 | - [b6e7ee6] add note about cors, closes #28 (`José F. Romaniello`) 86 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Auth0, Inc. (http://auth0.com) 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## This is a fork of Auth0s "socketio-jwt" library 2 | It reassembles all the changes made by the community, that never got reviewed by Auth0. 3 | 4 | These are most likely pull requests and changes by the owner of this fork. [See here.](https://github.com/Root-Core/socketio-jwt#differences-to-auth0-repo) 5 | 6 | [![Build Status](https://travis-ci.org/auth0/socketio-jwt.svg)](https://travis-ci.org/auth0/socketio-jwt) 7 | 8 | Authenticate socket.io incoming connections with JWTs. This is useful if you are build a single page application and you are not using cookies as explained in this blog post: [Cookies vs Tokens. Getting auth right with Angular.JS](http://blog.auth0.com/2014/01/07/angularjs-authentication-with-cookies-vs-token/). 9 | 10 | ## Installation 11 | This fork will be released to the npm repository, but for now you can install directly from GitHub 12 | 13 | ```bash 14 | npm install root-core/socketio-jwt 15 | ``` 16 | 17 | ## Example usage 18 | 19 | ```javascript 20 | // set authorization for socket.io 21 | io.sockets 22 | .on('connection', socketioJwt.authorize({ 23 | secret: 'your secret or public key', 24 | timeout: 15000 // 15 seconds to send the authentication message 25 | })).on('authenticated', function(socket) { 26 | //this socket is authenticated, we are good to handle more events from it. 27 | console.log('hello! ' + socket.decoded_token.name); 28 | }); 29 | ``` 30 | 31 | **Note:** If you are using a base64-encoded secret (e.g. your Auth0 secret key), you need to convert it to a Buffer: `Buffer('your secret key', 'base64')` 32 | 33 | __Client side__: 34 | 35 | ```javascript 36 | var socket = io.connect('http://localhost:9000'); 37 | socket.on('connect', function () { 38 | socket 39 | .emit('authenticate', {token: jwt}) //send the jwt 40 | .on('authenticated', function () { 41 | //do other things 42 | }) 43 | .on('unauthorized', function(msg) { 44 | console.log("unauthorized: " + JSON.stringify(msg.data)); 45 | throw new Error(msg.data.type); 46 | }) 47 | }); 48 | ``` 49 | 50 | ## One roundtrip 51 | 52 | The previous approach uses a second roundtrip to send the jwt, there is a way you can authenticate on the handshake by sending the JWT as a query string, the caveat is that intermediary HTTP servers can log the url. 53 | 54 | ```javascript 55 | var io = require('socket.io')(server); 56 | var socketioJwt = require('socketio-jwt'); 57 | 58 | //// With socket.io < 1.0 //// 59 | io.set('authorization', socketioJwt.authorize({ 60 | secret: 'your secret or public key', 61 | handshake: true 62 | })); 63 | ////////////////////////////// 64 | 65 | //// With socket.io >= 1.0 //// 66 | io.use(socketioJwt.authorize({ 67 | secret: 'your secret or public key', 68 | handshake: true 69 | })); 70 | /////////////////////////////// 71 | 72 | io.on('connection', function (socket) { 73 | // in socket.io < 1.0 74 | console.log('hello!', socket.handshake.decoded_token.name); 75 | 76 | // in socket.io 1.0 77 | console.log('hello! ', socket.decoded_token.name); 78 | }) 79 | ``` 80 | 81 | For more validation options see [auth0/jsonwebtoken](https://github.com/auth0/node-jsonwebtoken). 82 | 83 | __Client side__: 84 | 85 | Append the jwt token using query string: 86 | 87 | ```javascript 88 | //// token part of query string //// 89 | var socket = io.connect('http://localhost:9000', { 90 | 'query': 'token=' + your_jwt 91 | }); 92 | 93 | 94 | //// token coming in as Authorization Header //// 95 | var socket = io.connect('http://localhost:9000', { 96 | 'extraHeaders': { Authorization: `Bearer ${your_jwt}` } 97 | }); 98 | ``` 99 | 100 | ## Authorization Header Requirement 101 | 102 | Require Bearer Tokens to be passed in as an Authorization Header 103 | 104 | __Server side__: 105 | 106 | ```javascript 107 | io.use(socketioJwt.authorize({ 108 | secret: 'your secret or public key', 109 | handshake: true, 110 | auth_header_required: true 111 | })); 112 | ``` 113 | 114 | ## Handling token expiration 115 | 116 | __Server side__: 117 | 118 | When you sign the token with an expiration time: 119 | 120 | ```javascript 121 | var token = jwt.sign(user_profile, jwt_secret, {expiresInMinutes: 60}); 122 | ``` 123 | 124 | Your client-side code should handle it as below. 125 | 126 | __Client side__: 127 | 128 | ```javascript 129 | socket.on('unauthorized', function(error) { 130 | if (error.data.type == 'UnauthorizedError' || error.data.code == 'invalid_token') { 131 | // redirect user to login page perhaps? 132 | console.log('Users token has expired'); 133 | } 134 | }); 135 | ``` 136 | 137 | ## Handling invalid token 138 | 139 | Token sent by client is invalid. 140 | 141 | __Server side__: 142 | 143 | No further configuration needed. 144 | 145 | __Client side__: 146 | 147 | Add a callback client-side to execute socket disconnect server-side. 148 | 149 | ```javascript 150 | socket.on('unauthorized', function(error, callback) { 151 | if (error.data.type == 'UnauthorizedError' || error.data.code == 'invalid_token') { 152 | // redirect user to login page perhaps or execute callback: 153 | callback(); 154 | console.log('Users token has expired'); 155 | } 156 | }); 157 | ``` 158 | 159 | __Server side__: 160 | 161 | To disconnect socket server-side without client-side callback: 162 | 163 | ```javascript 164 | io.sockets.on('connection', socketioJwt.authorize({ 165 | secret: 'secret goes here', 166 | // No client-side callback, terminate connection server-side 167 | callback: false 168 | })) 169 | ``` 170 | 171 | __Client side__: 172 | 173 | Nothing needs to be changed client-side if callback is false. 174 | 175 | __Server side__: 176 | 177 | To disconnect socket server-side while giving client-side 15 seconds to execute callback: 178 | 179 | ```javascript 180 | io.sockets.on('connection', socketioJwt.authorize({ 181 | secret: 'secret goes here', 182 | // Delay server-side socket disconnect to wait for client-side callback 183 | callback: 15000 184 | })) 185 | ``` 186 | 187 | Your client-side code should handle it as below. 188 | 189 | __Client side__: 190 | 191 | ```javascript 192 | socket.on('unauthorized', function(error, callback) { 193 | if (error.data.type == 'UnauthorizedError' || error.data.code == 'invalid_token') { 194 | // redirect user to login page perhaps or execute callback: 195 | callback(); 196 | console.log('Users token has expired'); 197 | } 198 | }); 199 | ``` 200 | 201 | ## Getting the secret dynamically 202 | You can pass a function instead of an string when configuring secret. 203 | This function receives the request, the decoded token and a callback. This 204 | way, you are allowed to use a different secret based on the request and / or 205 | the provided token. 206 | 207 | __Server side__: 208 | 209 | ```javascript 210 | var SECRETS = { 211 | 'user1': 'secret 1', 212 | 'user2': 'secret 2' 213 | } 214 | 215 | io.use(socketioJwt.authorize({ 216 | secret: function(request, decodedToken, callback) { 217 | // SECRETS[decodedToken.userId] will be used as a secret or 218 | // public key for connection user. 219 | 220 | callback(null, SECRETS[decodedToken.userId]); 221 | }, 222 | handshake: false 223 | })); 224 | ``` 225 | 226 | ## Contribute 227 | 228 | You are always welcome to open an issue or provide a pull-request! 229 | 230 | Also check out the unit tests: 231 | ```bash 232 | npm test 233 | ``` 234 | 235 | ## Issue Reporting 236 | 237 | If you have found a bug or if you have a feature request, please report them at this repository issues section. Please do not report security vulnerabilities on the public GitHub issue tracker. The [Responsible Disclosure Program](https://auth0.com/whitehat) details the procedure for disclosing security issues. 238 | 239 | ## Original author 240 | 241 | [Auth0](auth0.com) 242 | 243 | ## Differences to Auth0-repo 244 | 245 | * Typescript support (Typings) 246 | * Fixed authentication in namspaces 247 | * With an more correct approach to get the header in the first place! 248 | * The encoded JWT is stored in `socket.encoded_token` 249 | * The propertys name is configurable via `encodedPropertyName` in the option object 250 | * Just like the decoded property name via `decodedPropertyName` in the option object 251 | * Exporting UnauthorizedError allows to throw own rejections / control flow 252 | * Added `auth_header_required` to option object to reject clients without an authentication header 253 | * Typos fixed, renamed variables 254 | * Removed empty example folder 255 | * Updated dependencies 256 | * Improved test coverage 257 | 258 | ## License 259 | 260 | This project is licensed under the MIT license. See the [LICENSE](LICENSE) file for more info. 261 | -------------------------------------------------------------------------------- /lib/UnauthorizedError.js: -------------------------------------------------------------------------------- 1 | function UnauthorizedError (code, error) { 2 | Error.call(this, error.message); 3 | this.message = error.message; 4 | this.inner = error; 5 | this.data = { 6 | message: this.message, 7 | code: code, 8 | type: "UnauthorizedError" 9 | }; 10 | } 11 | 12 | UnauthorizedError.prototype = Object.create(Error.prototype); 13 | UnauthorizedError.prototype.constructor = UnauthorizedError; 14 | 15 | module.exports = UnauthorizedError; -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | var xtend = require('xtend'); 2 | var jwt = require('jsonwebtoken'); 3 | var UnauthorizedError = require('./UnauthorizedError'); 4 | 5 | function noQsMethod(options) { 6 | var defaults = { required: true }; 7 | options = xtend(defaults, options); 8 | 9 | return function (socket) { 10 | var server = this.server || socket.server; 11 | 12 | if (!server.$emit) { 13 | //then is socket.io 1.0 14 | var Namespace = Object.getPrototypeOf(server.sockets).constructor; 15 | if (!~Namespace.events.indexOf('authenticated')) { 16 | Namespace.events.push('authenticated'); 17 | } 18 | } 19 | 20 | if(options.required){ 21 | var auth_timeout = setTimeout(function () { 22 | socket.disconnect('unauthorized'); 23 | }, options.timeout || 5000); 24 | } 25 | 26 | socket.on('authenticate', function (data) { 27 | if(options.required){ 28 | clearTimeout(auth_timeout); 29 | } 30 | // error handler 31 | var onError = function(err, code) { 32 | if (err) { 33 | code = code || 'unknown'; 34 | var error = new UnauthorizedError(code, { 35 | message: (Object.prototype.toString.call(err) === '[object Object]' && err.message) ? err.message : err 36 | }); 37 | var callback_timeout; 38 | // If callback explicitely set to false, start timeout to disconnect socket 39 | if (options.callback === false || typeof options.callback === "number") { 40 | if (typeof options.callback === "number") { 41 | if (options.callback < 0) { 42 | // If callback is negative(invalid value), make it positive 43 | options.callback = Math.abs(options.callback); 44 | } 45 | } 46 | callback_timeout = setTimeout(function () { 47 | socket.disconnect('unauthorized'); 48 | }, (options.callback === false ? 0 : options.callback)); 49 | } 50 | socket.emit('unauthorized', error, function() { 51 | if (typeof options.callback === "number") { 52 | clearTimeout(callback_timeout); 53 | } 54 | socket.disconnect('unauthorized'); 55 | }); 56 | return; // stop logic, socket will be close on next tick 57 | } 58 | }; 59 | 60 | if(!data || typeof data.token !== "string") { 61 | return onError({message: 'invalid token datatype'}, 'invalid_token'); 62 | } 63 | 64 | // Store encoded JWT 65 | socket[options.encodedPropertyName] = data.token; 66 | 67 | var onJwtVerificationReady = function(err, decoded) { 68 | 69 | if (err) { 70 | return onError(err, 'invalid_token'); 71 | } 72 | 73 | // success handler 74 | var onSuccess = function() { 75 | socket[options.decodedPropertyName] = decoded; 76 | socket.emit('authenticated'); 77 | if (server.$emit) { 78 | server.$emit('authenticated', socket); 79 | } else { 80 | //try getting the current namespace otherwise fallback to all sockets. 81 | var namespace = (server.nsps && socket.nsp && 82 | server.nsps[socket.nsp.name]) || 83 | server.sockets; 84 | 85 | // explicit namespace 86 | namespace.emit('authenticated', socket); 87 | } 88 | }; 89 | 90 | if(options.additional_auth && typeof options.additional_auth === 'function') { 91 | options.additional_auth(decoded, onSuccess, onError); 92 | } else { 93 | onSuccess(); 94 | } 95 | }; 96 | 97 | var onSecretReady = function(err, secret) { 98 | if (err || !secret) { 99 | return onError(err, 'invalid_secret'); 100 | } 101 | 102 | jwt.verify(data.token, secret, options, onJwtVerificationReady); 103 | }; 104 | 105 | getSecret(socket.request, options.secret, data.token, onSecretReady); 106 | }); 107 | }; 108 | } 109 | 110 | function authorize(options, onConnection) { 111 | options = xtend({ decodedPropertyName: 'decoded_token', encodedPropertyName: 'encoded_token' }, options); 112 | 113 | if (!options.handshake) { 114 | return noQsMethod(options); 115 | } 116 | 117 | var defaults = { 118 | success: function(socket, accept){ 119 | if (socket.request) { 120 | accept(); 121 | } else { 122 | accept(null, true); 123 | } 124 | }, 125 | fail: function(error, socket, accept){ 126 | if (socket.request) { 127 | accept(error); 128 | } else { 129 | accept(null, false); 130 | } 131 | } 132 | }; 133 | 134 | var auth = xtend(defaults, options); 135 | 136 | return function(socket, accept){ 137 | var token, error; 138 | var req = socket.request || socket; 139 | var handshake = socket.handshake; 140 | var authorization_header = (req.headers || {}).authorization; 141 | 142 | if (authorization_header) { 143 | var parts = authorization_header.split(' '); 144 | if (parts.length == 2) { 145 | var scheme = parts[0], 146 | credentials = parts[1]; 147 | 148 | if (scheme.toLowerCase() === 'bearer') { 149 | token = credentials; 150 | } 151 | } else { 152 | error = new UnauthorizedError('credentials_bad_format', { 153 | message: 'Format is Authorization: Bearer [token]' 154 | }); 155 | return auth.fail(error, socket, accept); 156 | } 157 | } 158 | 159 | if ( options.auth_header_required && !token ) { 160 | return auth.fail(new UnauthorizedError('missing_authorization_header', { 161 | message: 'Server requires Authorization Header' 162 | }), socket, accept); 163 | } 164 | 165 | //get the token from handshake or query string 166 | if (handshake && handshake.query.token){ 167 | token = handshake.query.token; 168 | } 169 | else if (req._query && req._query.token) { 170 | token = req._query.token; 171 | } 172 | else if (req.query && req.query.token) { 173 | token = req.query.token; 174 | } 175 | 176 | if (!token) { 177 | error = new UnauthorizedError('credentials_required', { 178 | message: 'No Authorization header was found' 179 | }); 180 | return auth.fail(error, socket, accept); 181 | } 182 | 183 | // Store encoded JWT 184 | socket[options.encodedPropertyName] = token; 185 | 186 | var onJwtVerificationReady = function(err, decoded) { 187 | 188 | if (err) { 189 | error = new UnauthorizedError(err.code || 'invalid_token', err); 190 | return auth.fail(error, socket, accept); 191 | } 192 | 193 | socket[options.decodedPropertyName] = decoded; 194 | 195 | return auth.success(socket, accept); 196 | }; 197 | 198 | var onSecretReady = function(err, secret) { 199 | if (err) { 200 | error = new UnauthorizedError(err.code || 'invalid_secret', err); 201 | return auth.fail(error, socket, accept); 202 | } 203 | 204 | jwt.verify(token, secret, options, onJwtVerificationReady); 205 | }; 206 | 207 | getSecret(req, options.secret, token, onSecretReady); 208 | }; 209 | } 210 | 211 | function getSecret(request, secret, token, callback) { 212 | if (typeof secret === 'function') { 213 | if (!token) { 214 | return callback({ code: 'invalid_token', message: 'jwt must be provided' }); 215 | } 216 | 217 | var parts = token.split('.'); 218 | 219 | if (parts.length < 3) { 220 | return callback({ code: 'invalid_token', message: 'jwt malformed' }); 221 | } 222 | 223 | if (parts[2].trim() === '') { 224 | return callback({ code: 'invalid_token', message: 'jwt signature is required' }); 225 | } 226 | 227 | var decodedToken = jwt.decode(token); 228 | 229 | if (!decodedToken) { 230 | return callback({ code: 'invalid_token', message: 'jwt malformed' }); 231 | } 232 | 233 | secret(request, decodedToken, callback); 234 | } else { 235 | callback(null, secret); 236 | } 237 | }; 238 | 239 | exports.authorize = authorize; 240 | exports.UnauthorizedError = UnauthorizedError; 241 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "socketio-jwt", 3 | "version": "5.0.0", 4 | "description": "authenticate socket.io connections using JWTs", 5 | "main": "lib/index.js", 6 | "types": "./types/index.d.ts", 7 | "keywords": [ 8 | "socket", 9 | "socket.io", 10 | "jwt" 11 | ], 12 | "author": { 13 | "name": "José F. Romaniello", 14 | "email": "jfromaniello@gmail.com", 15 | "url": "http://joseoncode.com" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/auth0/socketio-jwt.git" 20 | }, 21 | "scripts": { 22 | "test": "mocha" 23 | }, 24 | "license": "MIT", 25 | "dependencies": { 26 | "@types/socket.io": "~1.4.29", 27 | "jsonwebtoken": "^7.3.0", 28 | "xtend": "~4.0.1" 29 | }, 30 | "devDependencies": { 31 | "body-parser": "~1.17.1", 32 | "express": "~4.15.2", 33 | "mocha": "~3.2.0", 34 | "request": "~2.81.0", 35 | "serve-static": "^1.12.1", 36 | "q": "^1.4.1", 37 | "server-destroy": "~1.0.1", 38 | "should": "~11.2.1", 39 | "socket.io": "^1.7.3", 40 | "socket.io-client": "^1.7.3" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /test/authorizer.test.js: -------------------------------------------------------------------------------- 1 | var Q = require('q'); 2 | var fixture = require('./fixture'); 3 | var request = require('request'); 4 | var io = require('socket.io-client'); 5 | 6 | describe('authorizer', function() { 7 | //start and stop the server 8 | before(function(done) { fixture.start({ }, done) }); 9 | after(fixture.stop); 10 | 11 | describe('when the user is not logged in', function () { 12 | it('should emit error with unauthorized handshake', function (done){ 13 | var socket = io.connect('http://localhost:9000?token=boooooo', { 14 | 'forceNew': true 15 | }); 16 | 17 | socket.on('error', function(err){ 18 | err.message.should.eql("jwt malformed"); 19 | err.code.should.eql("invalid_token"); 20 | socket.close(); 21 | done(); 22 | }); 23 | }); 24 | 25 | }); 26 | 27 | describe('when the user is logged in', function() { 28 | before(function (done) { 29 | request.post({ 30 | url: 'http://localhost:9000/login', 31 | form: { username: 'jose', password: 'Pa123' }, 32 | json: true 33 | }, function (err, resp, body) { 34 | this.token = body.token; 35 | done(); 36 | }.bind(this)); 37 | }); 38 | 39 | describe('authorizer disallows query string token when specified in startup options', function() { 40 | before(function(done) { 41 | Q.ninvoke(fixture, 'stop') 42 | .then(function() { return Q.ninvoke(fixture, 'start', { auth_header_required: true })}) 43 | .done(done); 44 | }) 45 | after(function(done) { 46 | Q.ninvoke(fixture, 'stop') 47 | .then(function() { return Q.ninvoke(fixture, 'start', { })}) 48 | .done(done); 49 | }) 50 | 51 | it('auth headers are supported', function (done){ 52 | var socket = io.connect('http://localhost:9000', { 53 | 'forceNew':true, 54 | 'extraHeaders': {'Authorization': 'Bearer ' + this.token} 55 | }); 56 | socket.on('connect', function(){ 57 | socket.close(); 58 | done(); 59 | }).on('error', done); 60 | }); 61 | 62 | it('auth token in query string is disallowed', function (done){ 63 | var socket = io.connect('http://localhost:9000', { 64 | 'forceNew':true, 65 | 'query': 'token=' + this.token 66 | }); 67 | socket.on('error', function(err){ 68 | err.message.should.eql("Server requires Authorization Header"); 69 | err.code.should.eql("missing_authorization_header"); 70 | socket.close(); 71 | done(); 72 | }); 73 | }); 74 | }) 75 | 76 | describe('authorizer all auth types allowed', function() { 77 | before(function(done) { 78 | Q.ninvoke(fixture, 'stop') 79 | .then(function() { return Q.ninvoke(fixture, 'start', {})}) 80 | .done(done); 81 | }) 82 | 83 | it('auth headers are supported', function (done){ 84 | var socket = io.connect('http://localhost:9000', { 85 | 'forceNew':true, 86 | 'extraHeaders': {'Authorization': 'Bearer ' + this.token} 87 | }); 88 | socket.on('connect', function(){ 89 | socket.close(); 90 | done(); 91 | }).on('error', done); 92 | }); 93 | 94 | it('should do the handshake and connect', function (done){ 95 | var socket = io.connect('http://localhost:9000', { 96 | 'forceNew':true, 97 | 'query': 'token=' + this.token 98 | }); 99 | socket.on('connect', function(){ 100 | socket.close(); 101 | done(); 102 | }).on('error', done); 103 | }); 104 | }); 105 | }); 106 | 107 | describe('unsigned token', function() { 108 | beforeEach(function () { 109 | this.token = 'eyJhbGciOiJub25lIiwiY3R5IjoiSldUIn0.eyJuYW1lIjoiSm9obiBGb28ifQ.'; 110 | }); 111 | 112 | it('should not do the handshake and connect', function (done){ 113 | var socket = io.connect('http://localhost:9000', { 114 | 'forceNew':true, 115 | 'query': 'token=' + this.token 116 | }); 117 | socket.on('connect', function () { 118 | socket.close(); 119 | done(new Error('this shouldnt happen')); 120 | }).on('error', function (err) { 121 | socket.close(); 122 | err.message.should.eql("jwt signature is required"); 123 | done(); 124 | }); 125 | }); 126 | }); 127 | }); 128 | -------------------------------------------------------------------------------- /test/authorizer_namespaces.test.js: -------------------------------------------------------------------------------- 1 | var fixture = require('./fixture/namespace'); 2 | var request = require('request'); 3 | var io = require('socket.io-client'); 4 | 5 | describe('authorizer with namespaces', function () { 6 | 7 | //start and stop the server 8 | before(fixture.start); 9 | 10 | after(fixture.stop); 11 | 12 | describe('when the user is not logged in', function () { 13 | 14 | it('should be able to connect to the default namespace', function (done){ 15 | var socket = io.connect('http://localhost:9000'); 16 | socket.once('hi', done); 17 | }); 18 | 19 | it('should not be able to connect to the admin namespace', function (done){ 20 | var socket = io.connect('http://localhost:9000/admin'); 21 | socket.once('disconnect', function () { 22 | done(); 23 | }); 24 | }); 25 | 26 | }); 27 | 28 | describe('when the user is logged in', function() { 29 | 30 | beforeEach(function (done) { 31 | request.post({ 32 | url: 'http://localhost:9000/login', 33 | form: { username: 'jose', password: 'Pa123' }, 34 | json: true 35 | }, function (err, resp, body) { 36 | this.token = body.token; 37 | done(); 38 | }.bind(this)); 39 | }); 40 | 41 | it('should do the handshake and connect', function (done){ 42 | var socket = io.connect('http://localhost:9000/admin', { 43 | 'forceNew': true, 44 | }); 45 | var token = this.token; 46 | socket.on('connect', function(){ 47 | socket.on('authenticated', function () { 48 | done(); 49 | }).emit('authenticate', { token: token }); 50 | }); 51 | }); 52 | }); 53 | 54 | }); -------------------------------------------------------------------------------- /test/authorizer_noqs.test.js: -------------------------------------------------------------------------------- 1 | var fixture = require('./fixture'); 2 | var request = require('request'); 3 | var io = require('socket.io-client'); 4 | 5 | describe('authorizer without querystring', function () { 6 | 7 | //start and stop the server 8 | before(function (done) { 9 | fixture.start({ 10 | handshake: false 11 | } , done); 12 | }); 13 | 14 | after(fixture.stop); 15 | 16 | describe('when the user is not logged in', function () { 17 | 18 | it('should close the connection after a timeout if no auth message is received', function (done){ 19 | var socket = io.connect('http://localhost:9000', { 20 | forceNew: true 21 | }); 22 | socket.once('disconnect', function () { 23 | done(); 24 | }); 25 | }); 26 | 27 | it('should not respond echo', function (done){ 28 | var socket = io.connect('http://localhost:9000', { 29 | 'forceNew':true, 30 | }); 31 | 32 | socket.on('echo-response', function () { 33 | done(new Error('this should not happen')); 34 | }).emit('echo', { hi: 123 }); 35 | 36 | setTimeout(done, 1200); 37 | }); 38 | 39 | }); 40 | 41 | describe('when the user is logged in', function() { 42 | 43 | beforeEach(function (done) { 44 | request.post({ 45 | url: 'http://localhost:9000/login', 46 | form: { username: 'jose', password: 'Pa123' }, 47 | json: true 48 | }, function (err, resp, body) { 49 | this.token = body.token; 50 | done(); 51 | }.bind(this)); 52 | }); 53 | 54 | it('should do the handshake and connect', function (done){ 55 | var socket = io.connect('http://localhost:9000', { 56 | 'forceNew':true, 57 | }); 58 | var token = this.token; 59 | socket.on('connect', function(){ 60 | socket.on('echo-response', function () { 61 | socket.close(); 62 | done(); 63 | }).on('authenticated', function () { 64 | socket.emit('echo'); 65 | }).emit('authenticate', { token: token }); 66 | }); 67 | }); 68 | }); 69 | 70 | }); -------------------------------------------------------------------------------- /test/authorizer_secret_function_noqs.test.js: -------------------------------------------------------------------------------- 1 | var fixture = require('./fixture/secret_function'); 2 | var request = require('request'); 3 | var io = require('socket.io-client'); 4 | 5 | describe('authorizer with secret function', function () { 6 | 7 | //start and stop the server 8 | before(function (done) { 9 | fixture.start({ 10 | handshake: false 11 | } , done); 12 | }); 13 | 14 | after(fixture.stop); 15 | 16 | describe('when the user is not logged in', function () { 17 | 18 | describe('and when token is not valid', function() { 19 | beforeEach(function (done) { 20 | request.post({ 21 | url: 'http://localhost:9000/login', 22 | json: { username: 'invalid_signature', password: 'Pa123' } 23 | }, function (err, resp, body) { 24 | this.invalidToken = body.token; 25 | done(); 26 | }.bind(this)); 27 | }); 28 | 29 | it('should emit unauthorized', function (done){ 30 | var socket = io.connect('http://localhost:9000', { 31 | 'forceNew':true, 32 | }); 33 | 34 | var invalidToken = this.invalidToken; 35 | socket.on('unauthorized', function() { 36 | done(); 37 | }); 38 | 39 | socket.on('connect', function(){ 40 | socket 41 | .emit('authenticate', { token: invalidToken + 'ass' }) 42 | }); 43 | }); 44 | }); 45 | 46 | }); 47 | 48 | describe('when the user is logged in', function() { 49 | 50 | beforeEach(function (done) { 51 | request.post({ 52 | url: 'http://localhost:9000/login', 53 | json: { username: 'valid_signature', password: 'Pa123' } 54 | }, function (err, resp, body) { 55 | this.token = body.token; 56 | done(); 57 | }.bind(this)); 58 | }); 59 | 60 | it('should do the handshake and connect', function (done){ 61 | var socket = io.connect('http://localhost:9000', { 62 | 'forceNew':true, 63 | }); 64 | var token = this.token; 65 | socket.on('connect', function(){ 66 | socket.on('echo-response', function () { 67 | socket.close(); 68 | done(); 69 | }).on('authenticated', function () { 70 | socket.emit('echo'); 71 | }) 72 | .emit('authenticate', { token: token }) 73 | }); 74 | }); 75 | }); 76 | 77 | }); 78 | -------------------------------------------------------------------------------- /test/authorizer_secret_function_qs.test.js: -------------------------------------------------------------------------------- 1 | var fixture = require('./fixture/secret_function'); 2 | var request = require('request'); 3 | var io = require('socket.io-client'); 4 | 5 | describe('authorizer with secret function', function () { 6 | 7 | //start and stop the server 8 | before(fixture.start); 9 | after(fixture.stop); 10 | 11 | describe('when the user is not logged in', function () { 12 | 13 | it('should emit error with unauthorized handshake', function (done){ 14 | var socket = io.connect('http://localhost:9000?token=boooooo', { 15 | 'forceNew': true 16 | }); 17 | 18 | socket.on('error', function(err){ 19 | err.message.should.eql("jwt malformed"); 20 | err.code.should.eql("invalid_token"); 21 | socket.close(); 22 | done(); 23 | }); 24 | }); 25 | 26 | }); 27 | 28 | describe('when the user is logged in', function() { 29 | 30 | beforeEach(function (done) { 31 | request.post({ 32 | url: 'http://localhost:9000/login', 33 | json: { username: 'valid_signature', password: 'Pa123' } 34 | }, function (err, resp, body) { 35 | this.token = body.token; 36 | done(); 37 | }.bind(this)); 38 | }); 39 | 40 | it('should do the handshake and connect', function (done){ 41 | var socket = io.connect('http://localhost:9000', { 42 | 'forceNew':true, 43 | 'query': 'token=' + this.token 44 | }); 45 | socket.on('connect', function(){ 46 | socket.close(); 47 | done(); 48 | }).on('error', done); 49 | }); 50 | }); 51 | 52 | describe('unsigned token', function() { 53 | beforeEach(function () { 54 | this.token = 'eyJhbGciOiJub25lIiwiY3R5IjoiSldUIn0.eyJuYW1lIjoiSm9obiBGb28ifQ.'; 55 | }); 56 | 57 | it('should not do the handshake and connect', function (done){ 58 | var socket = io.connect('http://localhost:9000', { 59 | 'forceNew':true, 60 | 'query': 'token=' + this.token 61 | }); 62 | socket.on('connect', function () { 63 | socket.close(); 64 | done(new Error('this shouldnt happen')); 65 | }).on('error', function (err) { 66 | socket.close(); 67 | err.message.should.eql("jwt signature is required"); 68 | done(); 69 | }); 70 | }); 71 | }); 72 | 73 | }); 74 | -------------------------------------------------------------------------------- /test/fixture/index.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var http = require('http'); 3 | 4 | var socketIo = require('socket.io'); 5 | var socketio_jwt = require('../../lib'); 6 | 7 | var jwt = require('jsonwebtoken'); 8 | 9 | var xtend = require('xtend'); 10 | var bodyParser = require('body-parser'); 11 | 12 | var server, sio; 13 | var enableDestroy = require('server-destroy'); 14 | 15 | exports.start = function (options, callback) { 16 | 17 | if(typeof options == 'function'){ 18 | callback = options; 19 | options = {}; 20 | } 21 | 22 | options = xtend({ 23 | secret: 'aaafoo super sercret', 24 | timeout: 1000, 25 | handshake: true 26 | }, options); 27 | 28 | var app = express(); 29 | 30 | app.use(bodyParser.json()); 31 | 32 | app.post('/login', function (req, res) { 33 | var profile = { 34 | first_name: 'John', 35 | last_name: 'Doe', 36 | email: 'john@doe.com', 37 | id: 123 38 | }; 39 | 40 | // We are sending the profile inside the token 41 | var token = jwt.sign(profile, options.secret, { expiresIn: 60*60*5 }); 42 | 43 | res.json({token: token}); 44 | }); 45 | 46 | server = http.createServer(app); 47 | 48 | sio = socketIo.listen(server); 49 | 50 | if (options.handshake) { 51 | sio.use(socketio_jwt.authorize(options)); 52 | 53 | sio.sockets.on('echo', function (m) { 54 | sio.sockets.emit('echo-response', m); 55 | }); 56 | } else { 57 | sio.sockets 58 | .on('connection', socketio_jwt.authorize(options)) 59 | .on('authenticated', function (socket) { 60 | socket.on('echo', function (m) { 61 | socket.emit('echo-response', m); 62 | }); 63 | }); 64 | } 65 | 66 | server.__sockets = []; 67 | server.on('connection', function (c) { 68 | server.__sockets.push(c); 69 | }); 70 | server.listen(9000, callback); 71 | enableDestroy(server); 72 | }; 73 | 74 | exports.stop = function (callback) { 75 | sio.close(); 76 | try { 77 | server.destroy(); 78 | } catch (er) {} 79 | callback(); 80 | }; -------------------------------------------------------------------------------- /test/fixture/namespace.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var http = require('http'); 3 | 4 | var socketIo = require('socket.io'); 5 | var socketio_jwt = require('../../lib'); 6 | 7 | var jwt = require('jsonwebtoken'); 8 | 9 | var xtend = require('xtend'); 10 | var bodyParser = require('body-parser'); 11 | 12 | var server, sio; 13 | var enableDestroy = require('server-destroy'); 14 | 15 | /** 16 | * This is an example server that shows how to do namespace authentication. 17 | * 18 | * The /admin namespace is protected by JWTs while the global namespace is public. 19 | */ 20 | exports.start = function (callback) { 21 | 22 | options = { 23 | secret: 'aaafoo super sercret', 24 | timeout: 1000, 25 | handshake: false 26 | }; 27 | 28 | var app = express(); 29 | 30 | app.use(bodyParser.json()); 31 | 32 | app.post('/login', function (req, res) { 33 | var profile = { 34 | first_name: 'John', 35 | last_name: 'Doe', 36 | email: 'john@doe.com', 37 | id: 123 38 | }; 39 | 40 | // We are sending the profile inside the token 41 | var token = jwt.sign(profile, options.secret, { expiresIn: 60*60*5 }); 42 | 43 | res.json({token: token}); 44 | }); 45 | 46 | server = http.createServer(app); 47 | 48 | sio = socketIo.listen(server); 49 | 50 | sio.on('connection', function (socket) { 51 | socket.emit('hi'); 52 | }); 53 | 54 | var admin_nsp = sio.of('/admin'); 55 | 56 | admin_nsp.on('connection', socketio_jwt.authorize(options)) 57 | .on('authenticated', function (socket) { 58 | socket.emit('hi admin'); 59 | }); 60 | 61 | 62 | server.listen(9000, callback); 63 | enableDestroy(server); 64 | }; 65 | 66 | exports.stop = function (callback) { 67 | sio.close(); 68 | try { 69 | server.destroy(); 70 | } catch (er) {} 71 | callback(); 72 | }; -------------------------------------------------------------------------------- /test/fixture/secret_function.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var http = require('http'); 3 | 4 | var socketIo = require('socket.io'); 5 | var socketio_jwt = require('../../lib'); 6 | 7 | var jwt = require('jsonwebtoken'); 8 | 9 | var xtend = require('xtend'); 10 | var bodyParser = require('body-parser'); 11 | 12 | var server, sio; 13 | var enableDestroy = require('server-destroy'); 14 | 15 | exports.start = function (options, callback) { 16 | var SECRETS = { 17 | 123: 'aaafoo super sercret', 18 | 555: 'other' 19 | }; 20 | 21 | if(typeof options == 'function'){ 22 | callback = options; 23 | options = {}; 24 | } 25 | 26 | options = xtend({ 27 | secret: function(request, decodedToken, callback) { 28 | callback(null, SECRETS[decodedToken.id]); 29 | }, 30 | timeout: 1000, 31 | handshake: true 32 | }, options); 33 | 34 | var app = express(); 35 | 36 | app.use(bodyParser.json()); 37 | 38 | app.post('/login', function (req, res) { 39 | var profile = { 40 | first_name: 'John', 41 | last_name: 'Doe', 42 | email: 'john@doe.com', 43 | id: req.body.username === 'valid_signature' ? 123 : 555 44 | }; 45 | 46 | // We are sending the profile inside the token 47 | var token = jwt.sign(profile, SECRETS[123], { expiresIn: 60*60*5 }); 48 | 49 | res.json({token: token}); 50 | }); 51 | 52 | server = http.createServer(app); 53 | 54 | sio = socketIo.listen(server); 55 | 56 | if (options.handshake) { 57 | sio.use(socketio_jwt.authorize(options)); 58 | 59 | sio.sockets.on('echo', function (m) { 60 | sio.sockets.emit('echo-response', m); 61 | }); 62 | } else { 63 | sio.sockets 64 | .on('connection', socketio_jwt.authorize(options)) 65 | .on('authenticated', function (socket) { 66 | socket.on('echo', function (m) { 67 | socket.emit('echo-response', m); 68 | }); 69 | }); 70 | } 71 | 72 | server.__sockets = []; 73 | server.on('connection', function (c) { 74 | server.__sockets.push(c); 75 | }); 76 | 77 | server.listen(9000, callback); 78 | enableDestroy(server); 79 | }; 80 | 81 | exports.stop = function (callback) { 82 | sio.close(); 83 | try { 84 | server.destroy(); 85 | } catch (er) {} 86 | 87 | callback(); 88 | }; 89 | 90 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --require should 2 | --reporter spec 3 | --timeout 15000 -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This module allows to authenticate socket.io connections with JWTs. 3 | * This is especially if you do not want to use cookies in a single page application. 4 | */ 5 | 6 | /// 7 | 8 | declare module 'socketio-jwt' { 9 | 10 | /** 11 | * Defines possible errors for the secret-callback. 12 | */ 13 | interface ISocketIOError { 14 | readonly code: string; 15 | readonly message: string; 16 | } 17 | 18 | /** 19 | * Callback gets called, if secret is given dynamically. 20 | */ 21 | interface ISocketCallback { 22 | (err: ISocketIOError, success: string): void; 23 | } 24 | 25 | interface ISocketIOMiddleware { 26 | (socket: SocketIO.Socket, fn: (err?: any) => void): void; 27 | } 28 | 29 | interface IOptions { 30 | additional_auth?: (decoded: object, onSuccess: () => void, onError: (err: (string | ISocketIOError), code: string) => void) => void; 31 | 32 | callback?: (false | number); 33 | secret: (string | ((request: any, decodedToken: object, callback: ISocketCallback) => void)); 34 | 35 | decodedPropertyName?: string; 36 | encodedPropertyName?: string; 37 | 38 | auth_header_required?: boolean; 39 | handshake?: boolean; 40 | required?: boolean; 41 | timeout?: number; 42 | } 43 | 44 | function authorize(options: IOptions/*, onConnection: Function*/): ISocketIOMiddleware; 45 | 46 | interface UnauthorizedError extends Error { 47 | readonly message: string; 48 | readonly inner: object; 49 | readonly data: { message: string, code: string, type: 'UnauthorizedError' } 50 | } 51 | 52 | var UnauthorizedError: { 53 | prototype: UnauthorizedError; 54 | new (code: string, error: { message: string }): UnauthorizedError; 55 | } 56 | 57 | /** 58 | * This is an augmented version of the SocketIO.Server. 59 | * It knows the 'authenticated' event and should be extended in future. 60 | * @see SocketIO.Server 61 | */ 62 | export interface JWTServer extends SocketIO.Server { 63 | 64 | /** 65 | * The event gets fired when a new connection is authenticated via JWT. 66 | * @param event The event being fired: 'authenticated' 67 | * @param listener A listener that should take one parameter of type Socket 68 | * @return The default '/' Namespace 69 | */ 70 | on(event: ('authenticated' | string), listener: (socket: SocketIO.Socket) => void): SocketIO.Namespace; 71 | } 72 | } 73 | --------------------------------------------------------------------------------