socky-authenticator 0.5.0.beta4

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,2 @@
1
+ Gemfile.lock
2
+ pkg/
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "http://rubygems.org"
2
+
3
+ gemspec
@@ -0,0 +1,39 @@
1
+ # Socky Authentication Module
2
+
3
+ ## Installation
4
+
5
+ gem install socky-authenticator
6
+
7
+ ## Usage
8
+
9
+ First require authenticator:
10
+
11
+ require 'socky/authenticator'
12
+
13
+ After that call:
14
+
15
+ Socky::Authenticator.authenticate(<data>)
16
+
17
+ where \<data\> is Socky Client authentication data in Hash or JSON-encoded Hash format.
18
+
19
+ In return you will receive authentication Hash in format:
20
+
21
+ { 'auth' => <auth_data> }
22
+
23
+ If any error occurs then authenticator will raise ArgumentError with explanation.
24
+
25
+ If you are validating presence channel then except auth data you will receive user data in JSON-encoded format:
26
+
27
+ { 'auth' => <auth_dat>, 'data' => <json-encoded_user_data> }
28
+
29
+ ## Configuration
30
+
31
+ Before authenticating request you will need to provide application secret. If you are using only one Socky application in code then you can set it once using:
32
+
33
+ Socky.secret = <secret>
34
+
35
+ Otherwise you will need to provide secret each time when authenticating data.
36
+
37
+ Except of that you can enable or disable authenticaton of user rights - if disabled(default) then user will not be able to change their rights. Full version of authenticator call will look like that:
38
+
39
+ Socky::Authenticator.authenticate(<data>, allow_changing_rights, secret)
@@ -0,0 +1,9 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
3
+
4
+ require 'rspec/core/rake_task'
5
+
6
+ task :default => :spec
7
+
8
+ RSpec::Core::RakeTask.new(:spec) do |t|
9
+ end
@@ -0,0 +1,18 @@
1
+ require 'rack'
2
+ require 'json'
3
+ require File.expand_path(File.dirname(__FILE__)) + '/../lib/socky/authenticator'
4
+
5
+ Socky::Authenticator.secret = 'my_secret'
6
+
7
+ authenticator = proc do |env|
8
+ request = Rack::Request.new(env)
9
+
10
+ response = Socky::Authenticator.authenticate(request.params, true)
11
+ body = request.params['callback'].to_s + '(' + response.to_json + ');'
12
+ [ 200, {}, body ]
13
+ end
14
+
15
+
16
+ map '/socky/auth' do
17
+ run authenticator
18
+ end
@@ -0,0 +1,94 @@
1
+ require 'json'
2
+ require 'digest/md5'
3
+ require 'hmac-sha2'
4
+
5
+ module Socky
6
+ class Authenticator
7
+ VERSION = '0.5.0.beta4'
8
+
9
+ DEFAULT_RIGHTS = {
10
+ 'read' => true,
11
+ 'write' => false,
12
+ 'hide' => false
13
+ }
14
+
15
+ class << self
16
+ attr_accessor :secret
17
+
18
+ def authenticate(args = {}, allow_changing_rights = false, secret = nil)
19
+ self.new(args, allow_changing_rights, secret).result
20
+ end
21
+ end
22
+
23
+ attr_accessor :secret, :salt
24
+
25
+ def initialize(args = {}, allow_changing_rights = false, secret = nil)
26
+ @args = (args.is_a?(String) ? JSON.parse(args) : args) rescue nil
27
+ raise ArgumentError, 'Expected hash or JSON' unless @args.kind_of?(Hash)
28
+ @secret = secret || self.class.secret
29
+ @allow_changing_rights = allow_changing_rights
30
+ end
31
+
32
+ def result
33
+ raise ArgumentError, 'set Authenticator.secret first' unless self.secret
34
+ raise ArgumentError, 'expected connection_id' unless self.connection_id
35
+ raise ArgumentError, 'expected channel' unless self.channel_name
36
+ raise ArgumentError, 'user are not allowed to change channel rights' unless self.rights
37
+
38
+ r = { 'auth' => auth }
39
+ r.merge!('data' => user_data) unless user_data.nil?
40
+ r
41
+ end
42
+
43
+ def auth
44
+ [salt, signature].join(':')
45
+ end
46
+
47
+ def signature
48
+ HMAC::SHA256.hexdigest(self.secret, string_to_sign)
49
+ end
50
+
51
+ def string_to_sign
52
+ args = [salt, connection_id, channel_name, rights]
53
+ args << user_data unless user_data.nil?
54
+ args.collect(&:to_s).join(":")
55
+ end
56
+
57
+ def salt
58
+ @salt ||= Digest::MD5.hexdigest(rand.to_s)
59
+ end
60
+
61
+ def connection_id
62
+ @args['connection_id']
63
+ end
64
+
65
+ def channel_name
66
+ @args['channel']
67
+ end
68
+
69
+ def rights
70
+ return @rights if defined?(@rights)
71
+ r = DEFAULT_RIGHTS.merge(@args)
72
+
73
+ # Return nil if user is trying to change rights when this option is disabled
74
+ return nil if !@allow_changing_rights && DEFAULT_RIGHTS.any?{ |right,val| r[right] != val }
75
+
76
+ @rights = ['read', 'write', 'hide'].collect do |right|
77
+ r[right] && !(right == 'hide' && !self.presence?) ? '1' : '0'
78
+ end.join
79
+ end
80
+
81
+ def user_data
82
+ @user_data ||= case @args['data']
83
+ when NilClass then nil
84
+ when String then @args['data']
85
+ else @args['data'].to_json
86
+ end
87
+ end
88
+
89
+ def presence?
90
+ self.channel_name.is_a?(String) && !!self.channel_name.match(/\Apresence-/)
91
+ end
92
+
93
+ end
94
+ end
@@ -0,0 +1,23 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "socky/authenticator"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "socky-authenticator"
7
+ s.version = Socky::Authenticator::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Bernard Potocki"]
10
+ s.email = ["bernard.potocki@imanel.org"]
11
+ s.homepage = "http://socky.org"
12
+ s.summary = %q{Socky - Authentication Module}
13
+ s.description = %q{Socky is a WebSocket-based framework for realtime web applications.}
14
+
15
+ s.add_dependency 'json'
16
+ s.add_dependency 'ruby-hmac'
17
+ s.add_development_dependency 'rspec', '~> 2.0'
18
+
19
+ s.files = `git ls-files`.split("\n")
20
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
21
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
22
+ s.require_paths = ["lib"]
23
+ end
@@ -0,0 +1,120 @@
1
+ require 'spec_helper'
2
+
3
+ describe Socky::Authenticator do
4
+
5
+ # Set authenticator secret
6
+ before { Socky::Authenticator.secret = 'application_secret_key' }
7
+
8
+ it "should raise exception on invalid data" do
9
+ lambda { Socky::Authenticator.new("invalid") }.should raise_error ArgumentError, "Expected hash or JSON"
10
+ end
11
+
12
+ it "should allow passing Hash" do
13
+ subject = Socky::Authenticator.new('some' => 'data')
14
+ subject.instance_variable_get('@args').should eql('some' => 'data')
15
+ end
16
+
17
+ it "should allow passing JSON-encoded Hash" do
18
+ subject = Socky::Authenticator.new('{"some":"data"}')
19
+ subject.instance_variable_get('@args').should eql('some' => 'data')
20
+ end
21
+
22
+ it "should raise on JSON-encoded non-Hash" do
23
+ lambda { Socky::Authenticator.new('["some","data"]') }.should raise_error ArgumentError, "Expected hash or JSON"
24
+ end
25
+
26
+ context "instance" do
27
+ subject { Socky::Authenticator.new('connection_id' => '1234ABCD', 'channel' => 'some_channel') }
28
+ # Set salt to constant to make tests non-random
29
+ before { subject.salt = 'somerandomstring' }
30
+
31
+ its(:salt) { should eql('somerandomstring') }
32
+ its(:connection_id) { should eql('1234ABCD') }
33
+ its(:channel_name) { should eql('some_channel') }
34
+ its(:rights) { should eql('100') }
35
+ its(:presence?) { should eql(false) }
36
+ its(:string_to_sign) { should eql('somerandomstring:1234ABCD:some_channel:100') }
37
+ its(:signature) { should eql('28f138d68b1d4971d85355a5aa5a301be9084176b6ae1bbe2399de990de2039d') }
38
+ its(:auth) { should eql('somerandomstring:28f138d68b1d4971d85355a5aa5a301be9084176b6ae1bbe2399de990de2039d') }
39
+ its(:result) { should eql('auth' => 'somerandomstring:28f138d68b1d4971d85355a5aa5a301be9084176b6ae1bbe2399de990de2039d') }
40
+
41
+ it "should raise if authenticator secret is nil" do
42
+ subject.secret = nil
43
+ lambda { subject.result }.should raise_error ArgumentError, 'set Authenticator.secret first'
44
+ end
45
+
46
+ it "should raise if connection_id is nil" do
47
+ subject.instance_variable_get('@args').delete('connection_id')
48
+ subject.connection_id.should be_nil
49
+ lambda { subject.result }.should raise_error ArgumentError, 'expected connection_id'
50
+ end
51
+
52
+ it "should raise if channel is nil" do
53
+ subject.instance_variable_get('@args').delete('channel')
54
+ subject.channel_name.should be_nil
55
+ lambda { subject.result }.should raise_error ArgumentError, 'expected channel'
56
+ end
57
+
58
+ it "should not allow to changing rights at default" do
59
+ subject.instance_variable_get('@args').merge!('write' => true)
60
+ subject.rights.should be_nil
61
+ lambda { subject.result }.should raise_error ArgumentError, 'user are not allowed to change channel rights'
62
+ end
63
+
64
+ context "with changing rights enables" do
65
+ before { subject.instance_variable_set('@allow_changing_rights', true) }
66
+
67
+ it "should allow changing 'read' to false" do
68
+ subject.instance_variable_get('@args').merge!('read' => false)
69
+ subject.rights.should eql('000')
70
+ end
71
+
72
+ it "should allow changing 'write' to true" do
73
+ subject.instance_variable_get('@args').merge!('write' => true)
74
+ subject.rights.should eql('110')
75
+ end
76
+
77
+ it "should not allow changing 'hide' to true" do
78
+ subject.instance_variable_get('@args').merge!('hide' => true)
79
+ subject.rights.should eql('100')
80
+ end
81
+
82
+ end
83
+
84
+ context "presence channel" do
85
+ before { subject.instance_variable_get('@args').merge!('channel' => 'presence-channel') }
86
+
87
+ its(:channel_name) { should eql('presence-channel') }
88
+ its(:rights) { should eql('100') }
89
+ its(:presence?) { should eql(true) }
90
+ its(:user_data) { should eql(nil) }
91
+ its(:string_to_sign) { should eql('somerandomstring:1234ABCD:presence-channel:100') }
92
+ its(:signature) { should eql('f0332936d0c3e59e2d9840d0c0b538ad88fba467ba546d8f9f91bc8d3cd95a1c') }
93
+ its(:auth) { should eql('somerandomstring:f0332936d0c3e59e2d9840d0c0b538ad88fba467ba546d8f9f91bc8d3cd95a1c') }
94
+ its(:result) { should eql('auth' => 'somerandomstring:f0332936d0c3e59e2d9840d0c0b538ad88fba467ba546d8f9f91bc8d3cd95a1c') }
95
+
96
+ context "with hash user data provided" do
97
+ before { subject.instance_variable_get('@args').merge!('data' => { 'some' => 'data' }) }
98
+
99
+ its(:user_data) { should eql('{"some":"data"}') }
100
+ its(:string_to_sign) { should eql('somerandomstring:1234ABCD:presence-channel:100:{"some":"data"}') }
101
+ its(:signature) { should eql('71dabae0f47da5ac8e4982fa062abf09788f8fab40b7634427e380bfcec29855') }
102
+ its(:auth) { should eql('somerandomstring:71dabae0f47da5ac8e4982fa062abf09788f8fab40b7634427e380bfcec29855') }
103
+ its(:result) { should eql('auth' => 'somerandomstring:71dabae0f47da5ac8e4982fa062abf09788f8fab40b7634427e380bfcec29855', 'data' => '{"some":"data"}') }
104
+ end
105
+
106
+ context "with string user data provided" do
107
+ before { subject.instance_variable_get('@args').merge!('data' => '{"some":"data"}') }
108
+
109
+ its(:user_data) { should eql('{"some":"data"}') }
110
+ its(:string_to_sign) { should eql('somerandomstring:1234ABCD:presence-channel:100:{"some":"data"}') }
111
+ its(:signature) { should eql('71dabae0f47da5ac8e4982fa062abf09788f8fab40b7634427e380bfcec29855') }
112
+ its(:auth) { should eql('somerandomstring:71dabae0f47da5ac8e4982fa062abf09788f8fab40b7634427e380bfcec29855') }
113
+ its(:result) { should eql('auth' => 'somerandomstring:71dabae0f47da5ac8e4982fa062abf09788f8fab40b7634427e380bfcec29855', 'data' => '{"some":"data"}') }
114
+ end
115
+
116
+ end
117
+
118
+ end
119
+
120
+ end
@@ -0,0 +1,4 @@
1
+ require 'rubygems'
2
+ require 'rspec'
3
+
4
+ require 'socky/authenticator'
metadata ADDED
@@ -0,0 +1,98 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: socky-authenticator
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: 6
5
+ version: 0.5.0.beta4
6
+ platform: ruby
7
+ authors:
8
+ - Bernard Potocki
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2011-04-16 00:00:00 +02:00
14
+ default_executable:
15
+ dependencies:
16
+ - !ruby/object:Gem::Dependency
17
+ name: json
18
+ prerelease: false
19
+ requirement: &id001 !ruby/object:Gem::Requirement
20
+ none: false
21
+ requirements:
22
+ - - ">="
23
+ - !ruby/object:Gem::Version
24
+ version: "0"
25
+ type: :runtime
26
+ version_requirements: *id001
27
+ - !ruby/object:Gem::Dependency
28
+ name: ruby-hmac
29
+ prerelease: false
30
+ requirement: &id002 !ruby/object:Gem::Requirement
31
+ none: false
32
+ requirements:
33
+ - - ">="
34
+ - !ruby/object:Gem::Version
35
+ version: "0"
36
+ type: :runtime
37
+ version_requirements: *id002
38
+ - !ruby/object:Gem::Dependency
39
+ name: rspec
40
+ prerelease: false
41
+ requirement: &id003 !ruby/object:Gem::Requirement
42
+ none: false
43
+ requirements:
44
+ - - ~>
45
+ - !ruby/object:Gem::Version
46
+ version: "2.0"
47
+ type: :development
48
+ version_requirements: *id003
49
+ description: Socky is a WebSocket-based framework for realtime web applications.
50
+ email:
51
+ - bernard.potocki@imanel.org
52
+ executables: []
53
+
54
+ extensions: []
55
+
56
+ extra_rdoc_files: []
57
+
58
+ files:
59
+ - .gitignore
60
+ - Gemfile
61
+ - README.md
62
+ - Rakefile
63
+ - example/config.ru
64
+ - lib/socky/authenticator.rb
65
+ - socky-authenticator.gemspec
66
+ - spec/authenticator_spec.rb
67
+ - spec/spec_helper.rb
68
+ has_rdoc: true
69
+ homepage: http://socky.org
70
+ licenses: []
71
+
72
+ post_install_message:
73
+ rdoc_options: []
74
+
75
+ require_paths:
76
+ - lib
77
+ required_ruby_version: !ruby/object:Gem::Requirement
78
+ none: false
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: "0"
83
+ required_rubygems_version: !ruby/object:Gem::Requirement
84
+ none: false
85
+ requirements:
86
+ - - ">"
87
+ - !ruby/object:Gem::Version
88
+ version: 1.3.1
89
+ requirements: []
90
+
91
+ rubyforge_project:
92
+ rubygems_version: 1.6.1
93
+ signing_key:
94
+ specification_version: 3
95
+ summary: Socky - Authentication Module
96
+ test_files:
97
+ - spec/authenticator_spec.rb
98
+ - spec/spec_helper.rb