socketio-auth
Advanced tools
Comparing version 0.0.2 to 0.0.3
@@ -0,56 +1,52 @@ | ||
'use strict'; | ||
var _ = require('underscore'); | ||
var _ = require('lodash'); | ||
var debug = require('debug')('socketio-auth'); | ||
function forbidConnections(nsp) { | ||
/* | ||
Set a listener so connections from unauthenticated sockets are not | ||
considered when emitting to the namespace. The connections will be | ||
restored after authentication succeeds. | ||
*/ | ||
nsp.on('connect', function(socket){ | ||
if (!socket.auth) { | ||
debug('removing socket from %s', nsp.name); | ||
delete nsp.connected[socket.id]; | ||
} | ||
}); | ||
} | ||
function restoreConnection(nsp, socket) { | ||
/* | ||
If the socket attempted a connection before authentication, restore it. | ||
*/ | ||
if (_.findWhere(nsp.sockets, {id: socket.id})) { | ||
debug('restoring socket to %s', nsp.name); | ||
nsp.connected[socket.id] = socket; | ||
} | ||
} | ||
module.exports = function(io, config){ | ||
/* | ||
Adds connection listeners to the given socket.io server, so clients | ||
are forced to authenticate before they can receive events. | ||
*/ | ||
/** | ||
* Adds connection listeners to the given socket.io server, so clients | ||
* are forced to authenticate before they can receive events. | ||
* | ||
* @param {Object} io - the socket.io server socket | ||
* | ||
* @param {Object} config - configuration values | ||
* @param {Function} config.authenticate - indicates if authentication was successfull | ||
* @param {Function} config.postAuthenticate=noop - called after the client is authenticated | ||
* @param {Number} [config.timeout=1000] - amount of millisenconds to wait for a client to | ||
* authenticate before disconnecting it | ||
*/ | ||
module.exports = function socketIOAuth(io, config) { | ||
config = config || {}; | ||
var timeout = config.timeout || 1000; | ||
var postAuthenticate = config.postAuthenticate || function(){}; | ||
var postAuthenticate = config.postAuthenticate || _.noop; | ||
_.each(io.nsps, forbidConnections); | ||
io.on('connection', function(socket){ | ||
io.on('connection', function(socket) { | ||
socket.auth = false; | ||
socket.on('authentication', function(data){ | ||
socket.on('authentication', function(data) { | ||
config.authenticate(data, function(err, success){ | ||
config.authenticate(data, function(err, success) { | ||
if (success) { | ||
debug('Authenticated socket %s', socket.id); | ||
socket.auth = true; | ||
_.each(io.nsps, function(nsp) { | ||
restoreConnection(nsp, socket); | ||
}); | ||
socket.emit('authenticated', success); | ||
return postAuthenticate(socket, data); | ||
} else if (err) { | ||
debug('Authentication error socket %s: %s', socket.id, err.message); | ||
socket.emit('unauthorized', {message: err.message}, function() { | ||
socket.disconnect(); | ||
}); | ||
} else { | ||
debug('Authentication failure socket %s', socket.id); | ||
socket.emit('unauthorized', {message: 'Authentication failure'}, function() { | ||
socket.disconnect(); | ||
}); | ||
} | ||
socket.disconnect('unauthorized', {err: err}); | ||
}); | ||
@@ -60,4 +56,4 @@ | ||
setTimeout(function(){ | ||
//If the socket didn't authenticate after connection, disconnect it | ||
setTimeout(function() { | ||
// If the socket didn't authenticate after connection, disconnect it | ||
if (!socket.auth) { | ||
@@ -71,1 +67,25 @@ debug('Disconnecting socket %s', socket.id); | ||
}; | ||
/** | ||
* Set a listener so connections from unauthenticated sockets are not | ||
* considered when emitting to the namespace. The connections will be | ||
* restored after authentication succeeds. | ||
*/ | ||
function forbidConnections(nsp) { | ||
nsp.on('connect', function(socket) { | ||
if (!socket.auth) { | ||
debug('removing socket from %s', nsp.name); | ||
delete nsp.connected[socket.id]; | ||
} | ||
}); | ||
} | ||
/** | ||
* If the socket attempted a connection before authentication, restore it. | ||
*/ | ||
function restoreConnection(nsp, socket) { | ||
if (_.findWhere(nsp.sockets, {id: socket.id})) { | ||
debug('restoring socket to %s', nsp.name); | ||
nsp.connected[socket.id] = socket; | ||
} | ||
} |
{ | ||
"name": "socketio-auth", | ||
"version": "0.0.2", | ||
"version": "0.0.3", | ||
"description": "Authentication for socket.io", | ||
@@ -10,3 +10,7 @@ "main": "index.js", | ||
"scripts": { | ||
"test": "node_modules/mocha/bin/mocha" | ||
"jscs": "jscs lib/ test/", | ||
"jshint": "jshint lib/ test/", | ||
"lint": "npm run jshint && npm run jscs", | ||
"pretest": "npm run lint", | ||
"test": "mocha" | ||
}, | ||
@@ -32,7 +36,9 @@ "repository": { | ||
"debug": "^2.1.3", | ||
"underscore": "^1.7.0" | ||
"lodash": "^3.8.0" | ||
}, | ||
"devDependencies": { | ||
"jscs": "~1.8.0", | ||
"jshint": "~2.5.10", | ||
"mocha": "^1.21.5" | ||
} | ||
} |
@@ -1,9 +0,40 @@ | ||
# socketio-auth | ||
# socketio-auth [![Build Status](https://secure.travis-ci.org/invisiblejs/socketio-auth.png)](http://travis-ci.org/invisiblejs/socketio-auth) | ||
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. | ||
It works by marking the clients as unauthenticated by default and listening to an `authentication` event. If a client provides wrong credentials or doesn't authenticate it gets disconnected. While the server waits for a connected client to authenticate, it won't emit any events to it. | ||
Client: | ||
```javascript | ||
var socket = io.connect('http://localhost'); | ||
socket.on('connect', function(){ | ||
socket.emit('authentication', {username: "John", password: "secret"}); | ||
socket.on('authenticated', function() { | ||
// use the socket as usual | ||
}); | ||
}); | ||
``` | ||
## Usage | ||
Server: | ||
```javascript | ||
var io = require('socket.io').listen(app); | ||
require('socketio-auth')(io, { | ||
authenticate: function (data, callback) { | ||
//get credentials sent by the client | ||
var username = data.username; | ||
var password = data.password; | ||
db.findUser('User', {username:username}, function(err, user) { | ||
//inform the callback of auth success/failure | ||
if (err || !user) return callback(new Error("User not found")); | ||
return callback(null, user.password == password); | ||
} | ||
} | ||
}); | ||
``` | ||
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 and uses them to authenticate. | ||
## Configuration | ||
To setup authentication for the socket.io connections, just pass the server socket to socketio-auth with a configuration object: | ||
@@ -50,10 +81,36 @@ | ||
The client just needs to make sure to authenticate after connecting: | ||
## Auth error messages | ||
When client authentication fails, the server will emit an `unauthorized` event with the failure reason: | ||
```javascript | ||
var socket = io.connect('http://localhost'); | ||
socket.on('connect', function(){ | ||
socket.emit('authentication', {username: "John", password: "secret"}); | ||
socket.emit('authentication', {username: "John", password: "secret"}); | ||
socket.on('unauthorized', function(err){ | ||
console.log("There was an error with the authentication:", err.message); | ||
}); | ||
``` | ||
The server will emit the `authenticated` event to confirm authentication. | ||
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'` | ||
```javascript | ||
function authenticate(data, callback) { | ||
db.findUser('User', {username:data.username}, function(err, user) { | ||
if (err || !user) { | ||
//err.message will be "User not found" | ||
return callback(new Error("User not found")); | ||
} | ||
//if wrong password err.message will be "Authentication failure" | ||
return callback(null, user.password == data.password); | ||
} | ||
} | ||
``` | ||
After receiving the `unauthorized` event, the client is disconnected. | ||
## Implementation details | ||
**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. | ||
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. | ||
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. |
185
test/test.js
@@ -0,1 +1,3 @@ | ||
'use strict'; | ||
var assert = require('assert'); | ||
@@ -6,91 +8,105 @@ var EventEmitter = require('events').EventEmitter; | ||
function NamespaceMock(name) { | ||
this.name = name; | ||
this.sockets = []; | ||
this.connected = {} | ||
this.name = name; | ||
this.sockets = []; | ||
this.connected = {}; | ||
} | ||
util.inherits(NamespaceMock, EventEmitter); | ||
NamespaceMock.prototype.connect = function(client) { | ||
this.sockets.push(client); | ||
this.connected[client.id] = client; | ||
this.emit('connection', client); | ||
} | ||
this.sockets.push(client); | ||
this.connected[client.id] = client; | ||
this.emit('connection', client); | ||
}; | ||
function ServerSocketMock () { | ||
this.nsps = { | ||
"/User": new NamespaceMock("/User"), | ||
"/Message": new NamespaceMock("/Message") | ||
this.nsps = { | ||
'/User': new NamespaceMock('/User'), | ||
'/Message': new NamespaceMock('/Message') | ||
}; | ||
} | ||
util.inherits(ServerSocketMock, EventEmitter); | ||
ServerSocketMock.prototype.connect = function(nsp, client) { | ||
this.emit('connection', client); | ||
this.nsps[nsp].connect(client); | ||
} | ||
this.emit('connection', client); | ||
this.nsps[nsp].connect(client); | ||
}; | ||
ServerSocketMock.prototype.emit = function(event, data, cb) { | ||
ServerSocketMock.super_.prototype.emit.call(this, event, data); | ||
//fakes client acknowledgment | ||
if (cb) { | ||
process.nextTick(cb); | ||
} | ||
}; | ||
function ClientSocketMock(id) { | ||
this.id = id; | ||
this.client = {} | ||
this.id = id; | ||
this.client = {}; | ||
} | ||
util.inherits(ClientSocketMock, EventEmitter); | ||
ClientSocketMock.prototype.disconnect = function() { | ||
this.emit('disconnect'); | ||
} | ||
this.emit('disconnect'); | ||
}; | ||
function authenticate(data, cb) { | ||
if(!data.token) return cb(new Error("Missing credentials")); | ||
cb(null, data.token == "fixedtoken"); | ||
if (!data.token) { | ||
cb(new Error('Missing credentials')); | ||
} | ||
cb(null, data.token === 'fixedtoken'); | ||
} | ||
describe('Server socket authentication', function(){ | ||
var server; | ||
var client; | ||
describe('Server socket authentication', function() { | ||
var server; | ||
var client; | ||
beforeEach(function(){ | ||
beforeEach(function() { | ||
server = new ServerSocketMock(); | ||
require('../lib/socketio-auth')(server, { | ||
timeout:80, | ||
authenticate: authenticate | ||
}); | ||
}); | ||
client = new ClientSocketMock(5); | ||
}); | ||
}); | ||
it('Should mark the socket as unauthenticated upon connection', function(done) { | ||
assert(client.auth == undefined); | ||
server.connect("/User", client); | ||
process.nextTick(function(){ | ||
assert(client.auth == false); | ||
it('Should mark the socket as unauthenticated upon connection', function(done) { | ||
assert(client.auth === undefined); | ||
server.connect('/User', client); | ||
process.nextTick(function() { | ||
assert(client.auth === false); | ||
done(); | ||
}); | ||
}); | ||
}); | ||
it('Should not send messages to unauthenticated sockets', function(done) { | ||
server.connect("/User", client); | ||
process.nextTick(function(){ | ||
server.connect('/User', client); | ||
process.nextTick(function() { | ||
assert(!server.nsps['/User'][5]); | ||
done(); | ||
}); | ||
}); | ||
}); | ||
it('Should disconnect sockets that do not authenticate', function(done) { | ||
server.connect("/User", client); | ||
client.on('disconnect', function(){ | ||
done(); | ||
server.connect('/User', client); | ||
client.on('disconnect', function() { | ||
done(); | ||
}); | ||
}); | ||
}); | ||
it('Should authenticate with valid credentials', function(done) { | ||
server.connect("/User", client); | ||
process.nextTick(function(){ | ||
client.on('authenticated', function(){ | ||
server.connect('/User', client); | ||
process.nextTick(function() { | ||
client.on('authenticated', function() { | ||
assert(client.auth); | ||
done(); | ||
}); | ||
client.emit('authentication', {token: "fixedtoken"}); | ||
client.emit('authentication', {token: 'fixedtoken'}); | ||
}); | ||
}); | ||
}); | ||
@@ -102,7 +118,7 @@ it('Should call post auth function', function(done) { | ||
var postAuth = function(socket, tokenData) { | ||
assert.equal(tokenData.token, "fixedtoken"); | ||
assert.equal(tokenData.token, 'fixedtoken'); | ||
assert.equal(socket, client); | ||
done(); | ||
} | ||
}; | ||
require('../lib/socketio-auth')(server, { | ||
@@ -114,39 +130,74 @@ timeout:80, | ||
server.connect("/User", client); | ||
process.nextTick(function(){ | ||
client.emit('authentication', {token: "fixedtoken"}); | ||
server.connect('/User', client); | ||
process.nextTick(function() { | ||
client.emit('authentication', {token: 'fixedtoken'}); | ||
}); | ||
}); | ||
}); | ||
it('Should send updates to authenticated sockets', function(done) { | ||
server.connect("/User", client); | ||
process.nextTick(function(){ | ||
client.on('authenticated', function(){ | ||
server.connect('/User', client); | ||
process.nextTick(function() { | ||
client.on('authenticated', function() { | ||
assert.equal(server.nsps['/User'].connected[5], client); | ||
done(); | ||
}); | ||
client.emit('authentication', {token: "fixedtoken"}); | ||
client.emit('authentication', {token: 'fixedtoken'}); | ||
}); | ||
}); | ||
it('Should not authenticate without credentials', function(done) { | ||
server.connect("/User", client); | ||
process.nextTick(function(){ | ||
client.once('disconnect', function(){ | ||
it('Should send error event on invalid credentials', function(done) { | ||
server.connect('/User', client); | ||
process.nextTick(function() { | ||
client.once('unauthorized', function(err) { | ||
assert.equal(err.message, 'Authentication failure'); | ||
done(); | ||
}); | ||
client.emit('authentication', {}); | ||
client.emit('authentication', {token: 'invalid'}); | ||
}); | ||
}); | ||
it('Should not authenticate with invalid credentials', function(done) { | ||
server.connect("/User", client); | ||
process.nextTick(function(){ | ||
client.once('disconnect', function(){ | ||
it('Should send error event on missing credentials', function(done) { | ||
server.connect('/User', client); | ||
process.nextTick(function() { | ||
client.once('unauthorized', function(err) { | ||
assert.equal(err.message, 'Missing credentials'); | ||
done(); | ||
}); | ||
client.emit('authentication', {token: "invalid"}); | ||
client.emit('authentication', {}); | ||
}); | ||
}); | ||
it('Should disconnect on missing credentials', function(done) { | ||
server.connect('/User', client); | ||
process.nextTick(function() { | ||
client.once('unauthorized', function() { | ||
//make sure disconnect comes after unauthorized | ||
client.once('disconnect', function() { | ||
done(); | ||
}); | ||
}); | ||
client.emit('authentication', {}); | ||
}); | ||
}); | ||
it('Should disconnect on invalid credentials', function(done) { | ||
server.connect('/User', client); | ||
process.nextTick(function() { | ||
client.once('unauthorized', function() { | ||
//make sure disconnect comes after unauthorized | ||
client.once('disconnect', function() { | ||
done(); | ||
}); | ||
}); | ||
client.emit('authentication', {token: 'invalid'}); | ||
}); | ||
}); | ||
}); |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
15408
10
239
116
3
1
+ Addedlodash@^3.8.0
+ Addedlodash@3.10.1(transitive)
- Removedunderscore@^1.7.0
- Removedunderscore@1.13.7(transitive)