crashlog-auth-hmac 1.1.2

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color --order rand
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in crashlog-faraday-auth-hmac.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Ivan Vanderbyl
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,72 @@
1
+ # Crashlog::AuthHMAC
2
+
3
+ auth-hmac is a Ruby implementation of HMAC based authentication of HTTP requests.
4
+
5
+ HMAC authentication involves a client and server having a shared secret key. When sending the request the client, signs the request using the secret key. This involves building a canonical representation of the request and then generating a HMAC of the request using the secret. The generated HMAC is then sent as part of the request.
6
+
7
+ When the server receives the request it builds the same canonical representation and generates a HMAC using it’s copy of the secret key, if the HMAC produced by the server matches the HMAC sent by the client, the server can be assured that the client also possesses the shared secret key.
8
+
9
+ HMAC based authentication also provides message integrity checking because the HMAC is based on a combination of the shared secret and the content of the request. So if any part of the request that is used to build the canonical representation is modified by a malicious party or in transit the authentication will then fail.
10
+
11
+ AuthHMAC was built to support authentication between various applications build by Peerworks.
12
+
13
+ AuthHMAC is loosely based on the Amazon Web Services authentication scheme but without the Amazon specific components, i.e. it is HMAC for the rest of us.
14
+
15
+
16
+ ### Legacy note:
17
+
18
+ This gem is based largely on the original `auth-hmac` gem by Sean Geoghegan, however
19
+ we have removed the Rails support as we use it exclusively within our CrashLog Ruby Gem
20
+ which must maintain compatibility with many versions of Rails.
21
+
22
+ ## Installation
23
+
24
+ Add this line to your application's Gemfile:
25
+
26
+ gem 'crashlog-auth-hmac'
27
+
28
+ And then execute:
29
+
30
+ $ bundle
31
+
32
+ Or install it yourself as:
33
+
34
+ $ gem install crashlog-auth-hmac
35
+
36
+ ## Usage
37
+
38
+ The simplest way to use AuthHMAC is with the `CrashLog::AuthHMAC.sign!` and
39
+ `CrashLog::AuthHMAC#authenticate?` methods.
40
+
41
+ `CrashLog::AuthHMAC.sign!` takes a HTTP request object, an access id and a secret key and signs the request with the access_id and secret key.
42
+
43
+ The HTTP request object can be a Net::HTTP::HTTPRequest object, a CGI::Request object or a Webrick HTTP request object. AuthHMAC will do its best to figure out which type it is an handle it accordingly.
44
+
45
+ The access_id is used to identify the secret key that was used to sign the request. Think of it as like a user name, it allows you to hand out different keys to different clients and authenticate each of them individually. The access_id is sent in the clear so you should avoid making it an important string.
46
+
47
+ The secret key is the shared secret between the client and the server. You should make this sufficiently random so that is can’t be guessed or exposed to dictionary attacks. The follow code will give you a pretty good secret key:
48
+
49
+ random = File.read(‘/dev/random’, 512) secret_key = Base64.encode64(Digest::SHA2.new(512).digest(random)) On the server side you can then authenticate these requests using the AuthHMAC.authenticated? method. This takes the same arguments as the sign! method but returns true if the request has been signed with the access id and secret or false if it hasn’t.
50
+
51
+ If you have more than one set of credentials you might find it useful to create an instance of the AuthHMAC class, passing your credentials as a Hash of access id => secret keys, like so:
52
+
53
+ @authhmac = AuthHMAC.new(‘access_id1’ => ‘secret1’, ‘access_id2’ => ‘secret2’) You can then use the instance methods of the @authhmac object to sign and authenticate requests, for example:
54
+
55
+ @authhmac.sign!(request, “access_id1”) will sign request with “access_id1” and it’s corresponding secret key. Similarly authentication is done like so:
56
+
57
+ @authhmac.authenticated?(request)
58
+ which will return true if the request has been signed with one of the access id and secret key pairs provided in the constructor.
59
+
60
+ ## Contributing
61
+
62
+ 1. Fork it
63
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
64
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
65
+ 4. Push to the branch (`git push origin my-new-feature`)
66
+ 5. Create new Pull Request
67
+
68
+ ## Contributors
69
+
70
+ - This gem is based on the [original work by Sean Geoghegan](https://github.com/seangeo/auth-hmac)
71
+ - Ivan Vanderbyl
72
+
data/Rakefile ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
3
+
4
+ $: << 'lib'
5
+
6
+ require "crash_log/auth_hmac"
7
+
8
+ require 'rake'
9
+ require "rspec/core/rake_task"
10
+
11
+ desc "Run all examples"
12
+ RSpec::Core::RakeTask.new
13
+
14
+ task :default => :spec
15
+
@@ -0,0 +1,17 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/crash_log/auth_hmac/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Ivan Vanderbyl"]
6
+ gem.email = ["ivan@crashlog.io"]
7
+ gem.description = %q{A Ruby Gem for authenticating HTTP requests using a HMAC}
8
+ gem.summary = %q{A Ruby Gem for authenticating HTTP requests using a HMAC}
9
+ gem.homepage = "http://crashlog.io"
10
+
11
+ gem.files = `git ls-files`.split($\)
12
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
13
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
+ gem.name = "crashlog-auth-hmac"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = CrashLog::AuthHMAC::VERSION
17
+ end
data/lib/crash_log.rb ADDED
@@ -0,0 +1,3 @@
1
+ module CrashLog
2
+ autoload :AuthHMAC, 'crash_log/auth_hmac'
3
+ end
@@ -0,0 +1,237 @@
1
+ module CrashLog
2
+ class AuthHMAC
3
+ require 'openssl'
4
+ require 'base64'
5
+
6
+ # This module provides a HMAC Authentication method for HTTP requests. It should work with
7
+ # net/http request classes and CGIRequest classes and hence Rails.
8
+ #
9
+ # It is loosely based on the Amazon Web Services Authentication mechanism but
10
+ # generalized to be useful to any application that requires HMAC based authentication.
11
+ # As a result of the generalization, it won't work with AWS because it doesn't support
12
+ # the Amazon extension headers.
13
+ #
14
+ # === References
15
+ # Cryptographic Hash functions:: http://en.wikipedia.org/wiki/Cryptographic_hash_function
16
+ # SHA-1 Hash function:: http://en.wikipedia.org/wiki/SHA-1
17
+ # HMAC algorithm:: http://en.wikipedia.org/wiki/HMAC
18
+ # RFC 2104:: http://tools.ietf.org/html/rfc2104
19
+ #
20
+ module Headers
21
+ # Gets the headers for a request.
22
+ #
23
+ # Attempts to deal with known HTTP header representations in Ruby.
24
+ # Currently handles net/http and Rails.
25
+ #
26
+ def headers(request)
27
+ if request.respond_to?(:headers)
28
+ request.headers
29
+ elsif request.respond_to?(:[])
30
+ request
31
+ else
32
+ raise ArgumentError, "Don't know how to get the headers from #{request.inspect}"
33
+ end
34
+ end
35
+
36
+ def find_header(keys, headers)
37
+ keys.map do |key|
38
+ headers[key]
39
+ end.compact.first
40
+ end
41
+ end
42
+
43
+ include Headers
44
+
45
+ # Build a Canonical String for a HTTP request.
46
+ #
47
+ # A Canonical String has the following format:
48
+ #
49
+ # CanonicalString = HTTP-Verb + "\n" +
50
+ # Content-Type + "\n" +
51
+ # Content-MD5 + "\n" +
52
+ # Date + "\n" +
53
+ # request-uri;
54
+ #
55
+ #
56
+ # If the Date header doesn't exist, one will be generated since
57
+ # Net/HTTP will generate one if it doesn't exist and it will be
58
+ # used on the server side to do authentication.
59
+ #
60
+ class CanonicalString < String # :nodoc:
61
+ include Headers
62
+
63
+ def initialize(request)
64
+ self << request_method(request) + "\n"
65
+ self << header_values(headers(request)) + "\n"
66
+ self << request_path(request)
67
+ end
68
+
69
+ private
70
+
71
+ def request_method(request)
72
+ if request.respond_to?(:request_method) && request.request_method.is_a?(String)
73
+ request.request_method
74
+ elsif request.respond_to?(:method) && request.method.is_a?(String)
75
+ request.method
76
+ elsif request.respond_to?(:env) && request.env
77
+ request.env['REQUEST_METHOD']
78
+ else
79
+ raise ArgumentError, "Don't know how to get the request method from #{request.inspect}"
80
+ end
81
+ end
82
+
83
+ def header_values(headers)
84
+ [ content_type(headers),
85
+ content_md5(headers),
86
+ (date(headers) or headers['Date'] = Time.now.utc.httpdate)
87
+ ].join("\n")
88
+ end
89
+
90
+ def content_type(headers)
91
+ find_header(%w(CONTENT-TYPE CONTENT_TYPE HTTP_CONTENT_TYPE), headers)
92
+ end
93
+
94
+ def date(headers)
95
+ find_header(%w(DATE HTTP_DATE), headers)
96
+ end
97
+
98
+ def content_md5(headers)
99
+ find_header(%w(CONTENT-MD5 CONTENT_MD5), headers)
100
+ end
101
+
102
+ def request_path(request)
103
+ # Try unparsed_uri in case it is a Webrick request
104
+ path = if request.respond_to?(:unparsed_uri)
105
+ request.unparsed_uri
106
+ else
107
+ request.path
108
+ end
109
+
110
+ path[/^[^?]*/]
111
+ end
112
+ end
113
+
114
+ @@default_signature_class = CanonicalString
115
+
116
+ # Create an AuthHMAC instance using the given credential store
117
+ #
118
+ # Credential Store:
119
+ # * Credential store must respond to the [] method and return a secret for access key id
120
+ #
121
+ # Options:
122
+ # Override default options
123
+ # * <tt>:service_id</tt> - Service ID used in the AUTHORIZATION header string. Default is AuthHMAC.
124
+ # * <tt>:signature_method</tt> - Proc object that takes request and produces the signature string
125
+ # used for authentication. Default is CanonicalString.
126
+ # Examples:
127
+ # my_hmac = AuthHMAC.new('access_id1' => 'secret1', 'access_id2' => 'secret2')
128
+ #
129
+ # cred_store = { 'access_id1' => 'secret1', 'access_id2' => 'secret2' }
130
+ # options = { :service_id => 'MyApp', :signature_method => lambda { |r| MyRequestString.new(r) } }
131
+ # my_hmac = AuthHMAC.new(cred_store, options)
132
+ #
133
+ def initialize(credential_store, options = nil)
134
+ @credential_store = credential_store
135
+
136
+ # Defaults
137
+ @service_id = self.class.name.split(/::/).last
138
+ @signature_class = @@default_signature_class
139
+
140
+ unless options.nil?
141
+ @service_id = options[:service_id] if options.key?(:service_id)
142
+ @signature_class = options[:signature] if options.key?(:signature) && options[:signature].is_a?(Class)
143
+ end
144
+
145
+ @signature_method = lambda { |r| @signature_class.send(:new, r) }
146
+ end
147
+
148
+ # Generates canonical signing string for given request
149
+ #
150
+ # Supports same options as AuthHMAC.initialize for overriding service_id and
151
+ # signature method.
152
+ #
153
+ def AuthHMAC.canonical_string(request, options = nil)
154
+ self.new(nil, options).canonical_string(request)
155
+ end
156
+
157
+ # Generates signature string for a given secret
158
+ #
159
+ # Supports same options as AuthHMAC.initialize for overriding service_id and
160
+ # signature method.
161
+ #
162
+ def AuthHMAC.signature(request, secret, options = nil)
163
+ self.new(nil, options).signature(request, secret)
164
+ end
165
+
166
+ # Signs a request using a given access key id and secret.
167
+ #
168
+ # Supports same options as AuthHMAC.initialize for overriding service_id and
169
+ # signature method.
170
+ #
171
+ def AuthHMAC.sign!(request, access_key_id, secret, options = nil)
172
+ credentials = { access_key_id => secret }
173
+ self.new(credentials, options).sign!(request, access_key_id)
174
+ end
175
+
176
+ # Authenticates a request using HMAC
177
+ #
178
+ # Supports same options as AuthHMAC.initialize for overriding service_id and
179
+ # signature method.
180
+ #
181
+ def AuthHMAC.authenticated?(request, access_key_id, secret, options)
182
+ credentials = { access_key_id => secret }
183
+ self.new(credentials, options).authenticated?(request)
184
+ end
185
+
186
+ # Signs a request using the access_key_id and the secret associated with that id
187
+ # in the credential store.
188
+ #
189
+ # Signing a requests adds an Authorization header to the request in the format:
190
+ #
191
+ # <service_id> <access_key_id>:<signature>
192
+ #
193
+ # where <signature> is the Base64 encoded HMAC-SHA1 of the CanonicalString and the secret.
194
+ #
195
+ def sign!(request, access_key_id)
196
+ secret = @credential_store[access_key_id]
197
+ raise ArgumentError, "No secret found for key id '#{access_key_id}'" if secret.nil?
198
+ request['Authorization'] = authorization(request, access_key_id, secret)
199
+ end
200
+
201
+ # Authenticates a request using HMAC
202
+ #
203
+ # Returns true if the request has an AuthHMAC Authorization header and
204
+ # the access id and HMAC match an id and HMAC produced for the secret
205
+ # in the credential store. Otherwise returns false.
206
+ #
207
+ def authenticated?(request)
208
+ rx = Regexp.new("#{@service_id} ([^:]+):(.+)$")
209
+ if md = rx.match(authorization_header(request))
210
+ access_key_id = md[1]
211
+ hmac = md[2]
212
+ secret = @credential_store[access_key_id]
213
+ !secret.nil? && hmac == signature(request, secret)
214
+ else
215
+ false
216
+ end
217
+ end
218
+
219
+ def signature(request, secret)
220
+ digest = OpenSSL::Digest::Digest.new('sha1')
221
+ Base64.encode64(OpenSSL::HMAC.digest(digest, secret, canonical_string(request))).strip
222
+ end
223
+
224
+ def canonical_string(request)
225
+ @signature_method.call(request)
226
+ end
227
+
228
+ def authorization_header(request)
229
+ find_header(%w(Authorization HTTP_AUTHORIZATION), headers(request))
230
+ end
231
+
232
+ def authorization(request, access_key_id, secret)
233
+ "#{@service_id} #{access_key_id}:#{signature(request, secret)}"
234
+ end
235
+
236
+ end
237
+ end
@@ -0,0 +1,5 @@
1
+ module CrashLog
2
+ class AuthHMAC
3
+ VERSION = "1.1.2"
4
+ end
5
+ end
@@ -0,0 +1 @@
1
+ require "crash_log/auth_hmac"
@@ -0,0 +1,288 @@
1
+ require 'spec_helper'
2
+ require "net/http"
3
+ require 'time'
4
+ require 'active_support'
5
+
6
+ describe CrashLog::AuthHMAC do
7
+ # Class for doing a custom signature
8
+ class CustomSignature < String
9
+ def initialize(request)
10
+ self << "Custom signature string: #{request.method}"
11
+ end
12
+ end
13
+
14
+ def signature(value, secret)
15
+ digest = OpenSSL::Digest::Digest.new('sha1')
16
+ Base64.encode64(OpenSSL::HMAC.digest(digest, secret, value)).strip
17
+ end
18
+
19
+ let(:request) {
20
+ Net::HTTP::Put.new("/path/to/put?foo=bar&bar=foo",
21
+ 'content-type' => 'text/plain',
22
+ 'content-md5' => 'blahblah',
23
+ 'date' => "Thu, 10 Jul 2008 03:29:56 GMT")
24
+ }
25
+
26
+ describe ".canonical_string" do
27
+ it "should generate a canonical string using default method" do
28
+ CrashLog::AuthHMAC.canonical_string(request).should == "PUT\ntext/plain\nblahblah\nThu, 10 Jul 2008 03:29:56 GMT\n/path/to/put"
29
+ end
30
+ end
31
+
32
+ describe ".signature" do
33
+ it "should generate a valid signature string for a secret" do
34
+ CrashLog::AuthHMAC.signature(request, 'secret').should == "71wAJM4IIu/3o6lcqx/tw7XnAJs="
35
+ end
36
+ end
37
+
38
+ describe ".sign!" do
39
+ it "should sign using the key passed in as a parameter" do
40
+ CrashLog::AuthHMAC.sign!(request, "my-key-id", "secret")
41
+ request['Authorization'].should == "AuthHMAC my-key-id:71wAJM4IIu/3o6lcqx/tw7XnAJs="
42
+ end
43
+
44
+ it "should sign using custom service id" do
45
+ CrashLog::AuthHMAC.sign!(request, "my-key-id", "secret", { :service_id => 'MyService' })
46
+ request['Authorization'].should == "MyService my-key-id:71wAJM4IIu/3o6lcqx/tw7XnAJs="
47
+ end
48
+
49
+ it "should sign using custom signature method" do
50
+ options = {
51
+ :service_id => 'MyService',
52
+ :signature => CustomSignature
53
+ }
54
+ CrashLog::AuthHMAC.sign!(request, "my-key-id", "secret", options)
55
+ request['Authorization'].should == "MyService my-key-id:/L4N1v1BZSHfAYkQjsvZn696D9c="
56
+ end
57
+ end
58
+
59
+ describe "#sign!" do
60
+ before(:each) do
61
+ @get_request = Net::HTTP::Get.new("/")
62
+ @put_request = Net::HTTP::Put.new("/path/to/put?foo=bar&bar=foo",
63
+ 'content-type' => 'text/plain',
64
+ 'content-md5' => 'blahblah',
65
+ 'date' => "Thu, 10 Jul 2008 03:29:56 GMT")
66
+ @store = mock('store')
67
+ @store.stub!(:[]).and_return("")
68
+ @authhmac = CrashLog::AuthHMAC.new(@store)
69
+ end
70
+
71
+ describe "default AuthHMAC with CanonicalString signature" do
72
+ it "should add an Authorization header" do
73
+ @authhmac.sign!(@get_request, 'key-id')
74
+ @get_request.key?("Authorization").should be_true
75
+ end
76
+
77
+ it "should fetch the secret from the store" do
78
+ @store.should_receive(:[]).with('key-id').and_return('secret')
79
+ @authhmac.sign!(@get_request, 'key-id')
80
+ end
81
+
82
+ it "should prefix the Authorization Header with AuthHMAC" do
83
+ @authhmac.sign!(@get_request, 'key-id')
84
+ @get_request['Authorization'].should match(/^AuthHMAC /)
85
+ end
86
+
87
+ it "should include the key id as the first part of the Authorization header value" do
88
+ @authhmac.sign!(@get_request, 'key-id')
89
+ @get_request['Authorization'].should match(/^AuthHMAC key-id:/)
90
+ end
91
+
92
+ it "should include the base64 encoded HMAC signature as the last part of the header value" do
93
+ @authhmac.sign!(@get_request, 'key-id')
94
+ @get_request['Authorization'].should match(/:[A-Za-z0-9+\/]{26,28}[=]{0,2}$/)
95
+ end
96
+
97
+ it "should create a complete signature" do
98
+ @store.should_receive(:[]).with('my-key-id').and_return('secret')
99
+ @authhmac.sign!(@put_request, "my-key-id")
100
+ @put_request['Authorization'].should == "AuthHMAC my-key-id:71wAJM4IIu/3o6lcqx/tw7XnAJs="
101
+ end
102
+ end
103
+
104
+ describe "custom signatures" do
105
+ before(:each) do
106
+ @options = {
107
+ :service_id => 'MyService',
108
+ :signature => CustomSignature
109
+ }
110
+ @authhmac = CrashLog::AuthHMAC.new(@store, @options)
111
+ end
112
+
113
+ it "should prefix the Authorization header with custom service id" do
114
+ @authhmac.sign!(@get_request, 'key-id')
115
+ @get_request['Authorization'].should match(/^MyService /)
116
+ end
117
+
118
+ it "should create a complete signature using options" do
119
+ @store.should_receive(:[]).with('my-key-id').and_return('secret')
120
+ @authhmac.sign!(@put_request, "my-key-id")
121
+ @put_request['Authorization'].should == "MyService my-key-id:/L4N1v1BZSHfAYkQjsvZn696D9c="
122
+ end
123
+ end
124
+ end
125
+
126
+ describe "authenticated?" do
127
+ before(:each) do
128
+ @credentials = {
129
+ "access key 1" => 'secret1',
130
+ "access key 2" => 'secret2'
131
+ }
132
+ @authhmac = CrashLog::AuthHMAC.new(@credentials)
133
+ @request = Net::HTTP::Get.new("/path/to/get?foo=bar&bar=foo", 'date' => "Thu, 10 Jul 2008 03:29:56 GMT")
134
+ end
135
+
136
+ it "should return false when there is no Authorization Header" do
137
+ @authhmac.authenticated?(@request).should be_false
138
+ end
139
+
140
+ it "should return false when the Authorization value isn't prefixed with HMAC" do
141
+ @request['Authorization'] = "id:secret"
142
+ @authhmac.authenticated?(@request).should be_false
143
+ end
144
+
145
+ it "should return false when the access key id can't be found" do
146
+ @request['Authorization'] = 'AuthHMAC missing-key:blah'
147
+ @authhmac.authenticated?(@request).should be_false
148
+ end
149
+
150
+ it "should return false when there is no hmac" do
151
+ @request['Authorization'] = 'AuthHMAC missing-key:'
152
+ @authhmac.authenticated?(@request).should be_false
153
+ end
154
+
155
+ it "should return false when the hmac doesn't match" do
156
+ @request['Authorization'] = 'AuthHMAC access key 1:blah'
157
+ @authhmac.authenticated?(@request).should be_false
158
+ end
159
+
160
+ it "should return false if the request was modified after signing" do
161
+ @authhmac.sign!(@request, 'access key 1')
162
+ @request.content_type = 'text/plain'
163
+ @authhmac.authenticated?(@request).should be_false
164
+ end
165
+
166
+ it "should return true when the hmac does match" do
167
+ @authhmac.sign!(@request, 'access key 1')
168
+ @authhmac.authenticated?(@request).should be_true
169
+ end
170
+
171
+ describe "custom signatures" do
172
+ before(:each) do
173
+ @options = {
174
+ :service_id => 'MyService',
175
+ :signature => CustomSignature
176
+ }
177
+ end
178
+
179
+ it "should return false for invalid service id" do
180
+ @authhmac.sign!(@request, 'access key 1')
181
+ @options.delete(:signature)
182
+ CrashLog::AuthHMAC.new(@credentials, @options).authenticated?(@request).should be_false
183
+ end
184
+
185
+ it "should return false for request using default CanonicalString signature" do
186
+ @authhmac.sign!(@request, 'access key 1')
187
+ @options.delete(:service_id)
188
+ CrashLog::AuthHMAC.new(@credentials, @options).authenticated?(@request).should be_false
189
+ end
190
+
191
+ it "should return true when valid" do
192
+ @authhmac = CrashLog::AuthHMAC.new(@credentials, @options)
193
+ @authhmac.sign!(@request, 'access key 1')
194
+ @authhmac.authenticated?(@request).should be_true
195
+ end
196
+ end
197
+ end
198
+
199
+ describe "#sign! with YAML credentials" do
200
+ before(:each) do
201
+ credentials = {
202
+ "access key 1" => 'secret1',
203
+ "access key 2" => 'secret2'
204
+ }
205
+ @authhmac = CrashLog::AuthHMAC.new(credentials)
206
+ @request = Net::HTTP::Get.new("/path/to/get?foo=bar&bar=foo", 'date' => "Thu, 10 Jul 2008 03:29:56 GMT")
207
+ end
208
+
209
+ it "should raise an argument error if credentials are missing" do
210
+ lambda { @authhmac.sign!(@request, 'missing') }.should raise_error(ArgumentError)
211
+ end
212
+
213
+ it "should sign with the secret" do
214
+ @authhmac.sign!(@request, "access key 1")
215
+ @request['Authorization'].should == "AuthHMAC access key 1:ovwO0OBERuF3/uR3aowaUCkFMiE="
216
+ end
217
+
218
+ it "should sign with the other secret" do
219
+ @authhmac.sign!(@request, "access key 2")
220
+ @request['Authorization'].should == "AuthHMAC access key 2:vT010RQm4IZ6+UCVpK2/N0FLpLw="
221
+ end
222
+ end
223
+
224
+ describe CrashLog::AuthHMAC::CanonicalString do
225
+ it "should include the http verb when it is GET" do
226
+ request = Net::HTTP::Get.new("/")
227
+ CrashLog::AuthHMAC::CanonicalString.new(request).should match(/GET/)
228
+ end
229
+
230
+ it "should include the http verb when it is POST" do
231
+ request = Net::HTTP::Post.new("/")
232
+ CrashLog::AuthHMAC::CanonicalString.new(request).should match(/POST/)
233
+ end
234
+
235
+ it "should include the content-type" do
236
+ request = Net::HTTP::Put.new("/", {'Content-Type' => 'application/xml'})
237
+ CrashLog::AuthHMAC::CanonicalString.new(request).should match(/application\/xml/)
238
+ end
239
+
240
+ it "should include the content-type even if the case is messed up" do
241
+ request = Net::HTTP::Put.new("/", {'cOntent-type' => 'text/html'})
242
+ CrashLog::AuthHMAC::CanonicalString.new(request).should match(/text\/html/)
243
+ end
244
+
245
+ it "should include the content-md5" do
246
+ request = Net::HTTP::Put.new("/", {'Content-MD5' => 'skwkend'})
247
+ CrashLog::AuthHMAC::CanonicalString.new(request).should match(/skwkend/)
248
+ end
249
+
250
+ it "should include the content-md5 even if the case is messed up" do
251
+ request = Net::HTTP::Put.new("/", {'content-md5' => 'adsada'})
252
+ CrashLog::AuthHMAC::CanonicalString.new(request).should match(/adsada/)
253
+ end
254
+
255
+ it "should include the date" do
256
+ date = Time.now.httpdate
257
+ request = Net::HTTP::Put.new("/", {'Date' => date})
258
+ CrashLog::AuthHMAC::CanonicalString.new(request).should match(/#{date}/)
259
+ end
260
+
261
+ it "should include the request path" do
262
+ request = Net::HTTP::Get.new("/path/to/file")
263
+ CrashLog::AuthHMAC::CanonicalString.new(request).should match(/\/path\/to\/file[^?]?/)
264
+ end
265
+
266
+ it "should ignore the query string of the request path" do
267
+ request = Net::HTTP::Get.new("/other/path/to/file?query=foo")
268
+ CrashLog::AuthHMAC::CanonicalString.new(request).should match(/\/other\/path\/to\/file[^?]?/)
269
+ end
270
+
271
+ it "should build the correct string" do
272
+ date = Time.now.httpdate
273
+ request = Net::HTTP::Put.new("/path/to/put?foo=bar&bar=foo",
274
+ 'content-type' => 'text/plain',
275
+ 'content-md5' => 'blahblah',
276
+ 'date' => date)
277
+ CrashLog::AuthHMAC::CanonicalString.new(request).should == "PUT\ntext/plain\nblahblah\n#{date}\n/path/to/put"
278
+ end
279
+
280
+ it "should build the correct string when some elements are missing" do
281
+ date = Time.now.httpdate
282
+ request = Net::HTTP::Get.new("/path/to/get?foo=bar&bar=foo",
283
+ 'date' => date)
284
+ CrashLog::AuthHMAC::CanonicalString.new(request).should == "GET\n\n\n#{date}\n/path/to/get"
285
+ end
286
+ end
287
+
288
+ end
@@ -0,0 +1,8 @@
1
+ require "crash_log"
2
+
3
+ Dir[File.expand_path("../support/*.rb", __FILE__)].each { |file| require file }
4
+
5
+ RSpec.configure do |config|
6
+ config.mock_with :rspec
7
+ end
8
+
metadata ADDED
@@ -0,0 +1,60 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: crashlog-auth-hmac
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.1.2
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Ivan Vanderbyl
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-08-05 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description: A Ruby Gem for authenticating HTTP requests using a HMAC
15
+ email:
16
+ - ivan@crashlog.io
17
+ executables: []
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - .gitignore
22
+ - .rspec
23
+ - Gemfile
24
+ - LICENSE
25
+ - README.md
26
+ - Rakefile
27
+ - crashlog-auth-hmac.gemspec
28
+ - lib/crash_log.rb
29
+ - lib/crash_log/auth_hmac.rb
30
+ - lib/crash_log/auth_hmac/version.rb
31
+ - lib/crashlog-auth-hmac.rb
32
+ - spec/crash_log/auth_hmac_spec.rb
33
+ - spec/spec_helper.rb
34
+ homepage: http://crashlog.io
35
+ licenses: []
36
+ post_install_message:
37
+ rdoc_options: []
38
+ require_paths:
39
+ - lib
40
+ required_ruby_version: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ required_rubygems_version: !ruby/object:Gem::Requirement
47
+ none: false
48
+ requirements:
49
+ - - ! '>='
50
+ - !ruby/object:Gem::Version
51
+ version: '0'
52
+ requirements: []
53
+ rubyforge_project:
54
+ rubygems_version: 1.8.15
55
+ signing_key:
56
+ specification_version: 3
57
+ summary: A Ruby Gem for authenticating HTTP requests using a HMAC
58
+ test_files:
59
+ - spec/crash_log/auth_hmac_spec.rb
60
+ - spec/spec_helper.rb