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.
- data/.gitignore +2 -0
- data/Gemfile +3 -0
- data/README.md +39 -0
- data/Rakefile +9 -0
- data/example/config.ru +18 -0
- data/lib/socky/authenticator.rb +94 -0
- data/socky-authenticator.gemspec +23 -0
- data/spec/authenticator_spec.rb +120 -0
- data/spec/spec_helper.rb +4 -0
- metadata +98 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -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)
|
data/Rakefile
ADDED
data/example/config.ru
ADDED
@@ -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
|
data/spec/spec_helper.rb
ADDED
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
|