Skip to content
This repository has been archived by the owner on Jan 31, 2019. It is now read-only.

Commit

Permalink
Merge pull request #13 from invisiblejs/development
Browse files Browse the repository at this point in the history
v0.0.3
  • Loading branch information
facundoolano committed Jun 6, 2015
2 parents 08760f1 + 5d53919 commit 0bc0d5d
Show file tree
Hide file tree
Showing 8 changed files with 315 additions and 116 deletions.
21 changes: 21 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# EditorConfig helps developers define and maintain consistent
# coding styles between different editors and IDEs
# editorconfig.org

root = true


[*]

# Change these settings to your own preference
indent_style = space
indent_size = 2

# We recommend you to keep these unchanged
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[*.md]
trim_trailing_whitespace = false
3 changes: 3 additions & 0 deletions .jscsrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"preset": "grunt"
}
36 changes: 36 additions & 0 deletions .jshintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"bitwise":false,
"camelcase":false,
"curly":false,
"eqeqeq":true,
"freeze":true,
"immed":true,
"indent":2,
"latedef":"nofunc",
"laxbreak":true,
"laxcomma":true,
"newcap":true,
"noarg":true,
"node":true,
"strict": true,
"trailing":true,
"undef":true,
"unused":true,
"validthis":true,
"globals": {
"angular": false,
"after": false,
"afterEach": false,
"angular": false,
"before": false,
"beforeEach": false,
"browser": false,
"describe": false,
"expect": false,
"inject": false,
"it": false,
"jasmine": false,
"spyOn": false,
"window": false
}
}
5 changes: 5 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
language: node_js
node_js:
- "0.10"
- "0.12"
- "iojs"
73 changes: 65 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,41 @@
# 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
});
});
```

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);
}
}
});
```
## Usage
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:
Expand Down Expand Up @@ -47,12 +78,38 @@ function postAuthenticate(socket, data) {
* `timeout`: The amount of millisenconds to wait for a client to authenticate before disconnecting it. Defaults to 1000.
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.
96 changes: 58 additions & 38 deletions lib/socketio-auth.js
Original file line number Diff line number Diff line change
@@ -1,64 +1,60 @@
'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});

});

});

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) {
debug('Disconnecting socket %s', socket.id);
socket.disconnect('unauthorized');
Expand All @@ -67,3 +63,27 @@ module.exports = function(io, config){

});
};

/**
* 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;
}
}
12 changes: 9 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
{
"name": "socketio-auth",
"version": "0.0.2",
"version": "0.0.3",
"description": "Authentication for socket.io",
"main": "index.js",
"directories": {
"test": "test"
},
"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"
},
"repository": {
"type": "git",
Expand All @@ -28,9 +32,11 @@
"homepage": "https://github.com/invisiblejs/socketio-auth",
"dependencies": {
"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"
}
}
Loading

0 comments on commit 0bc0d5d

Please sign in to comment.