signature-acd 0.1.9

Sign up to get free protection for your applications and to get access to all the features.
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: