xignature 0.1.8

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 2f3c4d3bc528bed083fb7d10d4388f2248418c52
4
+ data.tar.gz: 2701be3436302be927b01e75c5060dffc89dd8c8
5
+ SHA512:
6
+ metadata.gz: 20ccf1339835ceae72f7272a1d4364fbd1ae4fd4e3a35872340186a55d77ae436cc3061ba2a28c97d944cbd53b4bf661aa7189b0a221193e45533314780628bc
7
+ data.tar.gz: d1ce0fa5de70ccfd768b4b85b5cd8a8475d2698fb1c4ac4454ce821fc8a796abe9028baf9923dcde4dbff200222ebc82e6feda9dbfdcda14b7c341a58f097547
@@ -0,0 +1,23 @@
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
22
+ .rbx
23
+ .rspec
@@ -0,0 +1,19 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.8.7
4
+ - 1.9.2
5
+ - 1.9.3
6
+ - 2.0.0
7
+ - 2.2.0
8
+ - jruby-18mode
9
+ - jruby-19mode
10
+ - rbx-18mode
11
+ - rbx-19mode
12
+ matrix:
13
+ allow_failures:
14
+ - rvm: jruby-18mode
15
+ - rvm: jruby-19mode
16
+ - rvm: rbx-18mode
17
+ - rvm: rbx-19mode
18
+
19
+ script: bundle exec rspec spec
@@ -0,0 +1,5 @@
1
+
2
+ 0.1.8 / 2015-01-16
3
+ ==================
4
+
5
+ * SECURITY: Perform constant time string comparison when validating xignatures
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
@@ -0,0 +1,33 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ xignature (0.1.8)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ bacon (1.2.0)
10
+ diff-lcs (1.2.4)
11
+ em-spec (0.2.6)
12
+ bacon
13
+ eventmachine
14
+ rspec (> 2.6.0)
15
+ test-unit
16
+ eventmachine (1.0.7)
17
+ rspec (2.13.0)
18
+ rspec-core (~> 2.13.0)
19
+ rspec-expectations (~> 2.13.0)
20
+ rspec-mocks (~> 2.13.0)
21
+ rspec-core (2.13.1)
22
+ rspec-expectations (2.13.0)
23
+ diff-lcs (>= 1.1.3, < 2.0)
24
+ rspec-mocks (2.13.1)
25
+ test-unit (2.5.4)
26
+
27
+ PLATFORMS
28
+ ruby
29
+
30
+ DEPENDENCIES
31
+ em-spec
32
+ rspec
33
+ xignature!
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.
@@ -0,0 +1,65 @@
1
+ xignature - xignature renamed.
2
+ =========
3
+
4
+ [![Build Status](https://secure.travis-ci.org/xn/xignature.png?branch=master)](http://travis-ci.org/xn/xignature)
5
+
6
+ Examples
7
+ --------
8
+
9
+ Client example
10
+
11
+ ```ruby
12
+ params = {:some => 'parameters'}
13
+ token = Xignature::Token.new('my_key', 'my_secret')
14
+ request = Xignature::Request.new('POST', '/api/thing', params)
15
+ auth_hash = request.sign(token)
16
+ query_params = params.merge(auth_hash)
17
+
18
+ HTTParty.post('http://myservice/api/thing', {
19
+ :body => query_params
20
+ })
21
+ ```
22
+
23
+ `query_params` looks like:
24
+
25
+ ```ruby
26
+ {
27
+ :some => "parameters",
28
+ :auth_timestamp => 1273231888,
29
+ :auth_xignature => "28b6bb0f242f71064916fad6ae463fe91f5adc302222dfc02c348ae1941eaf80",
30
+ :auth_version => "1.0",
31
+ :auth_key => "my_key"
32
+ }
33
+
34
+ ```
35
+ Server example (sinatra)
36
+
37
+ ```ruby
38
+ error Xignature::AuthenticationError do |controller|
39
+ error = controller.env["sinatra.error"]
40
+ halt 401, "401 UNAUTHORIZED: #{error.message}\n"
41
+ end
42
+
43
+ post '/api/thing' do
44
+ request = Xignature::Request.new('POST', env["REQUEST_PATH"], params)
45
+ # This will raise a Xignature::AuthenticationError if request does not authenticate
46
+ token = request.authenticate do |key|
47
+ Xignature::Token.new(key, lookup_secret(key))
48
+ end
49
+
50
+ # Do whatever you need to do
51
+ end
52
+ ```
53
+
54
+ Developing
55
+ ----------
56
+
57
+ bundle
58
+ bundle exec rspec spec/*_spec.rb
59
+
60
+ Please see the travis status for a list of rubies tested against
61
+
62
+ Copyright
63
+ ---------
64
+
65
+ Copyright (c) 2010 Martyn Loughran. See LICENSE for details.
@@ -0,0 +1,2 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
@@ -0,0 +1,232 @@
1
+ require 'openssl'
2
+
3
+ require 'xignature/query_encoder'
4
+
5
+ module Xignature
6
+ class AuthenticationError < RuntimeError; end
7
+
8
+ class Token
9
+ attr_reader :key, :secret
10
+
11
+ def initialize(key, secret)
12
+ @key, @secret = key, secret
13
+ end
14
+
15
+ def sign(request)
16
+ request.sign(self)
17
+ end
18
+ end
19
+
20
+ class Request
21
+ attr_accessor :path, :query_hash
22
+
23
+ include QueryEncoder
24
+
25
+ # http://www.w3.org/TR/NOTE-datetime
26
+ ISO8601 = "%Y-%m-%dT%H:%M:%SZ"
27
+
28
+ def initialize(method, path, query)
29
+ raise ArgumentError, "Expected string" unless path.kind_of?(String)
30
+ raise ArgumentError, "Expected hash" unless query.kind_of?(Hash)
31
+
32
+ query_hash = {}
33
+ auth_hash = {}
34
+ query.each do |key, v|
35
+ k = key.to_s.downcase
36
+ k[0..4] == 'auth_' ? auth_hash[k] = v : query_hash[k] = v
37
+ end
38
+
39
+ @method = method.upcase
40
+ @path, @query_hash, @auth_hash = path, query_hash, auth_hash
41
+ @signed = false
42
+ end
43
+
44
+ # Sign the request with the given token, and return the computed
45
+ # authentication parameters
46
+ #
47
+ def sign(token)
48
+ @auth_hash = {
49
+ :auth_version => "1.0",
50
+ :auth_key => token.key,
51
+ :auth_timestamp => Time.now.to_i.to_s
52
+ }
53
+ @auth_hash[:auth_signature] = signature(token)
54
+
55
+ @signed = true
56
+
57
+ return @auth_hash
58
+ end
59
+
60
+ # Authenticates the request with a token
61
+ #
62
+ # Raises an AuthenticationError if the request is invalid.
63
+ # AuthenticationError exception messages are designed to be exposed to API
64
+ # consumers, and should help them correct errors generating signatures
65
+ #
66
+ # Timestamp: Unless timestamp_grace is set to nil (which allows this check
67
+ # to be skipped), AuthenticationError will be raised if the timestamp is
68
+ # missing or further than timestamp_grace period away from the real time
69
+ # (defaults to 10 minutes)
70
+ #
71
+ # Xignature: Raises AuthenticationError if the signature does not match
72
+ # the computed HMAC. The error contains a hint for how to sign.
73
+ #
74
+ def authenticate_by_token!(token, timestamp_grace = 600)
75
+ # Validate that your code has provided a valid token. This does not
76
+ # raise an AuthenticationError since passing tokens with empty secret is
77
+ # a code error which should be fixed, not reported to the API's consumer
78
+ if token.secret.nil? || token.secret.empty?
79
+ raise "Provided token is missing secret"
80
+ end
81
+
82
+ validate_version!
83
+ validate_timestamp!(timestamp_grace)
84
+ validate_signature!(token)
85
+ true
86
+ end
87
+
88
+ # Authenticate the request with a token, but rather than raising an
89
+ # exception if the request is invalid, simply returns false
90
+ #
91
+ def authenticate_by_token(token, timestamp_grace = 600)
92
+ authenticate_by_token!(token, timestamp_grace)
93
+ rescue AuthenticationError
94
+ false
95
+ end
96
+
97
+ # Authenticate a request
98
+ #
99
+ # Takes a block which will be called with the auth_key from the request,
100
+ # and which should return a Xignature::Token (or nil if no token can be
101
+ # found for the key)
102
+ #
103
+ # Raises errors in the same way as authenticate_by_token!
104
+ #
105
+ def authenticate(timestamp_grace = 600)
106
+ raise ArgumentError, "Block required" unless block_given?
107
+ key = @auth_hash['auth_key']
108
+ raise AuthenticationError, "Missing parameter: auth_key" unless key
109
+ token = yield key
110
+ unless token
111
+ raise AuthenticationError, "Unknown auth_key"
112
+ end
113
+ authenticate_by_token!(token, timestamp_grace)
114
+ return token
115
+ end
116
+
117
+ # Authenticate a request asynchronously
118
+ #
119
+ # This method is useful it you're running a server inside eventmachine and
120
+ # need to lookup the token asynchronously.
121
+ #
122
+ # The block is passed an auth key and a deferrable which should succeed
123
+ # with the token, or fail if the token cannot be found
124
+ #
125
+ # This method returns a deferrable which succeeds with the valid token, or
126
+ # fails with an AuthenticationError which can be used to pass the error
127
+ # back to the user
128
+ #
129
+ def authenticate_async(timestamp_grace = 600)
130
+ raise ArgumentError, "Block required" unless block_given?
131
+ df = EM::DefaultDeferrable.new
132
+
133
+ key = @auth_hash['auth_key']
134
+
135
+ unless key
136
+ df.fail(AuthenticationError.new("Missing parameter: auth_key"))
137
+ return
138
+ end
139
+
140
+ token_df = yield key
141
+ token_df.callback { |token|
142
+ begin
143
+ authenticate_by_token!(token, timestamp_grace)
144
+ df.succeed(token)
145
+ rescue AuthenticationError => e
146
+ df.fail(e)
147
+ end
148
+ }
149
+ token_df.errback {
150
+ df.fail(AuthenticationError.new("Unknown auth_key"))
151
+ }
152
+ ensure
153
+ return df
154
+ end
155
+
156
+ # Expose the authentication parameters for a signed request
157
+ #
158
+ def auth_hash
159
+ raise "Request not signed" unless @signed
160
+ @auth_hash
161
+ end
162
+
163
+ # Query parameters merged with the computed authentication parameters
164
+ #
165
+ def signed_params
166
+ @query_hash.merge(auth_hash)
167
+ end
168
+
169
+ private
170
+
171
+ def signature(token)
172
+ digest = OpenSSL::Digest::SHA256.new
173
+ OpenSSL::HMAC.hexdigest(digest, token.secret, string_to_sign)
174
+ end
175
+
176
+ def string_to_sign
177
+ [@method, @path, parameter_string].join("\n")
178
+ end
179
+
180
+ def parameter_string
181
+ param_hash = @query_hash.merge(@auth_hash || {})
182
+
183
+ # Convert keys to lowercase strings
184
+ hash = {}; param_hash.each { |k,v| hash[k.to_s.downcase] = v }
185
+
186
+ # Exclude signature from signature generation!
187
+ hash.delete("auth_signature")
188
+
189
+ hash.sort.map do |k, v|
190
+ QueryEncoder.encode_param_without_escaping(k, v)
191
+ end.join('&')
192
+ end
193
+
194
+ def validate_version!
195
+ version = @auth_hash["auth_version"]
196
+ raise AuthenticationError, "Version required" unless version
197
+ raise AuthenticationError, "Version not supported" unless version == '1.0'
198
+ end
199
+
200
+ def validate_timestamp!(grace)
201
+ return true if grace.nil?
202
+
203
+ timestamp = @auth_hash["auth_timestamp"]
204
+ error = (timestamp.to_i - Time.now.to_i).abs
205
+ raise AuthenticationError, "Timestamp required" unless timestamp
206
+ if error >= grace
207
+ raise AuthenticationError, "Timestamp expired: Given timestamp "\
208
+ "(#{Time.at(timestamp.to_i).utc.strftime(ISO8601)}) "\
209
+ "not within #{grace}s of server time "\
210
+ "(#{Time.now.utc.strftime(ISO8601)})"
211
+ end
212
+ return true
213
+ end
214
+
215
+ def validate_signature!(token)
216
+ unless identical? @auth_hash["auth_signature"], signature(token)
217
+ raise AuthenticationError, "Invalid signature: you should have "\
218
+ "sent HmacSHA256Hex(#{string_to_sign.inspect}, your_secret_key)"\
219
+ ", but you sent #{@auth_hash["auth_signature"].inspect}"
220
+ end
221
+ return true
222
+ end
223
+
224
+ # Constant time string comparison
225
+ def identical?(a, b)
226
+ return true if a.nil? && b.nil?
227
+ return false if a.nil? || b.nil?
228
+ return false unless a.bytesize == b.bytesize
229
+ a.bytes.zip(b.bytes).reduce(0) { |memo, (a, b)| memo += a ^ b } == 0
230
+ end
231
+ end
232
+ end
@@ -0,0 +1,47 @@
1
+ module Xignature
2
+ # Query string encoding extracted with thanks from em-http-request
3
+ module QueryEncoder
4
+ class << self
5
+ # URL encodes query parameters:
6
+ # single k=v, or a URL encoded array, if v is an array of values
7
+ def encode_param(k, v)
8
+ if v.is_a?(Array)
9
+ v.map { |e| escape(k) + "[]=" + escape(e) }.join("&")
10
+ else
11
+ escape(k) + "=" + escape(v)
12
+ end
13
+ end
14
+
15
+ # Like encode_param, but doesn't url escape keys or values
16
+ def encode_param_without_escaping(k, v)
17
+ if v.is_a?(Array)
18
+ v.map { |e| k + "[]=" + e }.join("&")
19
+ else
20
+ "#{k}=#{v}"
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ def escape(s)
27
+ if defined?(EscapeUtils)
28
+ EscapeUtils.escape_url(s.to_s)
29
+ else
30
+ s.to_s.gsub(/([^a-zA-Z0-9_.-]+)/n) {
31
+ '%'+$1.unpack('H2'*bytesize($1)).join('%').upcase
32
+ }
33
+ end
34
+ end
35
+
36
+ if ''.respond_to?(:bytesize)
37
+ def bytesize(string)
38
+ string.bytesize
39
+ end
40
+ else
41
+ def bytesize(string)
42
+ string.size
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,3 @@
1
+ module Xignature
2
+ VERSION = "0.1.8"
3
+ end
@@ -0,0 +1,9 @@
1
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+ require 'xignature'
3
+
4
+ require 'rspec'
5
+ require 'em-spec/rspec'
6
+
7
+ RSpec.configure do |config|
8
+
9
+ end
@@ -0,0 +1,285 @@
1
+ require File.expand_path('../spec_helper', __FILE__)
2
+
3
+ describe Xignature do
4
+ before :each do
5
+ Time.stub!(:now).and_return(Time.at(1234))
6
+
7
+ @token = Xignature::Token.new('key', 'secret')
8
+
9
+ @request = Xignature::Request.new('POST', '/some/path', {
10
+ "query" => "params",
11
+ "go" => "here"
12
+ })
13
+ end
14
+
15
+ describe "generating signatures" do
16
+ before :each do
17
+ @signature = "3b237953a5ba6619875cbb2a2d43e8da9ef5824e8a2c689f6284ac85bc1ea0db"
18
+ end
19
+
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"
24
+
25
+ digest = OpenSSL::Digest::SHA256.new
26
+ signature = OpenSSL::HMAC.hexdigest(digest, @token.secret, string)
27
+ signature.should == @signature
28
+ end
29
+
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')
37
+
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
46
+
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
62
+
63
+ it "should generate correct string when query hash contains array" do
64
+ @request.query_hash = {
65
+ "things" => ["thing1", "thing2"]
66
+ }
67
+ @request.send(:string_to_sign).should == "POST\n/some/path\nthings[]=thing1&things[]=thing2"
68
+ end
69
+
70
+ # This may well change in auth version 2
71
+ it "should not escape keys or values in the query string" do
72
+ @request.query_hash = {
73
+ "key;" => "value@"
74
+ }
75
+ @request.send(:string_to_sign).should == "POST\n/some/path\nkey;=value@"
76
+ end
77
+
78
+ it "should cope with requests where the value is nil (antiregression)" do
79
+ @request.query_hash = {
80
+ "key" => nil
81
+ }
82
+ @request.send(:string_to_sign).should == "POST\n/some/path\nkey="
83
+ end
84
+
85
+ it "should use the path to generate signature" do
86
+ @request.path = '/some/other/path'
87
+ @request.sign(@token)[:auth_signature].should_not == @signature
88
+ end
89
+
90
+ it "should use the query string keys to generate signature" do
91
+ @request.query_hash = {
92
+ "other" => "query"
93
+ }
94
+ @request.sign(@token)[:auth_signature].should_not == @signature
95
+ end
96
+
97
+ it "should use the query string values to generate signature" do
98
+ @request.query_hash = {
99
+ "key" => "notfoo",
100
+ "other" => 'bar'
101
+ }
102
+ @request.sign(@token)[:signature].should_not == @signature
103
+ end
104
+ end
105
+
106
+ describe "verification" do
107
+ before :each do
108
+ @request.sign(@token)
109
+ @params = @request.signed_params
110
+ end
111
+
112
+ it "should verify requests" do
113
+ request = Xignature::Request.new('POST', '/some/path', @params)
114
+ request.authenticate_by_token(@token).should == true
115
+ end
116
+
117
+ it "should raise error if signature is not correct" do
118
+ @params[:auth_signature] = 'asdf'
119
+ request = Xignature::Request.new('POST', '/some/path', @params)
120
+ lambda {
121
+ request.authenticate_by_token!(@token)
122
+ }.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), but you sent "asdf"')
123
+ end
124
+
125
+ it "should raise error if timestamp not available" do
126
+ @params.delete(:auth_timestamp)
127
+ request = Xignature::Request.new('POST', '/some/path', @params)
128
+ lambda {
129
+ request.authenticate_by_token!(@token)
130
+ }.should raise_error('Timestamp required')
131
+ end
132
+
133
+ it "should raise error if timestamp has expired (default of 600s)" do
134
+ request = Xignature::Request.new('POST', '/some/path', @params)
135
+ Time.stub!(:now).and_return(Time.at(1234 + 599))
136
+ request.authenticate_by_token!(@token).should == true
137
+ Time.stub!(:now).and_return(Time.at(1234 - 599))
138
+ request.authenticate_by_token!(@token).should == true
139
+ Time.stub!(:now).and_return(Time.at(1234 + 600))
140
+ lambda {
141
+ request.authenticate_by_token!(@token)
142
+ }.should raise_error("Timestamp expired: Given timestamp (1970-01-01T00:20:34Z) not within 600s of server time (1970-01-01T00:30:34Z)")
143
+ Time.stub!(:now).and_return(Time.at(1234 - 600))
144
+ lambda {
145
+ request.authenticate_by_token!(@token)
146
+ }.should raise_error("Timestamp expired: Given timestamp (1970-01-01T00:20:34Z) not within 600s of server time (1970-01-01T00:10:34Z)")
147
+ end
148
+
149
+ it "should be possible to customize the timeout grace period" do
150
+ grace = 10
151
+ request = Xignature::Request.new('POST', '/some/path', @params)
152
+ Time.stub!(:now).and_return(Time.at(1234 + grace - 1))
153
+ request.authenticate_by_token!(@token, grace).should == true
154
+ Time.stub!(:now).and_return(Time.at(1234 + grace))
155
+ lambda {
156
+ request.authenticate_by_token!(@token, grace)
157
+ }.should raise_error("Timestamp expired: Given timestamp (1970-01-01T00:20:34Z) not within 10s of server time (1970-01-01T00:20:44Z)")
158
+ end
159
+
160
+ it "should be possible to skip timestamp check by passing nil" do
161
+ request = Xignature::Request.new('POST', '/some/path', @params)
162
+ Time.stub!(:now).and_return(Time.at(1234 + 1000))
163
+ request.authenticate_by_token!(@token, nil).should == true
164
+ end
165
+
166
+ it "should check that auth_version is supplied" do
167
+ @params.delete(:auth_version)
168
+ request = Xignature::Request.new('POST', '/some/path', @params)
169
+ lambda {
170
+ request.authenticate_by_token!(@token)
171
+ }.should raise_error('Version required')
172
+ end
173
+
174
+ it "should check that auth_version equals 1.0" do
175
+ @params[:auth_version] = '1.1'
176
+ request = Xignature::Request.new('POST', '/some/path', @params)
177
+ lambda {
178
+ request.authenticate_by_token!(@token)
179
+ }.should raise_error('Version not supported')
180
+ end
181
+
182
+ it "should validate that the provided token has a non-empty secret" do
183
+ token = Xignature::Token.new('key', '')
184
+ request = Xignature::Request.new('POST', '/some/path', @params)
185
+
186
+ lambda {
187
+ request.authenticate_by_token!(token)
188
+ }.should raise_error('Provided token is missing secret')
189
+ end
190
+
191
+ describe "when used with optional block" do
192
+ it "should optionally take a block which yields the signature" do
193
+ request = Xignature::Request.new('POST', '/some/path', @params)
194
+ request.authenticate do |key|
195
+ key.should == @token.key
196
+ @token
197
+ end.should == @token
198
+ end
199
+
200
+ it "should raise error if no auth_key supplied to request" do
201
+ @params.delete(:auth_key)
202
+ request = Xignature::Request.new('POST', '/some/path', @params)
203
+ lambda {
204
+ request.authenticate { |key| nil }
205
+ }.should raise_error('Missing parameter: auth_key')
206
+ end
207
+
208
+ it "should raise error if block returns nil (i.e. key doesn't exist)" do
209
+ request = Xignature::Request.new('POST', '/some/path', @params)
210
+ lambda {
211
+ request.authenticate { |key| nil }
212
+ }.should raise_error('Unknown auth_key')
213
+ end
214
+
215
+ it "should raise unless block given" do
216
+ request = Xignature::Request.new('POST', '/some/path', @params)
217
+ lambda {
218
+ request.authenticate
219
+ }.should raise_error(ArgumentError, "Block required")
220
+ end
221
+ end
222
+
223
+ describe "authenticate_async" do
224
+ include EM::SpecHelper
225
+ default_timeout 1
226
+
227
+ it "returns a deferrable which succeeds if authentication passes" do
228
+ request = Xignature::Request.new('POST', '/some/path', @params)
229
+ em {
230
+ df = EM::DefaultDeferrable.new
231
+
232
+ request_df = request.authenticate_async do |key|
233
+ df
234
+ end
235
+
236
+ df.succeed(@token)
237
+
238
+ request_df.callback { |token|
239
+ token.should == @token
240
+ done
241
+ }
242
+ }
243
+ end
244
+
245
+ it "returns a deferrable which fails if block df fails" do
246
+ request = Xignature::Request.new('POST', '/some/path', @params)
247
+ em {
248
+ df = EM::DefaultDeferrable.new
249
+
250
+ request_df = request.authenticate_async do |key|
251
+ df
252
+ end
253
+
254
+ df.fail()
255
+
256
+ request_df.errback { |e|
257
+ e.class.should == Xignature::AuthenticationError
258
+ e.message.should == 'Unknown auth_key'
259
+ done
260
+ }
261
+ }
262
+ end
263
+
264
+ it "returns a deferrable which fails if request does not validate" do
265
+ request = Xignature::Request.new('POST', '/some/path', @params)
266
+ em {
267
+ df = EM::DefaultDeferrable.new
268
+
269
+ request_df = request.authenticate_async do |key|
270
+ df
271
+ end
272
+
273
+ token = Xignature::Token.new('key', 'wrong_secret')
274
+ df.succeed(token)
275
+
276
+ request_df.errback { |e|
277
+ e.class.should == Xignature::AuthenticationError
278
+ e.message.should =~ /Invalid signature/
279
+ done
280
+ }
281
+ }
282
+ end
283
+ end
284
+ end
285
+ end
@@ -0,0 +1,23 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "xignature/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "xignature"
7
+ s.version = Xignature::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Martyn Loughran", "XN"]
10
+ s.email = ["me@mloughran.com", "christian.trosclair@gmail.com"]
11
+ s.homepage = "http://github.com/xn/xignature"
12
+ s.summary = %q{Simple key/secret based authentication for apis without domain collisions.}
13
+ s.description = %q{Simple key/secret based authentication for apis without domain collisions.}
14
+
15
+ s.files = `git ls-files`.split("\n")
16
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
17
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
18
+ s.require_paths = ["lib"]
19
+
20
+ s.add_dependency "jruby-openssl" if defined?(JRUBY_VERSION)
21
+ s.add_development_dependency "rspec"
22
+ s.add_development_dependency "em-spec"
23
+ end
metadata ADDED
@@ -0,0 +1,90 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: xignature
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.8
5
+ platform: ruby
6
+ authors:
7
+ - Martyn Loughran
8
+ - XN
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2015-09-26 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rspec
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - ">="
19
+ - !ruby/object:Gem::Version
20
+ version: '0'
21
+ type: :development
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ version: '0'
28
+ - !ruby/object:Gem::Dependency
29
+ name: em-spec
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - ">="
33
+ - !ruby/object:Gem::Version
34
+ version: '0'
35
+ type: :development
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ version: '0'
42
+ description: Simple key/secret based authentication for apis without domain collisions.
43
+ email:
44
+ - me@mloughran.com
45
+ - christian.trosclair@gmail.com
46
+ executables: []
47
+ extensions: []
48
+ extra_rdoc_files: []
49
+ files:
50
+ - ".gitignore"
51
+ - ".travis.yml"
52
+ - CHANGELOG.md
53
+ - Gemfile
54
+ - Gemfile.lock
55
+ - LICENSE
56
+ - README.md
57
+ - Rakefile
58
+ - lib/xignature.rb
59
+ - lib/xignature/query_encoder.rb
60
+ - lib/xignature/version.rb
61
+ - spec/spec_helper.rb
62
+ - spec/xignature_spec.rb
63
+ - xignature.gemspec
64
+ homepage: http://github.com/xn/xignature
65
+ licenses: []
66
+ metadata: {}
67
+ post_install_message:
68
+ rdoc_options: []
69
+ require_paths:
70
+ - lib
71
+ required_ruby_version: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ required_rubygems_version: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - ">="
79
+ - !ruby/object:Gem::Version
80
+ version: '0'
81
+ requirements: []
82
+ rubyforge_project:
83
+ rubygems_version: 2.4.6
84
+ signing_key:
85
+ specification_version: 4
86
+ summary: Simple key/secret based authentication for apis without domain collisions.
87
+ test_files:
88
+ - spec/spec_helper.rb
89
+ - spec/xignature_spec.rb
90
+ has_rdoc: