socky-authenticator 0.5.0.beta4

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.
@@ -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