xignature 0.1.8

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