signature-acd 0.1.9

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: bb3d0b4d61f9b917ea4ff7b3767ee3b3fabd276b
4
+ data.tar.gz: 94b38fe0aca3221961c9919dd57e6cda345ac5fd
5
+ SHA512:
6
+ metadata.gz: fc32e214764d82a4e18266a0ec90831c64587507d57fc0c49332449f7dbeb0b4bef74db5be0f7d5b17175c66e0ef6b6a844a5571cdceb2f62c428d7ac3aeee1d
7
+ data.tar.gz: 68f96770675a1ec5ff91e57c09a72b79438ec9f1111e11468a90bc6146496bea6af2f3fc6e0f7420bf4ec3f068a39a4058ef6f4b12bcc8bb756158b23e5fb7e6
data/.gitignore ADDED
@@ -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
data/.travis.yml ADDED
@@ -0,0 +1,18 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.8.7
4
+ - 1.9.2
5
+ - 1.9.3
6
+ - 2.0.0
7
+ - jruby-18mode
8
+ - jruby-19mode
9
+ - rbx-18mode
10
+ - rbx-19mode
11
+ matrix:
12
+ allow_failures:
13
+ - rvm: jruby-18mode
14
+ - rvm: jruby-19mode
15
+ - rvm: rbx-18mode
16
+ - rvm: rbx-19mode
17
+
18
+ script: bundle exec rspec spec
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,41 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ signature (0.1.9)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ bacon (1.2.0)
10
+ coderay (1.1.0)
11
+ diff-lcs (1.2.5)
12
+ em-spec (0.2.6)
13
+ bacon
14
+ eventmachine
15
+ rspec (> 2.6.0)
16
+ test-unit
17
+ eventmachine (1.0.3)
18
+ method_source (0.8.2)
19
+ pry (0.9.12.6)
20
+ coderay (~> 1.0)
21
+ method_source (~> 0.8)
22
+ slop (~> 3.4)
23
+ rspec (2.14.1)
24
+ rspec-core (~> 2.14.0)
25
+ rspec-expectations (~> 2.14.0)
26
+ rspec-mocks (~> 2.14.0)
27
+ rspec-core (2.14.8)
28
+ rspec-expectations (2.14.5)
29
+ diff-lcs (>= 1.1.3, < 2.0)
30
+ rspec-mocks (2.14.6)
31
+ slop (3.5.0)
32
+ test-unit (2.5.5)
33
+
34
+ PLATFORMS
35
+ ruby
36
+
37
+ DEPENDENCIES
38
+ em-spec
39
+ pry
40
+ rspec
41
+ signature!
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.
data/README.md ADDED
@@ -0,0 +1,65 @@
1
+ signature
2
+ =========
3
+
4
+ [![Build Status](https://secure.travis-ci.org/mloughran/signature.png?branch=master)](http://travis-ci.org/mloughran/signature)
5
+
6
+ Examples
7
+ --------
8
+
9
+ Client example
10
+
11
+ ```ruby
12
+ params = {:some => 'parameters'}
13
+ token = Signature::Token.new('my_key', 'my_secret')
14
+ request = Signature::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
+ :query => query_params
20
+ })
21
+ ```
22
+
23
+ `query_params` looks like:
24
+
25
+ ```ruby
26
+ {
27
+ :some => "parameters",
28
+ :auth_timestamp => 1273231888,
29
+ :auth_signature => "28b6bb0f242f71064916fad6ae463fe91f5adc302222dfc02c348ae1941eaf80",
30
+ :auth_version => "1.0",
31
+ :auth_key => "my_key"
32
+ }
33
+
34
+ ```
35
+ Server example (sinatra)
36
+
37
+ ```ruby
38
+ error Signature::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 = Signature::Request.new('POST', env["REQUEST_PATH"], params)
45
+ # This will raise a Signature::AuthenticationError if request does not authenticate
46
+ token = request.authenticate do |key|
47
+ Signature::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.
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
data/lib/signature.rb ADDED
@@ -0,0 +1,4 @@
1
+ require 'signature/exceptions'
2
+ require 'signature/token'
3
+ require 'signature/query_encoder'
4
+ require 'signature/request'
@@ -0,0 +1,3 @@
1
+ module Signature
2
+ class AuthenticationError < RuntimeError; end
3
+ end
@@ -0,0 +1,31 @@
1
+ module Signature
2
+ module Header
3
+ class Request < ::Signature::Request
4
+ AUTH_HEADER_PREFIX = "X-API-"
5
+ AUTH_HEADER_PREFIX_REGEX = /^X\-API\-(AUTH\-.+)$/
6
+
7
+ def self.parse_headers headers={}
8
+ hh = {}
9
+ headers.each do |k,v|
10
+ if match = k.upcase.match(AUTH_HEADER_PREFIX_REGEX)
11
+ hh[match[1].downcase.gsub!('-', '_')] = v
12
+ end
13
+ end
14
+ hh
15
+ end
16
+
17
+ def initialize method, path, query={}, headers={}
18
+ auth_hash = self.class.parse_headers(headers)
19
+ super(method, path, query.merge(auth_hash))
20
+ end
21
+
22
+ def sign token
23
+ auth_hash = super(token)
24
+ auth_hash.inject({}) do |memo, (k,v)|
25
+ memo["#{AUTH_HEADER_PREFIX}#{k.to_s.upcase.gsub('_', '-')}"] = v
26
+ memo
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,47 @@
1
+ module Signature
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,242 @@
1
+ require 'openssl'
2
+ require 'json'
3
+
4
+ module Signature
5
+ class Request
6
+ AUTH_HEADER_PREFIX = "X-API-"
7
+ AUTH_HEADER_PREFIX_REGEX = /^X\-API\-(AUTH\-.+)$/
8
+
9
+ attr_accessor :path, :query_hash
10
+
11
+ include QueryEncoder
12
+
13
+ # http://www.w3.org/TR/NOTE-datetime
14
+ ISO8601 = "%Y-%m-%dT%H:%M:%SZ"
15
+
16
+ def initialize(method, path, query, headers=nil)
17
+ raise ArgumentError, "Expected string" unless path.kind_of?(String)
18
+ raise ArgumentError, "Expected hash" unless query.kind_of?(Hash)
19
+
20
+ query_hash = {}
21
+
22
+ auth_hash = self.class.parse_headers(headers) if headers
23
+ auth_hash ||= {}
24
+
25
+ query.each do |key, v|
26
+ k = key.to_s.downcase
27
+ k[0..4] == 'auth_' ? auth_hash[k] = v : query_hash[k] = v
28
+ end
29
+
30
+ @method = method.upcase
31
+ @path, @query_hash, @auth_hash = path, query_hash, auth_hash
32
+ @signed = false
33
+ end
34
+
35
+ def self.parse_headers headers={}
36
+ hh = {}
37
+ headers.each do |k,v|
38
+ if match = k.upcase.match(AUTH_HEADER_PREFIX_REGEX)
39
+ hh[match[1].downcase.gsub!('-', '_')] = v
40
+ end
41
+ end
42
+ hh
43
+ end
44
+
45
+ # Sign the request with the given token, and return the computed
46
+ # authentication parameters
47
+ #
48
+ def sign(token)
49
+ @auth_hash = {
50
+ :auth_version => "1.0",
51
+ :auth_key => token.key,
52
+ :auth_timestamp => Time.now.to_i.to_s
53
+ }
54
+ @auth_hash[:auth_signature] = signature(token)
55
+
56
+ @signed = true
57
+
58
+ return @auth_hash
59
+ end
60
+
61
+ # Authenticates the request with a token
62
+ #
63
+ # Raises an AuthenticationError if the request is invalid.
64
+ # AuthenticationError exception messages are designed to be exposed to API
65
+ # consumers, and should help them correct errors generating signatures
66
+ #
67
+ # Timestamp: Unless timestamp_grace is set to nil (which allows this check
68
+ # to be skipped), AuthenticationError will be raised if the timestamp is
69
+ # missing or further than timestamp_grace period away from the real time
70
+ # (defaults to 10 minutes)
71
+ #
72
+ # Signature: Raises AuthenticationError if the signature does not match
73
+ # the computed HMAC. The error contains a hint for how to sign.
74
+ #
75
+ def authenticate_by_token!(token, timestamp_grace = 600)
76
+ # Validate that your code has provided a valid token. This does not
77
+ # raise an AuthenticationError since passing tokens with empty secret is
78
+ # a code error which should be fixed, not reported to the API's consumer
79
+ if token.secret.nil? || token.secret.empty?
80
+ raise "Provided token is missing secret"
81
+ end
82
+
83
+ validate_version!
84
+ validate_timestamp!(timestamp_grace)
85
+ validate_signature!(token)
86
+ true
87
+ end
88
+
89
+ # Authenticate the request with a token, but rather than raising an
90
+ # exception if the request is invalid, simply returns false
91
+ #
92
+ def authenticate_by_token(token, timestamp_grace = 600)
93
+ authenticate_by_token!(token, timestamp_grace)
94
+ rescue AuthenticationError
95
+ false
96
+ end
97
+
98
+ # Authenticate a request
99
+ #
100
+ # Takes a block which will be called with the auth_key from the request,
101
+ # and which should return a Signature::Token (or nil if no token can be
102
+ # found for the key)
103
+ #
104
+ # Raises errors in the same way as authenticate_by_token!
105
+ #
106
+ def authenticate(timestamp_grace = 600)
107
+ raise ArgumentError, "Block required" unless block_given?
108
+ key = @auth_hash['auth_key']
109
+ raise AuthenticationError, "Missing parameter: auth_key" unless key
110
+ token = yield key
111
+ unless token
112
+ raise AuthenticationError, "Unknown auth_key"
113
+ end
114
+ authenticate_by_token!(token, timestamp_grace)
115
+ return token
116
+ end
117
+
118
+ # Authenticate a request asynchronously
119
+ #
120
+ # This method is useful it you're running a server inside eventmachine and
121
+ # need to lookup the token asynchronously.
122
+ #
123
+ # The block is passed an auth key and a deferrable which should succeed
124
+ # with the token, or fail if the token cannot be found
125
+ #
126
+ # This method returns a deferrable which succeeds with the valid token, or
127
+ # fails with an AuthenticationError which can be used to pass the error
128
+ # back to the user
129
+ #
130
+ def authenticate_async(timestamp_grace = 600)
131
+ raise ArgumentError, "Block required" unless block_given?
132
+ df = EM::DefaultDeferrable.new
133
+
134
+ key = @auth_hash['auth_key']
135
+
136
+ unless key
137
+ df.fail(AuthenticationError.new("Missing parameter: auth_key"))
138
+ return
139
+ end
140
+
141
+ token_df = yield key
142
+ token_df.callback { |token|
143
+ begin
144
+ authenticate_by_token!(token, timestamp_grace)
145
+ df.succeed(token)
146
+ rescue AuthenticationError => e
147
+ df.fail(e)
148
+ end
149
+ }
150
+ token_df.errback {
151
+ df.fail(AuthenticationError.new("Unknown auth_key"))
152
+ }
153
+ ensure
154
+ return df
155
+ end
156
+
157
+ # Expose the authentication parameters for a signed request
158
+ #
159
+ def auth_hash
160
+ raise "Request not signed" unless @signed
161
+ @auth_hash
162
+ end
163
+
164
+ # Query parameters merged with the computed authentication parameters
165
+ #
166
+ def signed_params
167
+ @query_hash.merge(auth_hash)
168
+ end
169
+
170
+ private
171
+
172
+ def signature(token)
173
+ digest = OpenSSL::Digest::SHA256.new
174
+ OpenSSL::HMAC.hexdigest(digest, token.secret, string_to_sign)
175
+ end
176
+
177
+ def string_to_sign
178
+ auth_hash = @auth_hash || {}
179
+ {
180
+ api: {
181
+ method: @method,
182
+ path: @path,
183
+ timestamp: auth_hash[:auth_timestamp] || auth_hash["auth_timestamp"],
184
+ version: auth_hash[:auth_version] || auth_hash["auth_version"]
185
+ },
186
+ params: parameters_sorted
187
+ }.to_json
188
+ end
189
+
190
+ def parameters_sorted
191
+ shash = {}
192
+ @query_hash.sort.map do |k, v|
193
+ shash[k.to_s.downcase] = v
194
+ end
195
+ shash
196
+ end
197
+
198
+ # def parameter_string
199
+ # param_hash = @query_hash.merge(@auth_hash || {})
200
+ #
201
+ # # Convert keys to lowercase strings
202
+ # hash = {}; param_hash.each { |k,v| hash[k.to_s.downcase] = v }
203
+ #
204
+ # # Exclude signature from signature generation!
205
+ # hash.delete("auth_signature")
206
+ #
207
+ # hash.sort.map do |k, v|
208
+ # QueryEncoder.encode_param_without_escaping(k, v)
209
+ # end.join('&')
210
+ # end
211
+
212
+ def validate_version!
213
+ version = @auth_hash["auth_version"]
214
+ raise AuthenticationError, "Version required" unless version
215
+ raise AuthenticationError, "Version not supported" unless version == '1.0'
216
+ end
217
+
218
+ def validate_timestamp!(grace)
219
+ return true if grace.nil?
220
+
221
+ timestamp = @auth_hash["auth_timestamp"]
222
+ error = (timestamp.to_i - Time.now.to_i).abs
223
+ raise AuthenticationError, "Timestamp required" unless timestamp
224
+ if error >= grace
225
+ raise AuthenticationError, "Timestamp expired: Given timestamp "\
226
+ "(#{Time.at(timestamp.to_i).utc.strftime(ISO8601)}) "\
227
+ "not within #{grace}s of server time "\
228
+ "(#{Time.now.utc.strftime(ISO8601)})"
229
+ end
230
+ return true
231
+ end
232
+
233
+ def validate_signature!(token)
234
+ unless @auth_hash["auth_signature"] == signature(token)
235
+ raise AuthenticationError, "Invalid signature: you should have "\
236
+ "sent HmacSHA256Hex(#{string_to_sign.inspect}, your_secret_key)"\
237
+ ", but you sent #{@auth_hash["auth_signature"].inspect}"
238
+ end
239
+ return true
240
+ end
241
+ end
242
+ end
@@ -0,0 +1,13 @@
1
+ module Signature
2
+ class Token
3
+ attr_reader :key, :secret
4
+
5
+ def initialize(key, secret)
6
+ @key, @secret = key, secret
7
+ end
8
+
9
+ def sign(request)
10
+ request.sign(self)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,3 @@
1
+ module Signature
2
+ VERSION = "0.1.9"
3
+ end
data/signature.gemspec ADDED
@@ -0,0 +1,24 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "signature/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "signature-acd"
7
+ s.version = Signature::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Thomas Hanson", "CJ Lazell", "Thomas Hanson"]
10
+ s.email = ["thanson@acdcorp.com"]
11
+ s.homepage = "http://github.com/acdcorp/signature"
12
+ s.summary = %q{Simple key/secret based authentication for apis}
13
+ s.description = %q{Simple key/secret based authentication for apis}
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
+ s.add_development_dependency "pry"
24
+ end
@@ -0,0 +1,332 @@
1
+ require File.expand_path('../spec_helper', __FILE__)
2
+
3
+ describe Signature do
4
+ before :each do
5
+ Time.stub!(:now).and_return(Time.at(1234))
6
+
7
+ @token = Signature::Token.new('key', 'secret')
8
+
9
+ @request = Signature::Request.new('POST', '/some/path', {
10
+ "query" => "params",
11
+ "go" => "here"
12
+ })
13
+
14
+ @headers = {
15
+ "X-API-AUTH-VERSION" => "1.0",
16
+ "X-API-AUTH-KEY" => "key",
17
+ "X-API-AUTH-SIGNATURE" => "9a86683edaf7db6782ac2d78d1958f3d53fa6aeb4c80542335ac64ee5e926411",
18
+ "X-API-AUTH-TIMESTAMP" => "3456"
19
+ }
20
+ end
21
+
22
+ describe "generating signatures" do
23
+ before :each do
24
+ @signature = "9a86683edaf7db6782ac2d78d1958f3d53fa6aeb4c80542335ac64ee5e926411"
25
+ end
26
+
27
+ it "should generate signature correctly" do
28
+ @request.sign(@token)
29
+ string = @request.send(:string_to_sign)
30
+ # string.should == "POST\n/some/path\nauth_key=key&auth_timestamp=1234&auth_version=1.0&go=here&query=params"
31
+ string.should == {
32
+ api: {
33
+ method: "POST",
34
+ path: "/some/path",
35
+ timestamp: "1234",
36
+ version: "1.0"
37
+ },
38
+ params: {
39
+ go: "here",
40
+ query: "params"
41
+ }
42
+ }.to_json
43
+
44
+ # "{\"api\":{\"method\":\"POST\",\"path\":\"/some/path\",\"timestamp\":null,\"version\":null},\"params\":{\"query\":\"params\",\"go\":\"here\"}}"
45
+
46
+ digest = OpenSSL::Digest::SHA256.new
47
+ signature = OpenSSL::HMAC.hexdigest(digest, @token.secret, string)
48
+ signature.should == @signature
49
+ end
50
+
51
+ it "should make auth_hash available after request is signed" do
52
+ @request.query_hash = {
53
+ "query" => "params"
54
+ }
55
+ lambda {
56
+ @request.auth_hash
57
+ }.should raise_error('Request not signed')
58
+
59
+ @request.sign(@token)
60
+ @request.auth_hash.should == {
61
+ :auth_signature => "e4b1eee7fbe9beb5aebcd918b45d53c76a69157e8e3575d636a370b5afb3c662",
62
+ :auth_version => "1.0",
63
+ :auth_key => "key",
64
+ :auth_timestamp => '1234'
65
+ }
66
+ end
67
+
68
+ it "should cope with symbol keys" do
69
+ @request.query_hash = {
70
+ :query => "params",
71
+ :go => "here"
72
+ }
73
+ @request.sign(@token)[:auth_signature].should == @signature
74
+ end
75
+
76
+ it "should cope with upcase keys (keys are lowercased before signing)" do
77
+ @request.query_hash = {
78
+ "Query" => "params",
79
+ "GO" => "here"
80
+ }
81
+ @request.sign(@token)[:auth_signature].should == @signature
82
+ end
83
+
84
+ it "should generate correct string when query hash contains array" do
85
+ @request.query_hash = {
86
+ "things" => ["thing1", "thing2"]
87
+ }
88
+ @request.send(:string_to_sign).should == "{\"api\":{\"method\":\"POST\",\"path\":\"/some/path\",\"timestamp\":null,\"version\":null},\"params\":{\"things\":[\"thing1\",\"thing2\"]}}"
89
+ end
90
+
91
+ # This may well change in auth version 2
92
+ it "should not escape keys or values in the query string" do
93
+ @request.query_hash = {
94
+ "key;" => "value@"
95
+ }
96
+ @request.send(:string_to_sign).should == "{\"api\":{\"method\":\"POST\",\"path\":\"/some/path\",\"timestamp\":null,\"version\":null},\"params\":{\"key;\":\"value@\"}}"
97
+ end
98
+
99
+ it "should cope with requests where the value is nil (antiregression)" do
100
+ @request.query_hash = {
101
+ "key" => nil
102
+ }
103
+ @request.send(:string_to_sign).should == "{\"api\":{\"method\":\"POST\",\"path\":\"/some/path\",\"timestamp\":null,\"version\":null},\"params\":{\"key\":null}}"
104
+ end
105
+
106
+ it "should use the path to generate signature" do
107
+ @request.path = '/some/other/path'
108
+ @request.sign(@token)[:auth_signature].should_not == @signature
109
+ end
110
+
111
+ it "should use the query string keys to generate signature" do
112
+ @request.query_hash = {
113
+ "other" => "query"
114
+ }
115
+ @request.sign(@token)[:auth_signature].should_not == @signature
116
+ end
117
+
118
+ it "should use the query string values to generate signature" do
119
+ @request.query_hash = {
120
+ "key" => "notfoo",
121
+ "other" => 'bar'
122
+ }
123
+ @request.sign(@token)[:signature].should_not == @signature
124
+ end
125
+
126
+ it "should accept authentication parameters via HTTP headers" do
127
+ request = Signature::Request.new('POST', '/some/path/with/headers', {
128
+ "query" => "params",
129
+ "go" => "here"
130
+ }, @headers)
131
+
132
+ string = request.send(:string_to_sign)
133
+ string.should == {
134
+ api: {
135
+ method: "POST",
136
+ path: "/some/path/with/headers",
137
+ timestamp: "3456",
138
+ version: "1.0"
139
+ },
140
+ params: {
141
+ go: "here",
142
+ query: "params"
143
+ }
144
+ }.to_json
145
+
146
+ digest = OpenSSL::Digest::SHA256.new
147
+ signature = OpenSSL::HMAC.hexdigest(digest, @token.secret, string)
148
+ signature.should == "002b9f68b311a172995cb2f9c8ce3954b5adb37c721d501afae55a150f2f608d"
149
+ end
150
+ end
151
+
152
+ describe "verification" do
153
+ before :each do
154
+ @request.sign(@token)
155
+ @params = @request.signed_params
156
+ end
157
+
158
+ it "should verify requests" do
159
+ request = Signature::Request.new('POST', '/some/path', @params)
160
+ request.authenticate_by_token(@token).should == true
161
+ end
162
+
163
+ it "should raise error if signature is not correct" do
164
+ @params[:auth_signature] = 'asdf'
165
+ request = Signature::Request.new('POST', '/some/path', @params)
166
+ lambda {
167
+ request.authenticate_by_token!(@token)
168
+ }.should raise_error('Invalid signature: you should have sent HmacSHA256Hex("{\"api\":{\"method\":\"POST\",\"path\":\"/some/path\",\"timestamp\":\"1234\",\"version\":\"1.0\"},\"params\":{\"go\":\"here\",\"query\":\"params\"}}", your_secret_key), but you sent "asdf"')
169
+ end
170
+
171
+ it "should raise error if timestamp not available" do
172
+ @params.delete(:auth_timestamp)
173
+ request = Signature::Request.new('POST', '/some/path', @params)
174
+ lambda {
175
+ request.authenticate_by_token!(@token)
176
+ }.should raise_error('Timestamp required')
177
+ end
178
+
179
+ it "should raise error if timestamp has expired (default of 600s)" do
180
+ request = Signature::Request.new('POST', '/some/path', @params)
181
+ Time.stub!(:now).and_return(Time.at(1234 + 599))
182
+ request.authenticate_by_token!(@token).should == true
183
+ Time.stub!(:now).and_return(Time.at(1234 - 599))
184
+ request.authenticate_by_token!(@token).should == true
185
+ Time.stub!(:now).and_return(Time.at(1234 + 600))
186
+ lambda {
187
+ request.authenticate_by_token!(@token)
188
+ }.should raise_error("Timestamp expired: Given timestamp (1970-01-01T00:20:34Z) not within 600s of server time (1970-01-01T00:30:34Z)")
189
+ Time.stub!(:now).and_return(Time.at(1234 - 600))
190
+ lambda {
191
+ request.authenticate_by_token!(@token)
192
+ }.should raise_error("Timestamp expired: Given timestamp (1970-01-01T00:20:34Z) not within 600s of server time (1970-01-01T00:10:34Z)")
193
+ end
194
+
195
+ it "should be possible to customize the timeout grace period" do
196
+ grace = 10
197
+ request = Signature::Request.new('POST', '/some/path', @params)
198
+ Time.stub!(:now).and_return(Time.at(1234 + grace - 1))
199
+ request.authenticate_by_token!(@token, grace).should == true
200
+ Time.stub!(:now).and_return(Time.at(1234 + grace))
201
+ lambda {
202
+ request.authenticate_by_token!(@token, grace)
203
+ }.should raise_error("Timestamp expired: Given timestamp (1970-01-01T00:20:34Z) not within 10s of server time (1970-01-01T00:20:44Z)")
204
+ end
205
+
206
+ it "should be possible to skip timestamp check by passing nil" do
207
+ request = Signature::Request.new('POST', '/some/path', @params)
208
+ Time.stub!(:now).and_return(Time.at(1234 + 1000))
209
+ request.authenticate_by_token!(@token, nil).should == true
210
+ end
211
+
212
+ it "should check that auth_version is supplied" do
213
+ @params.delete(:auth_version)
214
+ request = Signature::Request.new('POST', '/some/path', @params)
215
+ lambda {
216
+ request.authenticate_by_token!(@token)
217
+ }.should raise_error('Version required')
218
+ end
219
+
220
+ it "should check that auth_version equals 1.0" do
221
+ @params[:auth_version] = '1.1'
222
+ request = Signature::Request.new('POST', '/some/path', @params)
223
+ lambda {
224
+ request.authenticate_by_token!(@token)
225
+ }.should raise_error('Version not supported')
226
+ end
227
+
228
+ it "should validate that the provided token has a non-empty secret" do
229
+ token = Signature::Token.new('key', '')
230
+ request = Signature::Request.new('POST', '/some/path', @params)
231
+
232
+ lambda {
233
+ request.authenticate_by_token!(token)
234
+ }.should raise_error('Provided token is missing secret')
235
+ end
236
+
237
+ describe "when used with optional block" do
238
+ it "should optionally take a block which yields the signature" do
239
+ request = Signature::Request.new('POST', '/some/path', @params)
240
+ request.authenticate do |key|
241
+ key.should == @token.key
242
+ @token
243
+ end.should == @token
244
+ end
245
+
246
+ it "should raise error if no auth_key supplied to request" do
247
+ @params.delete(:auth_key)
248
+ request = Signature::Request.new('POST', '/some/path', @params)
249
+ lambda {
250
+ request.authenticate { |key| nil }
251
+ }.should raise_error('Missing parameter: auth_key')
252
+ end
253
+
254
+ it "should raise error if block returns nil (i.e. key doesn't exist)" do
255
+ request = Signature::Request.new('POST', '/some/path', @params)
256
+ lambda {
257
+ request.authenticate { |key| nil }
258
+ }.should raise_error('Unknown auth_key')
259
+ end
260
+
261
+ it "should raise unless block given" do
262
+ request = Signature::Request.new('POST', '/some/path', @params)
263
+ lambda {
264
+ request.authenticate
265
+ }.should raise_error(ArgumentError, "Block required")
266
+ end
267
+ end
268
+
269
+ describe "authenticate_async" do
270
+ include EM::SpecHelper
271
+ default_timeout 1
272
+
273
+ it "returns a deferrable which succeeds if authentication passes" do
274
+ request = Signature::Request.new('POST', '/some/path', @params)
275
+ em {
276
+ df = EM::DefaultDeferrable.new
277
+
278
+ request_df = request.authenticate_async do |key|
279
+ df
280
+ end
281
+
282
+ df.succeed(@token)
283
+
284
+ request_df.callback { |token|
285
+ token.should == @token
286
+ done
287
+ }
288
+ }
289
+ end
290
+
291
+ it "returns a deferrable which fails if block df fails" do
292
+ request = Signature::Request.new('POST', '/some/path', @params)
293
+ em {
294
+ df = EM::DefaultDeferrable.new
295
+
296
+ request_df = request.authenticate_async do |key|
297
+ df
298
+ end
299
+
300
+ df.fail()
301
+
302
+ request_df.errback { |e|
303
+ e.class.should == Signature::AuthenticationError
304
+ e.message.should == 'Unknown auth_key'
305
+ done
306
+ }
307
+ }
308
+ end
309
+
310
+ it "returns a deferrable which fails if request does not validate" do
311
+ request = Signature::Request.new('POST', '/some/path', @params)
312
+ em {
313
+ df = EM::DefaultDeferrable.new
314
+
315
+ request_df = request.authenticate_async do |key|
316
+ df
317
+ end
318
+
319
+ token = Signature::Token.new('key', 'wrong_secret')
320
+ df.succeed(token)
321
+
322
+ request_df.errback { |e|
323
+ e.class.should == Signature::AuthenticationError
324
+ e.message.should =~ /Invalid signature/
325
+ done
326
+ }
327
+ }
328
+ end
329
+ end
330
+
331
+ end
332
+ end
@@ -0,0 +1,10 @@
1
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+ require 'signature'
3
+
4
+ require 'rspec'
5
+ require 'em-spec/rspec'
6
+ require 'pry'
7
+
8
+ RSpec.configure do |config|
9
+
10
+ end
metadata ADDED
@@ -0,0 +1,105 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: signature-acd
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.9
5
+ platform: ruby
6
+ authors:
7
+ - Thomas Hanson
8
+ - CJ Lazell
9
+ - Thomas Hanson
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+ date: 2014-05-20 00:00:00.000000000 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: rspec
17
+ requirement: !ruby/object:Gem::Requirement
18
+ requirements:
19
+ - - ">="
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ version: '0'
29
+ - !ruby/object:Gem::Dependency
30
+ name: em-spec
31
+ requirement: !ruby/object:Gem::Requirement
32
+ requirements:
33
+ - - ">="
34
+ - !ruby/object:Gem::Version
35
+ version: '0'
36
+ type: :development
37
+ prerelease: false
38
+ version_requirements: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: '0'
43
+ - !ruby/object:Gem::Dependency
44
+ name: pry
45
+ requirement: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: '0'
50
+ type: :development
51
+ prerelease: false
52
+ version_requirements: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: '0'
57
+ description: Simple key/secret based authentication for apis
58
+ email:
59
+ - thanson@acdcorp.com
60
+ executables: []
61
+ extensions: []
62
+ extra_rdoc_files: []
63
+ files:
64
+ - ".gitignore"
65
+ - ".travis.yml"
66
+ - Gemfile
67
+ - Gemfile.lock
68
+ - LICENSE
69
+ - README.md
70
+ - Rakefile
71
+ - lib/signature.rb
72
+ - lib/signature/exceptions.rb
73
+ - lib/signature/header.rb
74
+ - lib/signature/query_encoder.rb
75
+ - lib/signature/request.rb
76
+ - lib/signature/token.rb
77
+ - lib/signature/version.rb
78
+ - signature.gemspec
79
+ - spec/signature_spec.rb
80
+ - spec/spec_helper.rb
81
+ homepage: http://github.com/acdcorp/signature
82
+ licenses: []
83
+ metadata: {}
84
+ post_install_message:
85
+ rdoc_options: []
86
+ require_paths:
87
+ - lib
88
+ required_ruby_version: !ruby/object:Gem::Requirement
89
+ requirements:
90
+ - - ">="
91
+ - !ruby/object:Gem::Version
92
+ version: '0'
93
+ required_rubygems_version: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - ">="
96
+ - !ruby/object:Gem::Version
97
+ version: '0'
98
+ requirements: []
99
+ rubyforge_project:
100
+ rubygems_version: 2.2.2
101
+ signing_key:
102
+ specification_version: 4
103
+ summary: Simple key/secret based authentication for apis
104
+ test_files: []
105
+ has_rdoc: