signature 0.1.3 → 0.1.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -7,5 +7,9 @@ rvm:
7
7
  - jruby-19mode # JRuby in 1.9 mode
8
8
  - rbx-18mode
9
9
  - rbx-19mode
10
+ matrix:
11
+ allow_failures:
12
+ - rvm: rbx-18mode
13
+ - rvm: rbx-19mode
10
14
 
11
15
  script: bundle exec rspec spec
@@ -6,7 +6,14 @@ PATH
6
6
  GEM
7
7
  remote: http://rubygems.org/
8
8
  specs:
9
+ bacon (1.1.0)
9
10
  diff-lcs (1.1.3)
11
+ em-spec (0.2.6)
12
+ bacon
13
+ eventmachine
14
+ rspec (> 2.6.0)
15
+ test-unit
16
+ eventmachine (0.12.10)
10
17
  rspec (2.9.0)
11
18
  rspec-core (~> 2.9.0)
12
19
  rspec-expectations (~> 2.9.0)
@@ -15,10 +22,12 @@ GEM
15
22
  rspec-expectations (2.9.1)
16
23
  diff-lcs (~> 1.1.3)
17
24
  rspec-mocks (2.9.0)
25
+ test-unit (2.4.4)
18
26
 
19
27
  PLATFORMS
20
28
  ruby
21
29
 
22
30
  DEPENDENCIES
31
+ em-spec
23
32
  rspec (~> 2.9.0)
24
33
  signature!
@@ -34,59 +34,134 @@ module Signature
34
34
 
35
35
  @method = method.upcase
36
36
  @path, @query_hash, @auth_hash = path, query_hash, auth_hash
37
+ @signed = false
37
38
  end
38
39
 
40
+ # Sign the request with the given token, and return the computed
41
+ # authentication parameters
42
+ #
39
43
  def sign(token)
40
44
  @auth_hash = {
41
45
  :auth_version => "1.0",
42
46
  :auth_key => token.key,
43
47
  :auth_timestamp => Time.now.to_i.to_s
44
48
  }
45
-
46
49
  @auth_hash[:auth_signature] = signature(token)
47
50
 
51
+ @signed = true
52
+
48
53
  return @auth_hash
49
54
  end
50
55
 
51
56
  # Authenticates the request with a token
52
57
  #
53
- # Timestamp check: Unless timestamp_grace is set to nil (which will skip
54
- # the timestamp check), an exception will be raised if timestamp is not
55
- # supplied or if the timestamp provided is not within timestamp_grace of
56
- # the real time (defaults to 10 minutes)
58
+ # Raises an AuthenticationError if the request is invalid.
59
+ # AuthenticationError exception messages are designed to be exposed to API
60
+ # consumers, and should help them correct errors generating signatures
61
+ #
62
+ # Timestamp: Unless timestamp_grace is set to nil (which allows this check
63
+ # to be skipped), AuthenticationError will be raised if the timestamp is
64
+ # missing or further than timestamp_grace period away from the real time
65
+ # (defaults to 10 minutes)
57
66
  #
58
- # Signature check: Raises an exception if the signature does not match the
59
- # computed value
67
+ # Signature: Raises AuthenticationError if the signature does not match
68
+ # the computed HMAC. The error contains a hint for how to sign.
60
69
  #
61
70
  def authenticate_by_token!(token, timestamp_grace = 600)
71
+ # Validate that your code has provided a valid token. This does not
72
+ # raise an AuthenticationError since passing tokens with empty secret is
73
+ # a code error which should be fixed, not reported to the API's consumer
74
+ if token.secret.nil? || token.secret.empty?
75
+ raise "Provided token is missing secret"
76
+ end
77
+
62
78
  validate_version!
63
79
  validate_timestamp!(timestamp_grace)
64
80
  validate_signature!(token)
65
81
  true
66
82
  end
67
83
 
84
+ # Authenticate the request with a token, but rather than raising an
85
+ # exception if the request is invalid, simply returns false
86
+ #
68
87
  def authenticate_by_token(token, timestamp_grace = 600)
69
88
  authenticate_by_token!(token, timestamp_grace)
70
89
  rescue AuthenticationError
71
90
  false
72
91
  end
73
92
 
74
- def authenticate(timestamp_grace = 600, &block)
93
+ # Authenticate a request
94
+ #
95
+ # Takes a block which will be called with the auth_key from the request,
96
+ # and which should return a Signature::Token (or nil if no token can be
97
+ # found for the key)
98
+ #
99
+ # Raises errors in the same way as authenticate_by_token!
100
+ #
101
+ def authenticate(timestamp_grace = 600)
102
+ raise ArgumentError, "Block required" unless block_given?
75
103
  key = @auth_hash['auth_key']
76
- raise AuthenticationError, "Authentication key required" unless key
104
+ raise AuthenticationError, "Missing parameter: auth_key" unless key
77
105
  token = yield key
78
- unless token && token.secret
79
- raise AuthenticationError, "Invalid authentication key"
106
+ unless token
107
+ raise AuthenticationError, "Unknown auth_key"
80
108
  end
81
109
  authenticate_by_token!(token, timestamp_grace)
82
110
  return token
83
111
  end
84
112
 
113
+ # Authenticate a request asynchronously
114
+ #
115
+ # This method is useful it you're running a server inside eventmachine and
116
+ # need to lookup the token asynchronously.
117
+ #
118
+ # The block is passed an auth key and a deferrable which should succeed
119
+ # with the token, or fail if the token cannot be found
120
+ #
121
+ # This method returns a deferrable which succeeds with the valid token, or
122
+ # fails with an AuthenticationError which can be used to pass the error
123
+ # back to the user
124
+ #
125
+ def authenticate_async(timestamp_grace = 600)
126
+ raise ArgumentError, "Block required" unless block_given?
127
+ df = EM::DefaultDeferrable.new
128
+
129
+ key = @auth_hash['auth_key']
130
+
131
+ unless key
132
+ df.fail(AuthenticationError.new("Missing parameter: auth_key"))
133
+ return
134
+ end
135
+
136
+ token_df = yield key
137
+ token_df.callback { |token|
138
+ begin
139
+ authenticate_by_token!(token, timestamp_grace)
140
+ df.succeed(token)
141
+ rescue AuthenticationError => e
142
+ df.fail(e)
143
+ end
144
+ }
145
+ token_df.errback {
146
+ df.fail(AuthenticationError.new("Unknown auth_key"))
147
+ }
148
+ ensure
149
+ return df
150
+ end
151
+
152
+ # Expose the authentication parameters for a signed request
153
+ #
85
154
  def auth_hash
86
- raise "Request not signed" unless @auth_hash && @auth_hash[:auth_signature]
155
+ raise "Request not signed" unless @signed
87
156
  @auth_hash
88
157
  end
89
158
 
159
+ # Query parameters merged with the computed authentication parameters
160
+ #
161
+ def signed_params
162
+ @query_hash.merge(auth_hash)
163
+ end
164
+
90
165
  private
91
166
 
92
167
  def signature(token)
@@ -1,3 +1,3 @@
1
1
  module Signature
2
- VERSION = "0.1.3"
2
+ VERSION = "0.1.4"
3
3
  end
@@ -19,4 +19,5 @@ Gem::Specification.new do |s|
19
19
 
20
20
  s.add_dependency "jruby-openssl" if defined?(JRUBY_VERSION)
21
21
  s.add_development_dependency "rspec", "~> 2.9.0"
22
+ s.add_development_dependency "em-spec"
22
23
  end
@@ -10,72 +10,81 @@ describe Signature do
10
10
  "query" => "params",
11
11
  "go" => "here"
12
12
  })
13
- @signature = @request.sign(@token)[:auth_signature]
14
13
  end
15
14
 
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
15
+ describe "generating signatures" do
16
+ before :each do
17
+ @signature = "3b237953a5ba6619875cbb2a2d43e8da9ef5824e8a2c689f6284ac85bc1ea0db"
18
+ end
20
19
 
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
20
+ it "should generate signature correctly" do
21
+ @request.sign(@token)
22
+ string = @request.send(:string_to_sign)
23
+ string.should == "POST\n/some/path\nauth_key=key&auth_timestamp=1234&auth_version=1.0&go=here&query=params"
37
24
 
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
25
+ digest = OpenSSL::Digest::SHA256.new
26
+ signature = OpenSSL::HMAC.hexdigest(digest, @token.secret, string)
27
+ signature.should == @signature
28
+ end
45
29
 
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
30
+ it "should make auth_hash available after request is signed" do
31
+ @request.query_hash = {
32
+ "query" => "params"
33
+ }
34
+ lambda {
35
+ @request.auth_hash
36
+ }.should raise_error('Request not signed')
53
37
 
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
38
+ @request.sign(@token)
39
+ @request.auth_hash.should == {
40
+ :auth_signature => "da078fcedd72941b6c873caa40d0d6b2000ebfc700cee802b128dd20f72e74e9",
41
+ :auth_version => "1.0",
42
+ :auth_key => "key",
43
+ :auth_timestamp => '1234'
44
+ }
45
+ end
58
46
 
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
47
+ it "should cope with symbol keys" do
48
+ @request.query_hash = {
49
+ :query => "params",
50
+ :go => "here"
51
+ }
52
+ @request.sign(@token)[:auth_signature].should == @signature
53
+ end
54
+
55
+ it "should cope with upcase keys (keys are lowercased before signing)" do
56
+ @request.query_hash = {
57
+ "Query" => "params",
58
+ "GO" => "here"
59
+ }
60
+ @request.sign(@token)[:auth_signature].should == @signature
61
+ end
65
62
 
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
63
+ it "should use the path to generate signature" do
64
+ @request.path = '/some/other/path'
65
+ @request.sign(@token)[:auth_signature].should_not == @signature
66
+ end
67
+
68
+ it "should use the query string keys to generate signature" do
69
+ @request.query_hash = {
70
+ "other" => "query"
71
+ }
72
+ @request.sign(@token)[:auth_signature].should_not == @signature
73
+ end
74
+
75
+ it "should use the query string values to generate signature" do
76
+ @request.query_hash = {
77
+ "key" => "notfoo",
78
+ "other" => 'bar'
79
+ }
80
+ @request.sign(@token)[:signature].should_not == @signature
81
+ end
72
82
  end
73
83
 
74
84
  describe "verification" do
75
85
  before :each do
76
- Time.stub!(:now).and_return(Time.at(1234))
77
86
  @request.sign(@token)
78
- @params = @request.query_hash.merge(@request.auth_hash)
87
+ @params = @request.signed_params
79
88
  end
80
89
 
81
90
  it "should verify requests" do
@@ -148,6 +157,15 @@ describe Signature do
148
157
  }.should raise_error('Version not supported')
149
158
  end
150
159
 
160
+ it "should validate that the provided token has a non-empty secret" do
161
+ token = Signature::Token.new('key', '')
162
+ request = Signature::Request.new('POST', '/some/path', @params)
163
+
164
+ lambda {
165
+ request.authenticate_by_token!(token)
166
+ }.should raise_error('Provided token is missing secret')
167
+ end
168
+
151
169
  describe "when used with optional block" do
152
170
  it "should optionally take a block which yields the signature" do
153
171
  request = Signature::Request.new('POST', '/some/path', @params)
@@ -162,14 +180,83 @@ describe Signature do
162
180
  request = Signature::Request.new('POST', '/some/path', @params)
163
181
  lambda {
164
182
  request.authenticate { |key| nil }
165
- }.should raise_error('Authentication key required')
183
+ }.should raise_error('Missing parameter: auth_key')
166
184
  end
167
185
 
168
186
  it "should raise error if block returns nil (i.e. key doesn't exist)" do
169
187
  request = Signature::Request.new('POST', '/some/path', @params)
170
188
  lambda {
171
189
  request.authenticate { |key| nil }
172
- }.should raise_error('Invalid authentication key')
190
+ }.should raise_error('Unknown auth_key')
191
+ end
192
+
193
+ it "should raise unless block given" do
194
+ request = Signature::Request.new('POST', '/some/path', @params)
195
+ lambda {
196
+ request.authenticate
197
+ }.should raise_error(ArgumentError, "Block required")
198
+ end
199
+ end
200
+
201
+ describe "authenticate_async" do
202
+ include EM::SpecHelper
203
+ default_timeout 1
204
+
205
+ it "returns a deferrable which succeeds if authentication passes" do
206
+ request = Signature::Request.new('POST', '/some/path', @params)
207
+ em {
208
+ df = EM::DefaultDeferrable.new
209
+
210
+ request_df = request.authenticate_async do |key|
211
+ df
212
+ end
213
+
214
+ df.succeed(@token)
215
+
216
+ request_df.callback { |token|
217
+ token.should == @token
218
+ done
219
+ }
220
+ }
221
+ end
222
+
223
+ it "returns a deferrable which fails if block df fails" do
224
+ request = Signature::Request.new('POST', '/some/path', @params)
225
+ em {
226
+ df = EM::DefaultDeferrable.new
227
+
228
+ request_df = request.authenticate_async do |key|
229
+ df
230
+ end
231
+
232
+ df.fail()
233
+
234
+ request_df.errback { |e|
235
+ e.class.should == Signature::AuthenticationError
236
+ e.message.should == 'Unknown auth_key'
237
+ done
238
+ }
239
+ }
240
+ end
241
+
242
+ it "returns a deferrable which fails if request does not validate" do
243
+ request = Signature::Request.new('POST', '/some/path', @params)
244
+ em {
245
+ df = EM::DefaultDeferrable.new
246
+
247
+ request_df = request.authenticate_async do |key|
248
+ df
249
+ end
250
+
251
+ token = Signature::Token.new('key', 'wrong_secret')
252
+ df.succeed(token)
253
+
254
+ request_df.errback { |e|
255
+ e.class.should == Signature::AuthenticationError
256
+ e.message.should =~ /Invalid signature/
257
+ done
258
+ }
259
+ }
173
260
  end
174
261
  end
175
262
  end
@@ -1,9 +1,8 @@
1
- $LOAD_PATH.unshift(File.dirname(__FILE__))
2
1
  $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
