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.
- data/.travis.yml +4 -0
- data/Gemfile.lock +9 -0
- data/lib/signature.rb +87 -12
- data/lib/signature/version.rb +1 -1
- data/signature.gemspec +1 -0
- data/spec/signature_spec.rb +142 -55
- data/spec/spec_helper.rb +2 -3
- metadata +63 -29
- data/.document +0 -5
- data/VERSION +0 -1
data/.travis.yml
CHANGED
data/Gemfile.lock
CHANGED
@@ -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!
|
data/lib/signature.rb
CHANGED
@@ -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
|
-
#
|
54
|
-
#
|
55
|
-
#
|
56
|
-
#
|
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
|
59
|
-
# computed
|
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
|
-
|
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, "
|
104
|
+
raise AuthenticationError, "Missing parameter: auth_key" unless key
|
77
105
|
token = yield key
|
78
|
-
unless token
|
79
|
-
raise AuthenticationError, "
|
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 @
|
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)
|
data/lib/signature/version.rb
CHANGED
data/signature.gemspec
CHANGED
data/spec/signature_spec.rb
CHANGED
@@ -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
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
15
|
+
describe "generating signatures" do
|
16
|
+
before :each do
|
17
|
+
@signature = "3b237953a5ba6619875cbb2a2d43e8da9ef5824e8a2c689f6284ac85bc1ea0db"
|
18
|
+
end
|
20
19
|
|
21
|
-
|
22
|
-
|
23
|
-
|
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
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
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
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
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
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
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
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
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
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
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.
|
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('
|
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('
|
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
|
data/spec/spec_helper.rb
CHANGED
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
|
-
|
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
|
-
|
13
|
-
|
14
|
-
|
17
|
+
|
18
|
+
date: 2012-08-15 00:00:00 Z
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
15
21
|
name: rspec
|
16
|
-
|
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
|
-
|
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
|
-
|
32
|
-
|
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
|
-
|
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
|
-
|
58
|
-
|
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
|
-
|
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.
|
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
data/VERSION
DELETED
@@ -1 +0,0 @@
|
|
1
|
-
0.1.1
|