faye-authentication 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: cfd708e9dbb1900470d238f7d427ba3a6bd4322c
4
- data.tar.gz: 5d087943cf31d1ffe01f645039d8c9c034fdeacd
3
+ metadata.gz: 184e59a30a70d8300c56d55355689fc266f4c1d8
4
+ data.tar.gz: e27e148c68384f0f1f8d74cec6e7bbd92769dd51
5
5
  SHA512:
6
- metadata.gz: 7fa639d27c2ce7ecd9876355b241cb3db5c490f8082db95c156410f16bc3ee5790bc88020a2d4055c2a4f341569e3696299794bb52e672514d16390c2f0a5e93
7
- data.tar.gz: 4f2d0c80f71e1defc714a0a76da7472390dfb19b300b469c3d71d1f38189b418364912fa3988da35048598db584f55ea0bc6bc6891e464a9f632567e4229e85b
6
+ metadata.gz: 62c1b8a62630230868e2f748219970745e72a868dcafdc3de4112c82f25a25e4ee0f7de829caa7b7e85fdf017040287b13d7ce6b47495a9475f3aa6f772a0c81
7
+ data.tar.gz: 8dd4d44413f43949c21512b3932f3f06659ee1ff5193eed99c5ef75cba04218b68dfafcb33042b9af251d1169bf984538d6e29e4b2b4e255e3d4c735e37060f3
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## 0.2.0
2
+
3
+ - Use JWT instead of HMAC for signing the messages
4
+ - Allow expiration of the signature
5
+ - The client javascript extension now takes the faye client as its frst parameter
data/README.md CHANGED
@@ -1,19 +1,25 @@
1
- # Faye::Authentication [![Build Status](https://travis-ci.org/josevalim/rails-footnotes.svg?branch=master)](https://travis-ci.org/josevalim/rails-footnotes) [![Code Climate](https://codeclimate.com/github/dimelo/faye-authentication.png)](https://codeclimate.com/github/dimelo/faye-authentication)
1
+ # Faye::Authentication [![Build Status](https://travis-ci.org/dimelo/faye-authentication.svg?branch=master)](https://travis-ci.org/dimelo/faye-authentication) [![Code Climate](https://codeclimate.com/github/dimelo/faye-authentication.png)](https://codeclimate.com/github/dimelo/faye-authentication)
2
2
 
3
3
  Authentification implementation for faye
4
4
 
5
- Currently Implemented :
6
- - Javascript Client Extention
7
- - Ruby Server Extension
8
- - Ruby utils for signing messages
9
- - **Want another one ? Pull requests are welcome.**
5
+ ## Principle
6
+
7
+ This project implements (channel,client_id) authentication on channel subscription and publication and delegate it to an external HTTP endpoint through JWT tupple signature based on a shared secret key between Faye server and the endpoint.
10
8
 
11
- The authentication is performed through an Ajax Call to the webserver (JQuery needed).
9
+ On channel subscription the JS client performe an Ajax Call to an HTTP endpoint to be granted a signature that will be provided to Faye Server to connect and publish to channel. The authentication of the endpoint itself is up to you but in the general case this will be a session authenticated resource of your app and you will decide to provide the signature or not depending on your own business logic.
12
10
 
13
- For each channel and client id pair, a signature is added to the message.
11
+ This signature is required and valid for each channel and client id tupple and rely on JWT for security.
14
12
 
15
- Thanks to a shared key, the Faye Server will check the signature and reject the
16
- message if the signature is incorrect or not present.
13
+ The Faye server will verify the (channel,client_id) tupple signature and reject the message if the signature
14
+ is incorrect or not present.
15
+
16
+ ## Current support
17
+
18
+ Currently Implemented :
19
+ - Javascript Client Extention (JQuery needed)
20
+ - Ruby Faye Server Extension
21
+ - Ruby utils to signing messages in your webapp
22
+ - **Want another one ? Pull requests are welcome.**
17
23
 
18
24
  ## Installation
19
25
 
@@ -31,26 +37,9 @@ Or install it yourself as:
31
37
 
32
38
  ## Usage
33
39
 
34
- ### Javascript client extension
35
-
36
- Add the extension to your faye client :
37
-
38
- ````javascript
39
- var client = new Faye.Client('http://my.server/faye');
40
- client.add_extension(new FayeAuthentication());
41
- ````
42
-
43
- By default, when sending a subscribe request or publishing a message, the extension
44
- will issue an AJAX request to ``/faye/auth``
45
-
46
- If you wish to change the endpoint, you can supply it as the first argument of the extension constructor :
40
+ ### Authentication endpoint requirements
47
41
 
48
- client.add_extension(new FayeAuthentication('/my_custom_auth_endpoint'));
49
-
50
-
51
- ### Ruby utils
52
-
53
- The endpoint will a POST request, and shall return a JSON hash with a ``signature`` key.
42
+ The endpoint will receive a POST request, and shall return a JSON hash with a ``signature`` key.
54
43
 
55
44
  The parameters sent to the endpoint are the following :
56
45
 
@@ -73,7 +62,7 @@ Example (For a Rails application)
73
62
  ````ruby
74
63
  def auth
75
64
  if current_user.can?(:read, params[:message][:channel])
76
- render json: {signature: Faye::Authentication.sign(params[:message], 'your private key')}
65
+ render json: {signature: Faye::Authentication.sign(params[:message].slice(:channel,:clientId), 'your shared secret key')}
77
66
  else
78
67
  render json: {error: 'Not authorized'}, status: 403
79
68
  end
@@ -85,8 +74,23 @@ A Ruby HTTP Client is also available for publishing messages to your faye server
85
74
  without the hassle of using EventMachine :
86
75
 
87
76
  ````ruby
88
- Faye::Authentication::HTTPClient.publish('/channel', 'data', 'your private key')
77
+ Faye::Authentication::HTTPClient.publish('http://localhost:9290/faye', '/channel', 'data', 'your private key')
89
78
  ````
79
+ ### Javascript client extension
80
+
81
+ Add the extension to your faye client :
82
+
83
+ ````javascript
84
+ var client = new Faye.Client('http://my.server/faye');
85
+ client.addExtension(new FayeAuthentication(client));
86
+ ````
87
+
88
+ By default, when sending a subscribe request or publishing a message, the extension
89
+ will issue an AJAX request to ``/faye/auth``
90
+
91
+ If you wish to change the endpoint, you can supply it as the second argument of the extension constructor, the first one being the client :
92
+
93
+ client.addExtension(new FayeAuthentication(client, '/my_custom_auth_endpoint'));
90
94
 
91
95
  ### Faye server extension
92
96
 
@@ -94,9 +98,16 @@ Instanciate the extension with your secret key and add it to the server :
94
98
 
95
99
  ````ruby
96
100
  server = Faye::RackAdapter.new(:mount => '/faye', :timeout => 15)
97
- server.add_extension Faye::Authentication::Extension.new('your private key')
101
+ server.add_extension Faye::Authentication::Extension.new('your shared secret key')
98
102
  ````
99
103
 
104
+ Faye::Authentication::Extension expect that :
105
+ - a ``signature`` is present in the message for publish/subscribe request
106
+ - this signature is a valid JWT token
107
+ - the JWT payload contains "channel", "clientId" and a expiration timestamp "exp" that is not in the past.
108
+
109
+ Otherwise Faye Server will refuse the message.
110
+
100
111
  ## Contributing
101
112
 
102
113
  1. Fork it ( https://github.com/dimelo/faye-authentication/fork )
@@ -1,6 +1,8 @@
1
- function FayeAuthentication(endpoint) {
1
+ function FayeAuthentication(client, endpoint) {
2
+ this._client = client;
2
3
  this._endpoint = endpoint || '/faye/auth';
3
4
  this._signatures = {};
5
+ this._outbox = {};
4
6
  }
5
7
 
6
8
  FayeAuthentication.prototype.endpoint = function() {
@@ -11,15 +13,17 @@ FayeAuthentication.prototype.signMessage = function(message, callback) {
11
13
  var channel = message.subscription || message.channel;
12
14
  var clientId = message.clientId;
13
15
 
16
+ var self = this;
14
17
  if (!this._signatures[clientId])
15
18
  this._signatures[clientId] = {};
16
19
  if (this._signatures[clientId][channel]) {
17
20
  this._signatures[clientId][channel].then(function(signature) {
18
21
  message.signature = signature;
22
+ if (!message.retried)
23
+ self._outbox[message.id] = {message: message, clientId: clientId};
19
24
  callback(message);
20
25
  });
21
26
  } else {
22
- var self = this;
23
27
  self._signatures[clientId][channel] = new Faye.Promise(function(success, failure) {
24
28
  $.post(self.endpoint(), {message: {channel: channel, clientId: clientId}}, function(response) {
25
29
  success(response.signature);
@@ -29,16 +33,19 @@ FayeAuthentication.prototype.signMessage = function(message, callback) {
29
33
  });
30
34
  self._signatures[clientId][channel].then(function(signature) {
31
35
  message.signature = signature;
36
+ if (!message.retried){
37
+ self._outbox[message.id] = {message: message, clientId: clientId};
38
+ }
32
39
  callback(message);
33
40
  });
34
41
  }
35
42
  }
36
43
 
37
44
  FayeAuthentication.prototype.outgoing = function(message, callback) {
38
- if (message.channel === '/meta/subscribe') {
45
+ if (message.channel == '/meta/subscribe') {
39
46
  this.signMessage(message, callback);
40
47
  }
41
- else if (/^\/meta\/(.*)/.exec(message.channel) === null) { // Publish
48
+ else if (!/^\/meta\/(.*)/.test(message.channel)) { // Publish
42
49
  this.signMessage(message, callback);
43
50
  }
44
51
  else
@@ -46,7 +53,15 @@ FayeAuthentication.prototype.outgoing = function(message, callback) {
46
53
  };
47
54
 
48
55
  FayeAuthentication.prototype.incoming = function(message, callback) {
49
- if (message.error === 'Invalid signature')
50
- this._signatures = {};
51
- callback(message);
56
+ var outbox_message = this._outbox[message.id];
57
+ if (outbox_message && message.error) {
58
+ var channel = outbox_message.message.subscription || outbox_message.message.channel;
59
+ this._signatures[outbox_message.clientId][channel] = null;
60
+ outbox_message.message.retried = true;
61
+ delete outbox_message.message.id;
62
+ delete this._outbox[message.id];
63
+ this._client._send(outbox_message.message, callback);
64
+ }
65
+ else
66
+ callback(message);
52
67
  };
@@ -18,9 +18,11 @@ Gem::Specification.new do |spec|
18
18
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
19
  spec.require_paths = ["lib"]
20
20
 
21
+ spec.add_runtime_dependency 'jwt', '~> 1'
22
+
21
23
  spec.add_development_dependency "bundler", "~> 1.5"
22
24
  spec.add_development_dependency "rake", '~> 10.3'
23
- spec.add_development_dependency 'rspec', '~> 3.0.0.rc1'
25
+ spec.add_development_dependency 'rspec', '~> 3.0'
24
26
  spec.add_development_dependency 'jasmine', '~> 2.0'
25
27
  spec.add_development_dependency 'faye', '~> 1.0'
26
28
  spec.add_development_dependency 'rack', '~> 1.5'
@@ -1,24 +1,33 @@
1
+ require 'faye'
2
+
1
3
  module Faye
2
4
  module Authentication
3
5
  class Extension
6
+ include Faye::Logging
4
7
 
5
8
  def initialize(secret)
6
- @secret = secret
9
+ @secret = secret.to_s
7
10
  end
8
11
 
9
12
  def incoming(message, callback)
10
13
  if message['channel'] == '/meta/subscribe' || !(message['channel'] =~ /^\/meta\/.*/)
11
- unless Faye::Authentication.valid?({
12
- 'channel' => message['subscription'] || message['channel'],
13
- 'clientId' => message['clientId'],
14
- 'signature' => message['signature']
15
- }, @secret)
16
- message['error'] = 'Invalid signature'
14
+ begin
15
+ Faye::Authentication.validate(message['signature'],
16
+ message['subscription'] || message['channel'],
17
+ message['clientId'],
18
+ @secret)
19
+ debug("Authentication sucessful")
20
+ rescue AuthError => exception
21
+ message['error'] = case exception
22
+ when ExpiredError then 'Expired signature'
23
+ when PayloadError then 'Required argument not signed'
24
+ else 'Invalid signature'
25
+ end
26
+ debug("Authentication failed: #{message['error']}")
17
27
  end
18
28
  end
19
29
  callback.call(message)
20
30
  end
21
-
22
31
  end
23
32
  end
24
33
  end
@@ -7,8 +7,9 @@ module Faye
7
7
  def self.publish(url, channel, data, key)
8
8
  uri = URI(url)
9
9
  req = Net::HTTP::Post.new(url)
10
- message = {'channel' => channel, 'data' => data, 'clientId' => 'http'}
10
+ message = {'channel' => channel, 'clientId' => 'http'}
11
11
  message['signature'] = Faye::Authentication.sign(message, key)
12
+ message['data'] = data
12
13
  req.set_form_data(message: JSON.dump(message))
13
14
  Net::HTTP.start(uri.hostname, uri.port, :use_ssl => uri.scheme == 'https') { |http| http.request(req) }
14
15
  end
@@ -1,5 +1,5 @@
1
1
  module Faye
2
2
  module Authentication
3
- VERSION = "0.1.0"
3
+ VERSION = "0.2.0"
4
4
  end
5
5
  end
@@ -1,32 +1,34 @@
1
- require "faye/authentication/version"
1
+ require 'jwt'
2
+ require 'faye/authentication/version'
2
3
  require 'faye/authentication/extension'
3
4
  require 'faye/authentication/http_client'
4
5
  require 'faye/authentication/engine'
5
6
 
6
7
  module Faye
7
8
  module Authentication
9
+ class AuthError < StandardError; end
10
+ class ExpiredError < AuthError; end
11
+ class PayloadError < AuthError; end
8
12
 
9
- def self.sign(message, secret)
10
- OpenSSL::HMAC.hexdigest('sha1', secret, "#{message['channel']}-#{message['clientId']}")
13
+ # Return jwt signature, pass hash of payload including channel and client_id
14
+ def self.sign(payload, secret, options = {})
15
+ options = {expires_at: Time.now + 12*3600, algorithm: 'HS256'}.merge(options)
16
+ JWT.encode(payload.merge(exp: options[:expires_at].to_i), secret, options[:algorithm])
11
17
  end
12
18
 
13
- def self.valid?(message, secret)
14
- signature = message.delete('signature')
15
- return false unless signature
16
- secure_compare(signature, sign(message, secret))
19
+ # Return signed payload or raise
20
+ def self.decode(signature, secret)
21
+ payload, _ = JWT.decode(signature, secret) rescue raise(AuthError)
22
+ raise ExpiredError if Time.at(payload['exp'].to_i) < Time.now
23
+ payload
17
24
  end
18
25
 
19
- # constant-time comparison algorithm to prevent timing attacks
20
- # Copied from ActiveSupport::MessageVerifier
21
- def self.secure_compare(a, b)
22
- return false unless a.bytesize == b.bytesize
23
-
24
- l = a.unpack "C#{a.bytesize}"
25
-
26
- res = 0
27
- b.each_byte { |byte| res |= byte ^ l.shift }
28
- res == 0
26
+ # Return true if signature is valid and correspond to channel and clientId or raise
27
+ def self.validate(signature, channel, clientId, secret)
28
+ payload = self.decode(signature, secret)
29
+ raise PayloadError if channel.to_s.empty? || clientId.to_s.empty?
30
+ raise PayloadError unless channel == payload['channel'] && clientId == payload['clientId']
31
+ true
29
32
  end
30
-
31
33
  end
32
34
  end
@@ -3,12 +3,12 @@ describe('faye-authentication', function() {
3
3
  describe('constructor', function() {
4
4
 
5
5
  it('sets endpoint to /faye by default', function() {
6
- var auth = new FayeAuthentication();
6
+ var auth = new FayeAuthentication(new Faye.Client('http://example.com'));
7
7
  expect(auth.endpoint()).toBe('/faye/auth');
8
8
  });
9
9
 
10
10
  it('can specify a custom endpoint', function() {
11
- var auth = new FayeAuthentication('/custom');
11
+ var auth = new FayeAuthentication(new Faye.Client('http://example.com'), '/custom');
12
12
  expect(auth.endpoint()).toBe('/custom');
13
13
  });
14
14
 
@@ -41,7 +41,6 @@ describe('faye-authentication', function() {
41
41
  var self = this;
42
42
  setTimeout(function() {
43
43
  var request = jasmine.Ajax.requests.mostRecent();
44
- console.log(request);
45
44
  expect(request.data()['message[channel]'][0]).toBe('/foobar');
46
45
  done();
47
46
  }, 500);
@@ -24,14 +24,15 @@ describe('Faye extension', function() {
24
24
 
25
25
  describe('With extension', function() {
26
26
  beforeEach(function() {
27
- this.extension = new FayeAuthentication();
27
+ this.extension = new FayeAuthentication(this.client);
28
28
  this.client.addExtension(this.extension);
29
29
  });
30
30
 
31
31
  function stubSignature(context, callback) {
32
32
  var self = context;
33
33
  self.client.handshake(function() {
34
- var signature = CryptoJS.HmacSHA1("/foobar-" + self.client._clientId, "macaroni").toString();
34
+ var jwtsign = new jwt.WebToken('{"clientId": "' + self.client._clientId + '", "channel": "/foobar", "exp": 2803694528}', '{"alg": "HS256"}');
35
+ var signature = jwtsign.serialize("macaroni");
35
36
 
36
37
  jasmine.Ajax.stubRequest('/faye/auth').andReturn({
37
38
  'responseText': '{"signature": "' + signature + '"}'
@@ -58,17 +59,46 @@ describe('Faye extension', function() {
58
59
  });
59
60
  });
60
61
 
61
- it('clears the signatures when receiving an error from the server', function(done) {
62
- this.extension._signatures = {'123': []};
62
+ it('tries to get a new signature immediately when the used signature is bad or expired', function(done) {
63
63
  jasmine.Ajax.stubRequest('/faye/auth').andReturn({
64
64
  'responseText': '{"signature": "bad"}'
65
65
  });
66
66
  var self = this;
67
67
  this.client.subscribe('/toto').then(undefined, function() {
68
- expect(Object.keys(self.extension._signatures).length).toBe(0);
68
+ expect(jasmine.Ajax.requests.count()).toBe(3); // Handshake + auth * 2
69
69
  done();
70
70
  });
71
+ });
72
+
73
+ it('calls the success callback for a successfully retried message', function(done) {
74
+
75
+ this.client.subscribe('/foo').then(function() {
76
+ expect(jasmine.Ajax.requests.count()).toBe(3); // Handshake + auth * 2
77
+ done();
78
+ }, function(e) { console.log(e)});
79
+
80
+ setTimeout(function() {
81
+ var request = jasmine.Ajax.requests.mostRecent();
82
+ var params = queryString.parse(request.params);
83
+
84
+ var jwtsign_bad = new jwt.WebToken('{"clientId": "' + params['message[clientId]'] + '", "channel": "/foo", "exp": 1}', '{"alg": "HS256"}');
85
+ var signature_bad = jwtsign_bad.serialize("macaroni");
86
+
87
+ var jwtsign_good = new jwt.WebToken('{"clientId": "' + params['message[clientId]'] + '", "channel": "/foo", "exp": 2803694528}', '{"alg": "HS256"}');
88
+ var signature_good = jwtsign_good.serialize("macaroni");
89
+
90
+ request.response({
91
+ 'status' : 200,
92
+ 'responseText': '{"signature": "' + signature_bad + '"}'
93
+ });
94
+
95
+ jasmine.Ajax.stubRequest('/faye/auth').andReturn({
96
+ 'responseText': '{"signature": "' + signature_good + '"}'
97
+ });
98
+
99
+ }, 1000);
71
100
 
72
101
  });
102
+
73
103
  });
74
104
  });
@@ -19,6 +19,8 @@ FAYE_SECRET_KEY = 'macaroni'
19
19
  fork do
20
20
  Faye::WebSocket.load_adapter('thin')
21
21
  faye = Faye::RackAdapter.new(:mount => '/faye')
22
+ #require 'logger'
23
+ #Faye.logger = Logger.new(STDOUT)
22
24
  faye.add_extension Faye::Authentication::Extension.new(FAYE_SECRET_KEY)
23
25
  Rack::Handler::Thin.run faye, :Port => 9296
24
26
  end.tap do |id|
@@ -6,8 +6,9 @@ describe Faye::Authentication::HTTPClient do
6
6
  describe '.publish' do
7
7
 
8
8
  it 'should publish a HTTP request with correct params' do
9
- message = {'channel' => '/foo/bar', 'data' => 'hello', 'clientId' => 'http'}
9
+ message = {'channel' => '/foo/bar', 'clientId' => 'http'}
10
10
  message['signature'] = Faye::Authentication.sign(message, 'my private key')
11
+ message['data'] = 'hello'
11
12
  request = stub_request(:post, "http://www.example.com").with(:body => {:message => JSON.dump(message)}).to_return(:status => 200, :body => "", :headers => {})
12
13
  Faye::Authentication::HTTPClient.publish('http://www.example.com', '/foo/bar', "hello", 'my private key')
13
14
  expect(request).to have_been_made
@@ -3,21 +3,55 @@ require 'faye/authentication'
3
3
 
4
4
  describe Faye::Authentication do
5
5
 
6
+ let(:channel) { '/foo/bar' }
7
+ let(:clientId) { '42' }
8
+ let(:message) { {'channel' => channel, 'clientId' => clientId, 'text' => 'whatever'} }
6
9
  let(:secret) { 'helloworld' }
10
+ let(:signature) { Faye::Authentication.sign(message, secret) }
11
+
12
+ describe '#sign' do
13
+ it 'returns with a default expiry'
14
+ end
15
+
16
+ describe '#decode' do
17
+ it 'returns the payload if the message is correctly signed' do
18
+ expect(Faye::Authentication.decode(signature, secret)).to include(message)
19
+ end
20
+
21
+ it 'raises error if the message if keys differ' do
22
+ expect { Faye::Authentication.decode(signature, secret + 'foo') }.to raise_error(Faye::Authentication::AuthError)
23
+ end
24
+
25
+ it 'raises error if the expiry is in the past' do
26
+ signature = Faye::Authentication.sign(message, secret, expires_at: Time.now - 1)
27
+ expect { Faye::Authentication.decode(signature, secret) }.to raise_error(Faye::Authentication::ExpiredError)
28
+ end
29
+
30
+ it 'return the payload if the expiry is in the future' do
31
+ signature = Faye::Authentication.sign(message, secret, expires_at: Time.now + 10)
32
+ expect { Faye::Authentication.decode(signature, secret) }.not_to raise_error
33
+ end
34
+ end
7
35
 
8
- describe '#valid?' do
36
+ describe '#validate' do
9
37
  it 'returns true if the message is correctly signed' do
10
- message = {'channel' => '/foo/bar', 'clientId' => '42', 'text' => 'whatever'}
11
- signature = Faye::Authentication.sign(message, secret)
12
- message['signature'] = signature
13
- expect(Faye::Authentication.valid?(message, secret)).to be(true)
38
+ expect(Faye::Authentication.validate(signature, channel, clientId, secret)).to be(true)
39
+ end
40
+
41
+ it 'raises if the channel differs' do
42
+ expect { Faye::Authentication.validate(signature, channel + '1', clientId, secret) }.to raise_error(Faye::Authentication::PayloadError)
43
+ end
44
+
45
+ it 'raises if the channel is not defined' do
46
+ expect { Faye::Authentication.validate(signature, '', clientId, secret) }.to raise_error(Faye::Authentication::PayloadError)
47
+ end
48
+
49
+ it 'raises if the channel differs' do
50
+ expect { Faye::Authentication.validate(signature, channel, clientId + '1', secret) }.to raise_error(Faye::Authentication::PayloadError)
14
51
  end
15
52
 
16
- it 'returns false if the message if keys differ' do
17
- message = {'channel' => '/foo/bar', 'clientId' => '42', 'text' => 'whatever'}
18
- signature = Faye::Authentication.sign(message, secret)
19
- message['signature'] = signature
20
- expect(Faye::Authentication.valid?(message, secret + 'foo')).to be(false)
53
+ it 'raises if the channel is not defined' do
54
+ expect { Faye::Authentication.validate(signature, channel, nil, secret) }.to raise_error(Faye::Authentication::PayloadError)
21
55
  end
22
56
  end
23
57