3
-
4
- require 'rubygems'
5
2
  require 'signature'
3
+
6
4
  require 'rspec'
5
+ require 'em-spec/rspec'
7
6
 
8
7
  RSpec.configure do |config|
9
8
 
metadata CHANGED
@@ -1,35 +1,62 @@
1
- --- !ruby/object:Gem::Specification
1
+ --- !ruby/object:Gem::Specification
2
2
  name: signature
3
- version: !ruby/object:Gem::Version
4
- version: 0.1.3
3
+ version: !ruby/object:Gem::Version
4
+ hash: 3713544621248369165
5
5
  prerelease:
6
+ segments:
7
+ - 0
8
+ - 1
9
+ - 4
10
+ version: 0.1.4
6
11
  platform: ruby
7
- authors:
12
+ authors:
8
13
  - Martyn Loughran
9
14
  autorequire:
10
15
  bindir: bin
11
16
  cert_chain: []
12
- date: 2012-05-06 00:00:00.000000000 Z
13
- dependencies:
14
- - !ruby/object:Gem::Dependency
17
+
18
+ date: 2012-08-15 00:00:00 Z
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
15
21
  name: rspec
16
- requirement: &70163026801480 !ruby/object:Gem::Requirement
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
17
24
  none: false
18
- requirements:
25
+ requirements:
19
26
  - - ~>
20
- - !ruby/object:Gem::Version
27
+ - !ruby/object:Gem::Version
28
+ hash: 1156501143490752456
29
+ segments:
30
+ - 2
31
+ - 9
32
+ - 0
21
33
  version: 2.9.0
22
34
  type: :development
35
+ version_requirements: *id001
36
+ - !ruby/object:Gem::Dependency
37
+ name: em-spec
23
38
  prerelease: false
24
- version_requirements: *70163026801480
39
+ requirement: &id002 !ruby/object:Gem::Requirement
40
+ none: false
41
+ requirements:
42
+ - - ">="
43
+ - !ruby/object:Gem::Version
44
+ hash: 2002549777813010636
45
+ segments:
46
+ - 0
47
+ version: "0"
48
+ type: :development
49
+ version_requirements: *id002
25
50
  description: Simple key/secret based authentication for apis
26
- email:
51
+ email:
27
52
  - me@mloughran.com
28
53
  executables: []
54
+
29
55
  extensions: []
56
+
30
57
  extra_rdoc_files: []
31
- files:
32
- - .document
58
+
59
+ files:
33
60
  - .gitignore
34
61
  - .travis.yml
35
62
  - Gemfile
@@ -37,7 +64,6 @@ files:
37
64
  - LICENSE
38
65
  - README.md
39
66
  - Rakefile
40
- - VERSION
41
67
  - lib/signature.rb
42
68
  - lib/signature/version.rb
43
69
  - signature.gemspec
@@ -45,29 +71,37 @@ files:
45
71
  - spec/spec_helper.rb
46
72
  homepage: http://github.com/mloughran/signature
47
73
  licenses: []
74
+
48
75
  post_install_message:
49
76
  rdoc_options: []
50
- require_paths:
77
+
78
+ require_paths:
51
79
  - lib
52
- required_ruby_version: !ruby/object:Gem::Requirement
80
+ required_ruby_version: !ruby/object:Gem::Requirement
53
81
  none: false
54
- requirements:
55
- - - ! '>='
56
- - !ruby/object:Gem::Version
57
- version: '0'
58
- required_rubygems_version: !ruby/object:Gem::Requirement
82
+ requirements:
83
+ - - ">="
84
+ - !ruby/object:Gem::Version
85
+ hash: 2002549777813010636
86
+ segments:
87
+ - 0
88
+ version: "0"
89
+ required_rubygems_version: !ruby/object:Gem::Requirement
59
90
  none: false
60
- requirements:
61
- - - ! '>='
62
- - !ruby/object:Gem::Version
63
- version: '0'
91
+ requirements:
92
+ - - ">="
93
+ - !ruby/object:Gem::Version
94
+ hash: 2002549777813010636
95
+ segments:
96
+ - 0
97
+ version: "0"
64
98
  requirements: []
99
+
65
100
  rubyforge_project:
66
- rubygems_version: 1.8.10
101
+ rubygems_version: 1.8.12
67
102
  signing_key:
68
103
  specification_version: 3
69
104
  summary: Simple key/secret based authentication for apis
70
- test_files:
105
+ test_files:
71
106
  - spec/signature_spec.rb
72
107
  - spec/spec_helper.rb
73
- has_rdoc:
data/.document DELETED
@@ -1,5 +0,0 @@
1
- README.rdoc
2
- lib/**/*.rb
3
- bin/*
4
- features/**/*.feature
5
- LICENSE
data/VERSION DELETED
@@ -1 +0,0 @@
1
- 0.1.1