signature 0.1.3 → 0.1.4

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