├── .editorconfig ├── .gitignore ├── .jscsrc ├── .jshintrc ├── .travis.yml ├── README.md ├── index.js ├── lib └── socketio-auth.js ├── package.json └── test └── test.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | 10 | # Change these settings to your own preference 11 | indent_style = space 12 | indent_size = 2 13 | 14 | # We recommend you to keep these unchanged 15 | end_of_line = lf 16 | charset = utf-8 17 | trim_trailing_whitespace = true 18 | insert_final_newline = true 19 | 20 | [*.md] 21 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Commenting this out is preferred by some people, see 24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 25 | node_modules 26 | 27 | # Users Environment Variables 28 | .lock-wscript 29 | -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "grunt" 3 | } 4 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "bitwise":false, 3 | "camelcase":false, 4 | "curly":false, 5 | "eqeqeq":true, 6 | "freeze":true, 7 | "immed":true, 8 | "indent":2, 9 | "latedef":"nofunc", 10 | "laxbreak":true, 11 | "laxcomma":true, 12 | "newcap":true, 13 | "noarg":true, 14 | "node":true, 15 | "strict": true, 16 | "trailing":true, 17 | "undef":true, 18 | "unused":true, 19 | "validthis":true, 20 | "globals": { 21 | "angular": false, 22 | "after": false, 23 | "afterEach": false, 24 | "angular": false, 25 | "before": false, 26 | "beforeEach": false, 27 | "browser": false, 28 | "describe": false, 29 | "expect": false, 30 | "inject": false, 31 | "it": false, 32 | "jasmine": false, 33 | "spyOn": false, 34 | "window": false 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | - "0.12" 5 | - "iojs" 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # socketio-auth [![Build Status](https://secure.travis-ci.org/facundoolano/socketio-auth.png)](http://travis-ci.org/facundoolano/socketio-auth) 2 | 3 | This module provides hooks to implement authentication in [socket.io](https://github.com/Automattic/socket.io) without using querystrings to send credentials, which is not a good security practice. 4 | 5 | **NOTE: I'm not maintaining this project anymore. I don't use socket.io so I can't tell if it's still useful.** 6 | 7 | Client: 8 | ```javascript 9 | var socket = io.connect('http://localhost'); 10 | socket.on('connect', function(){ 11 | socket.emit('authentication', {username: "John", password: "secret"}); 12 | socket.on('authenticated', function() { 13 | // use the socket as usual 14 | }); 15 | }); 16 | ``` 17 | 18 | Server: 19 | ```javascript 20 | var io = require('socket.io').listen(app); 21 | 22 | require('socketio-auth')(io, { 23 | authenticate: function (socket, data, callback) { 24 | //get credentials sent by the client 25 | var username = data.username; 26 | var password = data.password; 27 | 28 | db.findUser('User', {username:username}, function(err, user) { 29 | 30 | //inform the callback of auth success/failure 31 | if (err || !user) return callback(new Error("User not found")); 32 | return callback(null, user.password == password); 33 | }); 34 | } 35 | }); 36 | ``` 37 | 38 | The client should send an `authentication` event right after connecting, including whatever credentials are needed by the server to identify the user (i.e. user/password, auth token, etc.). The `authenticate` function receives those same credentials in 'data', and the actual 'socket' in case header information like the origin domain is important, and uses them to authenticate. 39 | 40 | ## Configuration 41 | 42 | To setup authentication for the socket.io connections, just pass the server socket to socketio-auth with a configuration object: 43 | 44 | ```javascript 45 | var io = require('socket.io').listen(app); 46 | 47 | require('socketio-auth')(io, { 48 | authenticate: authenticate, 49 | postAuthenticate: postAuthenticate, 50 | disconnect: disconnect, 51 | timeout: 1000 52 | }); 53 | ``` 54 | 55 | The supported parameters are: 56 | 57 | * `authenticate`: The only required parameter. It's a function that takes the data sent by the client and calls a callback indicating if authentication was successfull: 58 | 59 | ```javascript 60 | function authenticate(socket, data, callback) { 61 | var username = data.username; 62 | var password = data.password; 63 | 64 | db.findUser('User', {username:username}, function(err, user) { 65 | if (err || !user) return callback(new Error("User not found")); 66 | return callback(null, user.password == password); 67 | }); 68 | } 69 | ``` 70 | * `postAuthenticate`: a function to be called after the client is authenticated. It's useful to keep track of the user associated with a client socket: 71 | 72 | ```javascript 73 | function postAuthenticate(socket, data) { 74 | var username = data.username; 75 | 76 | db.findUser('User', {username:username}, function(err, user) { 77 | socket.client.user = user; 78 | }); 79 | } 80 | ``` 81 | * `disconnect`: a function to be called after the client is disconnected. 82 | 83 | ```javascript 84 | function disconnect(socket) { 85 | console.log(socket.id + ' disconnected'); 86 | } 87 | ``` 88 | 89 | * `timeout`: The amount of millisenconds to wait for a client to authenticate before disconnecting it. Defaults to 1000. The value 'none' disables the timeout feature. 90 | 91 | ## Auth error messages 92 | 93 | When client authentication fails, the server will emit an `unauthorized` event with the failure reason: 94 | 95 | ```javascript 96 | socket.emit('authentication', {username: "John", password: "secret"}); 97 | socket.on('unauthorized', function(err){ 98 | console.log("There was an error with the authentication:", err.message); 99 | }); 100 | ``` 101 | 102 | The value of `err.message` depends on the outcome of the `authenticate` function used in the server: if the callback receives an error its message is used, if the success parameter is false the message is `'Authentication failure'` 103 | 104 | ```javascript 105 | function authenticate(socket, data, callback) { 106 | db.findUser('User', {username:data.username}, function(err, user) { 107 | if (err || !user) { 108 | //err.message will be "User not found" 109 | return callback(new Error("User not found")); 110 | } 111 | 112 | //if wrong password err.message will be "Authentication failure" 113 | return callback(null, user.password == data.password); 114 | }); 115 | } 116 | ``` 117 | 118 | After receiving the `unauthorized` event, the client is disconnected. 119 | 120 | ## Implementation details 121 | 122 | **socketio-auth** implements two-step authentication: upon connection, the server marks the clients as unauthenticated and listens to an `authentication` event. If a client provides wrong credentials or doesn't authenticate after a timeout period it gets disconnected. While the server waits for a connected client to authenticate, it won't emit any broadcast/namespace events to it. By using this approach the sensitive authentication data, such as user credentials or tokens, travel in the body of a secure request, rather than a querystring that can be logged or cached. 123 | 124 | Note that during the window while the server waits for authentication, direct messages emitted to the socket (i.e. `socket.emit(msg)`) *will* be received by the client. To avoid those types of messages reaching unauthorized clients, the emission code should either be defined after the `authenticated` event is triggered by the server or the `socket.auth` flag should be checked to make sure the socket is authenticated. 125 | 126 | See [this blog post](https://facundoolano.wordpress.com/2014/10/11/better-authentication-for-socket-io-no-query-strings/) for more details on this authentication method. 127 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/socketio-auth'); 2 | -------------------------------------------------------------------------------- /lib/socketio-auth.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var debug = require('debug')('socketio-auth'); 5 | 6 | /** 7 | * Adds connection listeners to the given socket.io server, so clients 8 | * are forced to authenticate before they can receive events. 9 | * 10 | * @param {Object} io - the socket.io server socket 11 | * 12 | * @param {Object} config - configuration values 13 | * @param {Function} config.authenticate - indicates if authentication was successfull 14 | * @param {Function} config.postAuthenticate=noop - called after the client is authenticated 15 | * @param {Function} config.disconnect=noop - called after the client is disconnected 16 | * @param {Number} [config.timeout=1000] - amount of millisenconds to wait for a client to 17 | * authenticate before disconnecting it. A value of 'none' means no connection timeout. 18 | */ 19 | module.exports = function socketIOAuth(io, config) { 20 | config = config || {}; 21 | var timeout = config.timeout || 1000; 22 | var postAuthenticate = config.postAuthenticate || _.noop; 23 | var disconnect = config.disconnect || _.noop; 24 | 25 | _.each(io.nsps, forbidConnections); 26 | io.on('connection', function(socket) { 27 | 28 | socket.auth = false; 29 | socket.on('authentication', function(data) { 30 | 31 | config.authenticate(socket, data, function(err, success) { 32 | if (success) { 33 | debug('Authenticated socket %s', socket.id); 34 | socket.auth = true; 35 | 36 | _.each(io.nsps, function(nsp) { 37 | restoreConnection(nsp, socket); 38 | }); 39 | 40 | socket.emit('authenticated', success); 41 | return postAuthenticate(socket, data); 42 | } else if (err) { 43 | debug('Authentication error socket %s: %s', socket.id, err.message); 44 | socket.emit('unauthorized', {message: err.message}, function() { 45 | socket.disconnect(); 46 | }); 47 | } else { 48 | debug('Authentication failure socket %s', socket.id); 49 | socket.emit('unauthorized', {message: 'Authentication failure'}, function() { 50 | socket.disconnect(); 51 | }); 52 | } 53 | 54 | }); 55 | 56 | }); 57 | 58 | socket.on('disconnect', function() { 59 | return disconnect(socket); 60 | }); 61 | 62 | if (timeout !== 'none') { 63 | setTimeout(function() { 64 | // If the socket didn't authenticate after connection, disconnect it 65 | if (!socket.auth) { 66 | debug('Disconnecting socket %s', socket.id); 67 | socket.disconnect('unauthorized'); 68 | } 69 | }, timeout); 70 | } 71 | 72 | }); 73 | }; 74 | 75 | /** 76 | * Set a listener so connections from unauthenticated sockets are not 77 | * considered when emitting to the namespace. The connections will be 78 | * restored after authentication succeeds. 79 | */ 80 | function forbidConnections(nsp) { 81 | nsp.on('connect', function(socket) { 82 | if (!socket.auth) { 83 | debug('removing socket from %s', nsp.name); 84 | delete nsp.connected[socket.id]; 85 | } 86 | }); 87 | } 88 | 89 | /** 90 | * If the socket attempted a connection before authentication, restore it. 91 | */ 92 | function restoreConnection(nsp, socket) { 93 | if (_.find(nsp.sockets, {id: socket.id})) { 94 | debug('restoring socket to %s', nsp.name); 95 | nsp.connected[socket.id] = socket; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "socketio-auth", 3 | "version": "0.1.1", 4 | "description": "Authentication for socket.io", 5 | "main": "index.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "scripts": { 10 | "jscs": "jscs lib/ test/", 11 | "jshint": "jshint lib/ test/", 12 | "lint": "npm run jshint && npm run jscs", 13 | "pretest": "npm run lint", 14 | "test": "mocha" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/facundoolano/socketio-auth" 19 | }, 20 | "keywords": [ 21 | "socket", 22 | "socket.io", 23 | "authentication", 24 | "auth", 25 | "invisible.js" 26 | ], 27 | "author": "Facundo Olano and Martín Paulucci", 28 | "license": "ISC", 29 | "bugs": { 30 | "url": "https://github.com/facundoolano/socketio-auth/issues" 31 | }, 32 | "homepage": "https://github.com/facundoolano/socketio-auth", 33 | "dependencies": { 34 | "debug": "^2.1.3", 35 | "lodash": "^4.17.5" 36 | }, 37 | "devDependencies": { 38 | "jscs": "~1.8.0", 39 | "jshint": "~2.5.10", 40 | "mocha": "^1.21.5" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assert = require('assert'); 4 | var EventEmitter = require('events').EventEmitter; 5 | var util = require('util'); 6 | 7 | function NamespaceMock(name) { 8 | this.name = name; 9 | this.sockets = []; 10 | this.connected = {}; 11 | } 12 | 13 | util.inherits(NamespaceMock, EventEmitter); 14 | 15 | NamespaceMock.prototype.connect = function(client) { 16 | this.sockets.push(client); 17 | this.connected[client.id] = client; 18 | this.emit('connection', client); 19 | }; 20 | 21 | function ServerSocketMock () { 22 | this.nsps = { 23 | '/User': new NamespaceMock('/User'), 24 | '/Message': new NamespaceMock('/Message') 25 | }; 26 | } 27 | 28 | util.inherits(ServerSocketMock, EventEmitter); 29 | 30 | ServerSocketMock.prototype.connect = function(nsp, client) { 31 | this.emit('connection', client); 32 | this.nsps[nsp].connect(client); 33 | }; 34 | 35 | ServerSocketMock.prototype.emit = function(event, data, cb) { 36 | ServerSocketMock.super_.prototype.emit.call(this, event, data); 37 | 38 | //fakes client acknowledgment 39 | if (cb) { 40 | process.nextTick(cb); 41 | } 42 | }; 43 | 44 | function ClientSocketMock(id) { 45 | this.id = id; 46 | this.client = {}; 47 | } 48 | util.inherits(ClientSocketMock, EventEmitter); 49 | 50 | ClientSocketMock.prototype.disconnect = function() { 51 | this.emit('disconnect'); 52 | }; 53 | 54 | function authenticate(socket, data, cb) { 55 | if (!data.token) { 56 | cb(new Error('Missing credentials')); 57 | } 58 | 59 | cb(null, data.token === 'fixedtoken'); 60 | } 61 | 62 | describe('Server socket authentication', function() { 63 | var server; 64 | var client; 65 | 66 | beforeEach(function() { 67 | server = new ServerSocketMock(); 68 | 69 | require('../lib/socketio-auth')(server, { 70 | timeout:80, 71 | authenticate: authenticate 72 | }); 73 | 74 | client = new ClientSocketMock(5); 75 | }); 76 | 77 | it('Should mark the socket as unauthenticated upon connection', function(done) { 78 | assert(client.auth === undefined); 79 | server.connect('/User', client); 80 | process.nextTick(function() { 81 | assert(client.auth === false); 82 | done(); 83 | }); 84 | }); 85 | 86 | it('Should not send messages to unauthenticated sockets', function(done) { 87 | server.connect('/User', client); 88 | process.nextTick(function() { 89 | assert(!server.nsps['/User'][5]); 90 | done(); 91 | }); 92 | }); 93 | 94 | it('Should disconnect sockets that do not authenticate', function(done) { 95 | server.connect('/User', client); 96 | client.on('disconnect', function() { 97 | done(); 98 | }); 99 | }); 100 | 101 | it('Should authenticate with valid credentials', function(done) { 102 | server.connect('/User', client); 103 | process.nextTick(function() { 104 | client.on('authenticated', function() { 105 | assert(client.auth); 106 | done(); 107 | }); 108 | client.emit('authentication', {token: 'fixedtoken'}); 109 | }); 110 | }); 111 | 112 | it('Should call post auth function', function(done) { 113 | server = new ServerSocketMock(); 114 | client = new ClientSocketMock(5); 115 | 116 | var postAuth = function(socket, tokenData) { 117 | assert.equal(tokenData.token, 'fixedtoken'); 118 | assert.equal(socket, client); 119 | done(); 120 | }; 121 | 122 | require('../lib/socketio-auth')(server, { 123 | timeout:80, 124 | authenticate: authenticate, 125 | postAuthenticate: postAuth 126 | }); 127 | 128 | server.connect('/User', client); 129 | 130 | process.nextTick(function() { 131 | client.emit('authentication', {token: 'fixedtoken'}); 132 | }); 133 | }); 134 | 135 | it('Should send updates to authenticated sockets', function(done) { 136 | server.connect('/User', client); 137 | 138 | process.nextTick(function() { 139 | client.on('authenticated', function() { 140 | assert.equal(server.nsps['/User'].connected[5], client); 141 | done(); 142 | }); 143 | client.emit('authentication', {token: 'fixedtoken'}); 144 | }); 145 | }); 146 | 147 | it('Should send error event on invalid credentials', function(done) { 148 | server.connect('/User', client); 149 | 150 | process.nextTick(function() { 151 | client.once('unauthorized', function(err) { 152 | assert.equal(err.message, 'Authentication failure'); 153 | done(); 154 | }); 155 | client.emit('authentication', {token: 'invalid'}); 156 | }); 157 | }); 158 | 159 | it('Should send error event on missing credentials', function(done) { 160 | server.connect('/User', client); 161 | 162 | process.nextTick(function() { 163 | client.once('unauthorized', function(err) { 164 | assert.equal(err.message, 'Missing credentials'); 165 | done(); 166 | }); 167 | client.emit('authentication', {}); 168 | }); 169 | }); 170 | 171 | it('Should disconnect on missing credentials', function(done) { 172 | server.connect('/User', client); 173 | 174 | process.nextTick(function() { 175 | client.once('unauthorized', function() { 176 | //make sure disconnect comes after unauthorized 177 | client.once('disconnect', function() { 178 | done(); 179 | }); 180 | }); 181 | 182 | client.emit('authentication', {}); 183 | }); 184 | }); 185 | 186 | it('Should disconnect on invalid credentials', function(done) { 187 | server.connect('/User', client); 188 | 189 | process.nextTick(function() { 190 | client.once('unauthorized', function() { 191 | //make sure disconnect comes after unauthorized 192 | client.once('disconnect', function() { 193 | done(); 194 | }); 195 | }); 196 | client.emit('authentication', {token: 'invalid'}); 197 | }); 198 | }); 199 | 200 | }); 201 | 202 | describe('Server socket disconnect', function() { 203 | var server; 204 | var client; 205 | 206 | it('Should call discon function', function(done) { 207 | server = new ServerSocketMock(); 208 | client = new ClientSocketMock(5); 209 | 210 | var discon = function(socket) { 211 | assert.equal(socket, client); 212 | done(); 213 | }; 214 | 215 | require('../lib/socketio-auth')(server, { 216 | timeout:80, 217 | authenticate: authenticate, 218 | disconnect: discon 219 | }); 220 | 221 | server.connect('/User', client); 222 | 223 | process.nextTick(function() { 224 | client.disconnect(); 225 | }); 226 | }); 227 | 228 | }); 229 | --------------------------------------------------------------------------------