faye-authentication 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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