signature 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.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,21 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+
21
+ ## PROJECT::SPECIFIC
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 Martyn Loughran
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,37 @@
1
+ signature
2
+ ---------
3
+
4
+ Examples
5
+ ========
6
+
7
+ Client example
8
+
9
+ params = {:some => 'parameters'}
10
+ token = Signature::Token.new(key, secret)
11
+ request = Signature::Request.new('POST', '/api/thing, params)
12
+ auth_hash = request.sign(token)
13
+
14
+ HTTParty.post('http://myservice/api/thing', {
15
+ :query => params.merge(auth_hash)
16
+ })
17
+
18
+ Server example (sinatra)
19
+
20
+ error Signature::AuthenticationError do |controller|
21
+ error = controller.env["sinatra.error"]
22
+ halt 401, "401 UNAUTHORIZED: #{error.message}\n"
23
+ end
24
+
25
+ post '/api/thing' do
26
+ request = Authentication::Request.new('POST', env["REQUEST_PATH"], params)
27
+ token = request.authenticate do |key|
28
+ Signature::Token.new(key, lookup_secret(key))
29
+ end
30
+
31
+ # Do whatever you need to do
32
+ end
33
+
34
+ Copyright
35
+ =========
36
+
37
+ Copyright (c) 2010 Martyn Loughran. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,46 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "signature"
8
+ gem.summary = %Q{Simple key/secret based authentication for apis}
9
+ gem.description = %Q{Simple key/secret based authentication for apis}
10
+ gem.email = "me@mloughran.com"
11
+ gem.homepage = "http://github.com/mloughran/signature"
12
+ gem.authors = ["Martyn Loughran"]
13
+ gem.add_dependency "ruby-hmac"
14
+ gem.add_development_dependency "rspec", ">= 1.2.9"
15
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
16
+ end
17
+ Jeweler::GemcutterTasks.new
18
+ rescue LoadError
19
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
20
+ end
21
+
22
+ require 'spec/rake/spectask'
23
+ Spec::Rake::SpecTask.new(:spec) do |spec|
24
+ spec.libs << 'lib' << 'spec'
25
+ spec.spec_files = FileList['spec/**/*_spec.rb']
26
+ end
27
+
28
+ Spec::Rake::SpecTask.new(:rcov) do |spec|
29
+ spec.libs << 'lib' << 'spec'
30
+ spec.pattern = 'spec/**/*_spec.rb'
31
+ spec.rcov = true
32
+ end
33
+
34
+ task :spec => :check_dependencies
35
+
36
+ task :default => :spec
37
+
38
+ require 'rake/rdoctask'
39
+ Rake::RDocTask.new do |rdoc|
40
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
41
+
42
+ rdoc.rdoc_dir = 'rdoc'
43
+ rdoc.title = "signature #{version}"
44
+ rdoc.rdoc_files.include('README*')
45
+ rdoc.rdoc_files.include('lib/**/*.rb')
46
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
data/lib/signature.rb ADDED
@@ -0,0 +1,142 @@
1
+ require 'hmac-sha2'
2
+ require 'base64'
3
+
4
+ module Signature
5
+ class AuthenticationError < RuntimeError; end
6
+
7
+ class Token
8
+ attr_reader :key, :secret
9
+
10
+ def initialize(key, secret)
11
+ @key, @secret = key, secret
12
+ end
13
+
14
+ def sign(request)
15
+ request.sign(self)
16
+ end
17
+ end
18
+
19
+ class Request
20
+ attr_accessor :path, :query_hash
21
+
22
+ # http://www.w3.org/TR/NOTE-datetime
23
+ ISO8601 = "%Y-%m-%dT%H:%M:%SZ"
24
+
25
+ def initialize(method, path, query)
26
+ raise ArgumentError, "Expected string" unless path.kind_of?(String)
27
+ raise ArgumentError, "Expected hash" unless query.kind_of?(Hash)
28
+
29
+ query_hash = {}
30
+ auth_hash = {}
31
+ query.each do |key, v|
32
+ k = key.to_s.downcase
33
+ k[0..4] == 'auth_' ? auth_hash[k] = v : query_hash[k] = v
34
+ end
35
+
36
+ @method = method.upcase
37
+ @path, @query_hash, @auth_hash = path, query_hash, auth_hash
38
+ end
39
+
40
+ def sign(token)
41
+ @auth_hash = {
42
+ :auth_version => "1.0",
43
+ :auth_key => token.key,
44
+ :auth_timestamp => Time.now.to_i
45
+ }
46
+
47
+ @auth_hash[:auth_signature] = signature(token)
48
+
49
+ return @auth_hash
50
+ end
51
+
52
+ # Authenticates the request with a token
53
+ #
54
+ # Timestamp check: Unless timestamp_grace is set to nil (which will skip
55
+ # the timestamp check), an exception will be raised if timestamp is not
56
+ # supplied or if the timestamp provided is not within timestamp_grace of
57
+ # the real time (defaults to 10 minutes)
58
+ #
59
+ # Signature check: Raises an exception if the signature does not match the
60
+ # computed value
61
+ #
62
+ def authenticate_by_token!(token, timestamp_grace = 600)
63
+ validate_version!
64
+ validate_timestamp!(timestamp_grace)
65
+ validate_signature!(token)
66
+ true
67
+ end
68
+
69
+ def authenticate_by_token(token, timestamp_grace = 600)
70
+ authenticate_by_token!(token, timestamp_grace)
71
+ rescue AuthenticationError
72
+ false
73
+ end
74
+
75
+ def authenticate(timestamp_grace = 600, &block)
76
+ key = @auth_hash['auth_key']
77
+ raise AuthenticationError, "Authentication key required" unless key
78
+ token = yield key
79
+ unless token && token.secret
80
+ raise AuthenticationError, "Invalid authentication key"
81
+ end
82
+ authenticate_by_token!(token, timestamp_grace)
83
+ return token
84
+ end
85
+
86
+ def auth_hash
87
+ raise "Request not signed" unless @auth_hash && @auth_hash[:auth_signature]
88
+ @auth_hash
89
+ end
90
+
91
+ private
92
+
93
+ def signature(token)
94
+ HMAC::SHA256.hexdigest(token.secret, string_to_sign)
95
+ end
96
+
97
+ def string_to_sign
98
+ [@method, @path, parameter_string].join("\n")
99
+ end
100
+
101
+ def parameter_string
102
+ param_hash = @query_hash.merge(@auth_hash || {})
103
+
104
+ # Convert keys to lowercase strings
105
+ hash = {}; param_hash.each { |k,v| hash[k.to_s.downcase] = v }
106
+
107
+ # Exclude signature from signature generation!
108
+ hash.delete("auth_signature")
109
+
110
+ hash.keys.sort.map { |k| "#{k}=#{hash[k]}" }.join("&")
111
+ end
112
+
113
+ def validate_version!
114
+ version = @auth_hash["auth_version"]
115
+ raise AuthenticationError, "Version required" unless version
116
+ raise AuthenticationError, "Version not supported" unless version == '1.0'
117
+ end
118
+
119
+ def validate_timestamp!(grace)
120
+ return true if grace.nil?
121
+
122
+ timestamp = @auth_hash["auth_timestamp"]
123
+ error = (timestamp.to_i - Time.now.to_i).abs
124
+ raise AuthenticationError, "Timestamp required" unless timestamp
125
+ if error >= grace
126
+ raise AuthenticationError, "Timestamp expired: Given timestamp "\
127
+ "(#{Time.at(timestamp.to_i).utc.strftime(ISO8601)}) "\
128
+ "not within #{grace}s of server time "\
129
+ "(#{Time.now.utc.strftime(ISO8601)})"
130
+ end
131
+ return true
132
+ end
133
+
134
+ def validate_signature!(token)
135
+ unless @auth_hash["auth_signature"] == signature(token)
136
+ raise AuthenticationError, "Invalid signature: you should have "\
137
+ "sent HmacSHA256Hex(#{string_to_sign.inspect}, your_secret_key)"
138
+ end
139
+ return true
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,176 @@
1
+ require File.expand_path('../spec_helper', __FILE__)
2
+
3
+ describe Signature do
4
+ before :each do
5
+ Time.stub!(:now).and_return(Time.at(1234))
6
+
7
+ @token = Signature::Token.new('key', 'secret')
8
+
9
+ @request = Signature::Request.new('POST', '/some/path', {
10
+ "query" => "params",
11
+ "go" => "here"
12
+ })
13
+ @signature = @request.sign(@token)[:auth_signature]
14
+ end
15
+
16
+ it "should generate base64 encoded signature from correct key" do
17
+ @request.send(:string_to_sign).should == "POST\n/some/path\nauth_key=key&auth_timestamp=1234&auth_version=1.0&go=here&query=params"
18
+ @signature.should == '3b237953a5ba6619875cbb2a2d43e8da9ef5824e8a2c689f6284ac85bc1ea0db'
19
+ end
20
+
21
+ it "should make auth_hash available after request is signed" do
22
+ request = Signature::Request.new('POST', '/some/path', {
23
+ "query" => "params"
24
+ })
25
+ lambda {
26
+ request.auth_hash
27
+ }.should raise_error('Request not signed')
28
+
29
+ request.sign(@token)
30
+ request.auth_hash.should == {
31
+ :auth_signature => "da078fcedd72941b6c873caa40d0d6b2000ebfc700cee802b128dd20f72e74e9",
32
+ :auth_version => "1.0",
33
+ :auth_key => "key",
34
+ :auth_timestamp => 1234
35
+ }
36
+ end
37
+
38
+ it "should cope with symbol keys" do
39
+ @request.query_hash = {
40
+ :query => "params",
41
+ :go => "here"
42
+ }
43
+ @request.sign(@token)[:auth_signature].should == @signature
44
+ end
45
+
46
+ it "should cope with upcase keys (keys are lowercased before signing)" do
47
+ @request.query_hash = {
48
+ "Query" => "params",
49
+ "GO" => "here"
50
+ }
51
+ @request.sign(@token)[:auth_signature].should == @signature
52
+ end
53
+
54
+ it "should use the path to generate signature" do
55
+ @request.path = '/some/other/path'
56
+ @request.sign(@token)[:auth_signature].should_not == @signature
57
+ end
58
+
59
+ it "should use the query string keys to generate signature" do
60
+ @request.query_hash = {
61
+ "other" => "query"
62
+ }
63
+ @request.sign(@token)[:auth_signature].should_not == @signature
64
+ end
65
+
66
+ it "should use the query string values to generate signature" do
67
+ @request.query_hash = {
68
+ "key" => "notfoo",
69
+ "other" => 'bar'
70
+ }
71
+ @request.sign(@token)[:signature].should_not == @signature
72
+ end
73
+
74
+ describe "verification" do
75
+ before :each do
76
+ Time.stub!(:now).and_return(Time.at(1234))
77
+ @request.sign(@token)
78
+ @params = @request.query_hash.merge(@request.auth_hash)
79
+ end
80
+
81
+ it "should verify requests" do
82
+ request = Signature::Request.new('POST', '/some/path', @params)
83
+ request.authenticate_by_token(@token).should == true
84
+ end
85
+
86
+ it "should raise error if signature is not correct" do
87
+ @params[:auth_signature] = 'asdf'
88
+ request = Signature::Request.new('POST', '/some/path', @params)
89
+ lambda {
90
+ request.authenticate_by_token!(@token)
91
+ }.should raise_error('Invalid signature: you should have sent HmacSHA256Hex("POST\n/some/path\nauth_key=key&auth_timestamp=1234&auth_version=1.0&go=here&query=params", your_secret_key)')
92
+ end
93
+
94
+ it "should raise error if timestamp not available" do
95
+ @params.delete(:auth_timestamp)
96
+ request = Signature::Request.new('POST', '/some/path', @params)
97
+ lambda {
98
+ request.authenticate_by_token!(@token)
99
+ }.should raise_error('Timestamp required')
100
+ end
101
+
102
+ it "should raise error if timestamp has expired (default of 600s)" do
103
+ request = Signature::Request.new('POST', '/some/path', @params)
104
+ Time.stub!(:now).and_return(Time.at(1234 + 599))
105
+ request.authenticate_by_token!(@token).should == true
106
+ Time.stub!(:now).and_return(Time.at(1234 - 599))
107
+ request.authenticate_by_token!(@token).should == true
108
+ Time.stub!(:now).and_return(Time.at(1234 + 600))
109
+ lambda {
110
+ request.authenticate_by_token!(@token)
111
+ }.should raise_error("Timestamp expired: Given timestamp (1970-01-01T00:20:34Z) not within 600s of server time (1970-01-01T00:30:34Z)")
112
+ Time.stub!(:now).and_return(Time.at(1234 - 600))
113
+ lambda {
114
+ request.authenticate_by_token!(@token)
115
+ }.should raise_error("Timestamp expired: Given timestamp (1970-01-01T00:20:34Z) not within 600s of server time (1970-01-01T00:10:34Z)")
116
+ end
117
+
118
+ it "should be possible to customize the timeout grace period" do
119
+ grace = 10
120
+ request = Signature::Request.new('POST', '/some/path', @params)
121
+ Time.stub!(:now).and_return(Time.at(1234 + grace - 1))
122
+ request.authenticate_by_token!(@token, grace).should == true
123
+ Time.stub!(:now).and_return(Time.at(1234 + grace))
124
+ lambda {
125
+ request.authenticate_by_token!(@token, grace)
126
+ }.should raise_error("Timestamp expired: Given timestamp (1970-01-01T00:20:34Z) not within 10s of server time (1970-01-01T00:20:44Z)")
127
+ end
128
+
129
+ it "should be possible to skip timestamp check by passing nil" do
130
+ request = Signature::Request.new('POST', '/some/path', @params)
131
+ Time.stub!(:now).and_return(Time.at(1234 + 1000))
132
+ request.authenticate_by_token!(@token, nil).should == true
133
+ end
134
+
135
+ it "should check that auth_version is supplied" do
136
+ @params.delete(:auth_version)
137
+ request = Signature::Request.new('POST', '/some/path', @params)
138
+ lambda {
139
+ request.authenticate_by_token!(@token)
140
+ }.should raise_error('Version required')
141
+ end
142
+
143
+ it "should check that auth_version equals 1.0" do
144
+ @params[:auth_version] = '1.1'
145
+ request = Signature::Request.new('POST', '/some/path', @params)
146
+ lambda {
147
+ request.authenticate_by_token!(@token)
148
+ }.should raise_error('Version not supported')
149
+ end
150
+
151
+ describe "when used with optional block" do
152
+ it "should optionally take a block which yields the signature" do
153
+ request = Signature::Request.new('POST', '/some/path', @params)
154
+ request.authenticate do |key|
155
+ key.should == @token.key
156
+ @token
157
+ end.should == @token
158
+ end
159
+
160
+ it "should raise error if no auth_key supplied to request" do
161
+ @params.delete(:auth_key)
162
+ request = Signature::Request.new('POST', '/some/path', @params)
163
+ lambda {
164
+ request.authenticate { |key| nil }
165
+ }.should raise_error('Authentication key required')
166
+ end
167
+
168
+ it "should raise error if block returns nil (i.e. key doesn't exist)" do
169
+ request = Signature::Request.new('POST', '/some/path', @params)
170
+ lambda {
171
+ request.authenticate { |key| nil }
172
+ }.should raise_error('Invalid authentication key')
173
+ end
174
+ end
175
+ end
176
+ end
data/spec/spec.opts ADDED
@@ -0,0 +1 @@
1
+ --color
@@ -0,0 +1,11 @@
1
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
2
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
3
+
4
+ require 'rubygems'
5
+ require 'signature'
6
+ require 'spec'
7
+ require 'spec/autorun'
8
+
9
+ Spec::Runner.configure do |config|
10
+
11
+ end
metadata ADDED
@@ -0,0 +1,98 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: signature
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 1
8
+ - 0
9
+ version: 0.1.0
10
+ platform: ruby
11
+ authors:
12
+ - Martyn Loughran
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2010-05-07 00:00:00 +01:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: ruby-hmac
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ segments:
28
+ - 0
29
+ version: "0"
30
+ type: :runtime
31
+ version_requirements: *id001
32
+ - !ruby/object:Gem::Dependency
33
+ name: rspec
34
+ prerelease: false
35
+ requirement: &id002 !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ segments:
40
+ - 1
41
+ - 2
42
+ - 9
43
+ version: 1.2.9
44
+ type: :development
45
+ version_requirements: *id002
46
+ description: Simple key/secret based authentication for apis
47
+ email: me@mloughran.com
48
+ executables: []
49
+
50
+ extensions: []
51
+
52
+ extra_rdoc_files:
53
+ - LICENSE
54
+ - README.md
55
+ files:
56
+ - .document
57
+ - .gitignore
58
+ - LICENSE
59
+ - README.md
60
+ - Rakefile
61
+ - VERSION
62
+ - lib/signature.rb
63
+ - spec/signature_spec.rb
64
+ - spec/spec.opts
65
+ - spec/spec_helper.rb
66
+ has_rdoc: true
67
+ homepage: http://github.com/mloughran/signature
68
+ licenses: []
69
+
70
+ post_install_message:
71
+ rdoc_options:
72
+ - --charset=UTF-8
73
+ require_paths:
74
+ - lib
75
+ required_ruby_version: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ segments:
80
+ - 0
81
+ version: "0"
82
+ required_rubygems_version: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ segments:
87
+ - 0
88
+ version: "0"
89
+ requirements: []
90
+
91
+ rubyforge_project:
92
+ rubygems_version: 1.3.6
93
+ signing_key:
94
+ specification_version: 3
95
+ summary: Simple key/secret based authentication for apis
96
+ test_files:
97
+ - spec/signature_spec.rb
98
+ - spec/spec_helper.rb