faye-authentication 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: cfd708e9dbb1900470d238f7d427ba3a6bd4322c
4
+ data.tar.gz: 5d087943cf31d1ffe01f645039d8c9c034fdeacd
5
+ SHA512:
6
+ metadata.gz: 7fa639d27c2ce7ecd9876355b241cb3db5c490f8082db95c156410f16bc3ee5790bc88020a2d4055c2a4f341569e3696299794bb52e672514d16390c2f0a5e93
7
+ data.tar.gz: 4f2d0c80f71e1defc714a0a76da7472390dfb19b300b469c3d71d1f38189b418364912fa3988da35048598db584f55ea0bc6bc6891e464a9f632567e4229e85b
data/.gitignore ADDED
@@ -0,0 +1,23 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ *.bundle
19
+ *.so
20
+ *.o
21
+ *.a
22
+ mkmf.log
23
+ .DS_Store
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --color
2
+ --warnings
3
+ --require spec_helper
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.1.2
4
+ - 1.9.3
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in faye-authentication.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Adrien Siami
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,106 @@
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)
2
+
3
+ Authentification implementation for faye
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.**
10
+
11
+ The authentication is performed through an Ajax Call to the webserver (JQuery needed).
12
+
13
+ For each channel and client id pair, a signature is added to the message.
14
+
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.
17
+
18
+ ## Installation
19
+
20
+ Add this line to your application's Gemfile:
21
+
22
+ gem 'faye-authentication'
23
+
24
+ And then execute:
25
+
26
+ $ bundle
27
+
28
+ Or install it yourself as:
29
+
30
+ $ gem install faye-authentication
31
+
32
+ ## Usage
33
+
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 :
47
+
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.
54
+
55
+ The parameters sent to the endpoint are the following :
56
+
57
+ ````
58
+ {
59
+ 'message' =>
60
+ {
61
+ 'channel' => '/foo/bar',
62
+ 'clientId' => '123abc'
63
+ }
64
+ }
65
+ ````
66
+
67
+ If the endpoint returns an error, the message won't be signed and the server will reject it.
68
+
69
+ You can use ``Faye::Authentication.sign`` to generate the signature from the message and a private key.
70
+
71
+ Example (For a Rails application)
72
+
73
+ ````ruby
74
+ def auth
75
+ if current_user.can?(:read, params[:message][:channel])
76
+ render json: {signature: Faye::Authentication.sign(params[:message], 'your private key')}
77
+ else
78
+ render json: {error: 'Not authorized'}, status: 403
79
+ end
80
+ end
81
+
82
+ ````
83
+
84
+ A Ruby HTTP Client is also available for publishing messages to your faye server
85
+ without the hassle of using EventMachine :
86
+
87
+ ````ruby
88
+ Faye::Authentication::HTTPClient.publish('/channel', 'data', 'your private key')
89
+ ````
90
+
91
+ ### Faye server extension
92
+
93
+ Instanciate the extension with your secret key and add it to the server :
94
+
95
+ ````ruby
96
+ server = Faye::RackAdapter.new(:mount => '/faye', :timeout => 15)
97
+ server.add_extension Faye::Authentication::Extension.new('your private key')
98
+ ````
99
+
100
+ ## Contributing
101
+
102
+ 1. Fork it ( https://github.com/dimelo/faye-authentication/fork )
103
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
104
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
105
+ 4. Push to the branch (`git push origin my-new-feature`)
106
+ 5. Create a new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,9 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ require 'jasmine'
4
+ require 'rspec/core/rake_task'
5
+ load 'jasmine/tasks/jasmine.rake'
6
+
7
+ RSpec::Core::RakeTask.new(:spec)
8
+
9
+ task :default => [:spec, 'jasmine:ci']
@@ -0,0 +1,52 @@
1
+ function FayeAuthentication(endpoint) {
2
+ this._endpoint = endpoint || '/faye/auth';
3
+ this._signatures = {};
4
+ }
5
+
6
+ FayeAuthentication.prototype.endpoint = function() {
7
+ return (this._endpoint);
8
+ };
9
+
10
+ FayeAuthentication.prototype.signMessage = function(message, callback) {
11
+ var channel = message.subscription || message.channel;
12
+ var clientId = message.clientId;
13
+
14
+ if (!this._signatures[clientId])
15
+ this._signatures[clientId] = {};
16
+ if (this._signatures[clientId][channel]) {
17
+ this._signatures[clientId][channel].then(function(signature) {
18
+ message.signature = signature;
19
+ callback(message);
20
+ });
21
+ } else {
22
+ var self = this;
23
+ self._signatures[clientId][channel] = new Faye.Promise(function(success, failure) {
24
+ $.post(self.endpoint(), {message: {channel: channel, clientId: clientId}}, function(response) {
25
+ success(response.signature);
26
+ }, 'json').fail(function(xhr, textStatus, e) {
27
+ success(null);
28
+ });
29
+ });
30
+ self._signatures[clientId][channel].then(function(signature) {
31
+ message.signature = signature;
32
+ callback(message);
33
+ });
34
+ }
35
+ }
36
+
37
+ FayeAuthentication.prototype.outgoing = function(message, callback) {
38
+ if (message.channel === '/meta/subscribe') {
39
+ this.signMessage(message, callback);
40
+ }
41
+ else if (/^\/meta\/(.*)/.exec(message.channel) === null) { // Publish
42
+ this.signMessage(message, callback);
43
+ }
44
+ else
45
+ callback(message);
46
+ };
47
+
48
+ FayeAuthentication.prototype.incoming = function(message, callback) {
49
+ if (message.error === 'Invalid signature')
50
+ this._signatures = {};
51
+ callback(message);
52
+ };
@@ -0,0 +1,29 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'faye/authentication/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "faye-authentication"
8
+ spec.version = Faye::Authentication::VERSION
9
+ spec.authors = ["Adrien Siami"]
10
+ spec.email = ["adrien.siami@dimelo.com"]
11
+ spec.summary =
12
+ spec.description = "A faye extension to add authentication mechanisms"
13
+ spec.homepage = "https://github.com/dimelo/faye-authentication"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.5"
22
+ spec.add_development_dependency "rake", '~> 10.3'
23
+ spec.add_development_dependency 'rspec', '~> 3.0.0.rc1'
24
+ spec.add_development_dependency 'jasmine', '~> 2.0'
25
+ spec.add_development_dependency 'faye', '~> 1.0'
26
+ spec.add_development_dependency 'rack', '~> 1.5'
27
+ spec.add_development_dependency 'thin', '~> 1.6'
28
+ spec.add_development_dependency 'webmock', '~> 1.18'
29
+ end
@@ -0,0 +1,8 @@
1
+ if defined?(Rails)
2
+ module Faye
3
+ module Authentication
4
+ class Engine < Rails::Engine
5
+ end
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,24 @@
1
+ module Faye
2
+ module Authentication
3
+ class Extension
4
+
5
+ def initialize(secret)
6
+ @secret = secret
7
+ end
8
+
9
+ def incoming(message, callback)
10
+ 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'
17
+ end
18
+ end
19
+ callback.call(message)
20
+ end
21
+
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,18 @@
1
+ require 'json'
2
+
3
+ module Faye
4
+ module Authentication
5
+ class HTTPClient
6
+
7
+ def self.publish(url, channel, data, key)
8
+ uri = URI(url)
9
+ req = Net::HTTP::Post.new(url)
10
+ message = {'channel' => channel, 'data' => data, 'clientId' => 'http'}
11
+ message['signature'] = Faye::Authentication.sign(message, key)
12
+ req.set_form_data(message: JSON.dump(message))
13
+ Net::HTTP.start(uri.hostname, uri.port, :use_ssl => uri.scheme == 'https') { |http| http.request(req) }
14
+ end
15
+
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,5 @@
1
+ module Faye
2
+ module Authentication
3
+ VERSION = "0.1.0"
4
+ end
5
+ end
@@ -0,0 +1,32 @@
1
+ require "faye/authentication/version"
2
+ require 'faye/authentication/extension'
3
+ require 'faye/authentication/http_client'
4
+ require 'faye/authentication/engine'
5
+
6
+ module Faye
7
+ module Authentication
8
+
9
+ def self.sign(message, secret)
10
+ OpenSSL::HMAC.hexdigest('sha1', secret, "#{message['channel']}-#{message['clientId']}")
11
+ end
12
+
13
+ def self.valid?(message, secret)
14
+ signature = message.delete('signature')
15
+ return false unless signature
16
+ secure_compare(signature, sign(message, secret))
17
+ end
18
+
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
29
+ end
30
+
31
+ end
32
+ end
@@ -0,0 +1,153 @@
1
+ describe('faye-authentication', function() {
2
+
3
+ describe('constructor', function() {
4
+
5
+ it('sets endpoint to /faye by default', function() {
6
+ var auth = new FayeAuthentication();
7
+ expect(auth.endpoint()).toBe('/faye/auth');
8
+ });
9
+
10
+ it('can specify a custom endpoint', function() {
11
+ var auth = new FayeAuthentication('/custom');
12
+ expect(auth.endpoint()).toBe('/custom');
13
+ });
14
+
15
+ });
16
+
17
+ describe('extension', function() {
18
+ beforeEach(function() {
19
+ jasmine.Ajax.install();
20
+ this.auth = new FayeAuthentication();
21
+ this.client = new Faye.Client('http://localhost:9296/faye');
22
+ this.client.addExtension(this.auth);
23
+ });
24
+
25
+ afterEach(function() {
26
+ jasmine.Ajax.uninstall();
27
+ });
28
+
29
+ it('should make an ajax request to the extension endpoint', function(done) {
30
+ this.client.subscribe('/foobar');
31
+ var self = this;
32
+ setTimeout(function() {
33
+ var request = jasmine.Ajax.requests.mostRecent();
34
+ expect(request.url).toBe(self.auth.endpoint());
35
+ done();
36
+ }, 500);
37
+ });
38
+
39
+ it('should make an ajax request with the correct params', function(done) {
40
+ this.client.subscribe('/foobar');
41
+ var self = this;
42
+ setTimeout(function() {
43
+ var request = jasmine.Ajax.requests.mostRecent();
44
+ console.log(request);
45
+ expect(request.data()['message[channel]'][0]).toBe('/foobar');
46
+ done();
47
+ }, 500);
48
+ });
49
+
50
+
51
+ describe('signature', function() {
52
+
53
+ beforeEach(function() {
54
+ jasmine.Ajax.stubRequest('/faye/auth').andReturn({
55
+ 'responseText': '{"signature": "foobarsignature"}'
56
+ });
57
+
58
+ this.fake_transport = {connectionType: "fake", endpoint: {}, send: function() {}};
59
+ spyOn(this.fake_transport, 'send');
60
+ });
61
+
62
+ it('should add the signature to subscribe message', function(done) {
63
+ var self = this;
64
+
65
+ this.client.handshake(function() {
66
+ self.client._transport = self.fake_transport
67
+ self.client.subscribe('/foobar');
68
+ }, this.client);
69
+
70
+ setTimeout(function() {
71
+ var calls = self.fake_transport.send.calls.all();
72
+ var last_call = calls[calls.length - 1];
73
+ var message = last_call.args[0].message;
74
+ expect(message.channel).toBe('/meta/subscribe');
75
+ expect(message.signature).toBe('foobarsignature');
76
+ done();
77
+ }, 500);
78
+
79
+ });
80
+
81
+ it('should add the signature to publish message', function(done) {
82
+ var self = this;
83
+
84
+ this.client.handshake(function() {
85
+ self.client._transport = self.fake_transport
86
+ self.client.publish('/foobar', {text: 'hallo'});
87
+ }, this.client);
88
+
89
+ setTimeout(function() {
90
+ var calls = self.fake_transport.send.calls.all();
91
+ var last_call = calls[calls.length - 1];
92
+ var message = last_call.args[0].message;
93
+ expect(message.channel).toBe('/foobar');
94
+ expect(message.signature).toBe('foobarsignature');
95
+ done();
96
+ }, 500);
97
+ });
98
+
99
+ it('preserves messages integrity', function(done) {
100
+ var self = this;
101
+
102
+ this.client.handshake(function() {
103
+ self.client._transport = self.fake_transport
104
+ self.client.publish('/foo', {text: 'hallo'});
105
+ self.client.subscribe('/foo');
106
+ }, this.client);
107
+
108
+ setTimeout(function() {
109
+ var calls = self.fake_transport.send.calls.all();
110
+ var subscribe_call = calls[calls.length - 1];
111
+ var publish_call = calls[calls.length - 2];
112
+ var subscribe_message = subscribe_call.args[0].message;
113
+ var publish_message = publish_call.args[0].message;
114
+ expect(publish_message.channel).toBe('/foo');
115
+ expect(subscribe_message.channel).toBe('/meta/subscribe');
116
+ expect(publish_message.signature).toBe('foobarsignature');
117
+ expect(subscribe_message.signature).toBe('foobarsignature');
118
+ done();
119
+ }, 500);
120
+ });
121
+
122
+ it('should make only one ajax call when dealing with one channel', function(done) {
123
+ this.client.subscribe('/foobar');
124
+ this.client.publish('/foobar', {text: 'hallo'});
125
+ this.client.publish('/foobar', {text: 'hallo'});
126
+
127
+ setTimeout(function() {
128
+ expect(jasmine.Ajax.requests.count()).toBe(2); // Handshake + auth
129
+ done();
130
+ }, 500);
131
+
132
+ })
133
+
134
+ it('should make two ajax calls when dealing with two channels', function(done) {
135
+ this.client.subscribe('/foo');
136
+ this.client.publish('/foo', {text: 'hallo'});
137
+ this.client.publish('/foo', {text: 'hallo'});
138
+
139
+ this.client.subscribe('/bar');
140
+ this.client.publish('/bar', {text: 'hallo'});
141
+ this.client.publish('/bar', {text: 'hallo'});
142
+
143
+ setTimeout(function() {
144
+ expect(jasmine.Ajax.requests.count()).toBe(3); // Handshake + auth * 2
145
+ done();
146
+ }, 500);
147
+ });
148
+ });
149
+
150
+ });
151
+
152
+
153
+ })
@@ -0,0 +1,74 @@
1
+ describe('Faye extension', function() {
2
+ beforeEach(function() {
3
+ this.client = new Faye.Client('http://localhost:9296/faye');
4
+ jasmine.Ajax.install();
5
+ });
6
+
7
+ afterEach(function() {
8
+ jasmine.Ajax.uninstall();
9
+ });
10
+
11
+ describe('Without extension', function() {
12
+ it('fails to subscribe', function(done) {
13
+ this.client.subscribe('/foobar').then(undefined, function() {
14
+ done();
15
+ });
16
+ });
17
+
18
+ it('fails to publish', function(done) {
19
+ this.client.publish('/foobar', {text: 'whatever'}).then(undefined, function() {
20
+ done();
21
+ });
22
+ });
23
+ });
24
+
25
+ describe('With extension', function() {
26
+ beforeEach(function() {
27
+ this.extension = new FayeAuthentication();
28
+ this.client.addExtension(this.extension);
29
+ });
30
+
31
+ function stubSignature(context, callback) {
32
+ var self = context;
33
+ self.client.handshake(function() {
34
+ var signature = CryptoJS.HmacSHA1("/foobar-" + self.client._clientId, "macaroni").toString();
35
+
36
+ jasmine.Ajax.stubRequest('/faye/auth').andReturn({
37
+ 'responseText': '{"signature": "' + signature + '"}'
38
+ });
39
+ callback();
40
+ }, self.client);
41
+ }
42
+
43
+ it('succeeds to subscribe', function(done) {
44
+ var self = this;
45
+ stubSignature(this, function() {
46
+ self.client.subscribe('/foobar').then(function() {
47
+ done();
48
+ });
49
+ });
50
+ });
51
+
52
+ it('succeeds to publish', function(done) {
53
+ var self = this;
54
+ stubSignature(this, function() {
55
+ self.client.publish('/foobar', {hello: 'world'}).then(function() {
56
+ done();
57
+ });
58
+ });
59
+ });
60
+
61
+ it('clears the signatures when receiving an error from the server', function(done) {
62
+ this.extension._signatures = {'123': []};
63
+ jasmine.Ajax.stubRequest('/faye/auth').andReturn({
64
+ 'responseText': '{"signature": "bad"}'
65
+ });
66
+ var self = this;
67
+ this.client.subscribe('/toto').then(undefined, function() {
68
+ expect(Object.keys(self.extension._signatures).length).toBe(0);
69
+ done();
70
+ });
71
+
72
+ });
73
+ });
74
+ });
File without changes