captivus-auth_hmac 0.0.1

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.
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/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in captivus-auth_hmac.gemspec
4
+ gemspec
5
+
6
+ gem "rspec", "~> 2.11"
7
+ gem "rack-test", "~> 0.6"
8
+ gem "delorean", "~> 2.0.0"
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Austin Schneider
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,29 @@
1
+ # Captivus::AuthHMAC
2
+
3
+ HMAC based authentication for HTTP
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'captivus-auth_hmac'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install captivus-auth_hmac
18
+
19
+ ## Usage
20
+
21
+ TODO: Write usage instructions here
22
+
23
+ ## Contributing
24
+
25
+ 1. Fork it
26
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
27
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
28
+ 4. Push to the branch (`git push origin my-new-feature`)
29
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,19 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'captivus/auth_hmac/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "captivus-auth_hmac"
8
+ gem.version = Captivus::AuthHMAC::VERSION
9
+ gem.authors = ["Austin Schneider"]
10
+ gem.email = ["austinthecoder@gmail.com"]
11
+ gem.description = "HMAC based authentication for HTTP"
12
+ gem.summary = "HMAC based authentication for HTTP"
13
+ gem.homepage = ""
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ["lib"]
19
+ end
@@ -0,0 +1,5 @@
1
+ module Captivus
2
+ class AuthHMAC
3
+ VERSION = "0.0.1"
4
+ end
5
+ end
@@ -0,0 +1,249 @@
1
+ require 'openssl'
2
+ require 'base64'
3
+
4
+ module Captivus
5
+ class AuthHMAC
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.is_a?(Hash) && request.has_key?(:request_headers)
30
+ request[:request_headers]
31
+ elsif request.respond_to?(:[])
32
+ request
33
+ else
34
+ raise ArgumentError, "Don't know how to get the headers from #{request.inspect}"
35
+ end
36
+ end
37
+
38
+ def find_header(keys, headers)
39
+ keys.map do |key|
40
+ headers[key]
41
+ end.compact.first
42
+ end
43
+ end
44
+
45
+ include Headers
46
+
47
+ # Build a Canonical String for a HTTP request.
48
+ #
49
+ # A Canonical String has the following format:
50
+ #
51
+ # CanonicalString = HTTP-Verb + "\n" +
52
+ # Content-Type + "\n" +
53
+ # Content-MD5 + "\n" +
54
+ # Date + "\n" +
55
+ # request-uri;
56
+ #
57
+ #
58
+ # If the Date header doesn't exist, one will be generated since
59
+ # Net/HTTP will generate one if it doesn't exist and it will be
60
+ # used on the server side to do authentication.
61
+ #
62
+ class CanonicalString < String # :nodoc:
63
+ include Headers
64
+
65
+ def initialize(request)
66
+ self << request_method(request) + "\n"
67
+ self << header_values(headers(request)) + "\n"
68
+ self << request_path(request)
69
+ end
70
+
71
+ private
72
+
73
+ def request_method(request)
74
+ if request.respond_to?(:request_method) && request.request_method.is_a?(String)
75
+ request.request_method
76
+ elsif request.is_a?(Hash) && request.has_key?(:method)
77
+ request[:method].to_s
78
+ elsif request.respond_to?(:env) && request.env
79
+ request.env['REQUEST_METHOD']
80
+ elsif request.is_a?(Hash) && request.has_key?('REQUEST_METHOD')
81
+ request['REQUEST_METHOD']
82
+ else
83
+ begin
84
+ request.method
85
+ rescue ArgumentError
86
+ raise ArgumentError, "Don't know how to get the request method from #{request.inspect}"
87
+ end
88
+ end
89
+ end
90
+
91
+ def header_values(headers)
92
+ [ content_type(headers),
93
+ content_md5(headers),
94
+ (date(headers) or headers['Date'] = Time.now.utc.httpdate)
95
+ ].join("\n")
96
+ end
97
+
98
+ def content_type(headers)
99
+ find_header(%w(CONTENT-TYPE CONTENT_TYPE HTTP_CONTENT_TYPE content-type), headers)
100
+ end
101
+
102
+ def date(headers)
103
+ find_header(%w(DATE HTTP_DATE date), headers)
104
+ end
105
+
106
+ def content_md5(headers)
107
+ find_header(%w(CONTENT-MD5 CONTENT_MD5 content-md5), headers)
108
+ end
109
+
110
+ def request_path(request)
111
+ # Try unparsed_uri in case it is a Webrick request
112
+ path = if request.respond_to?(:unparsed_uri)
113
+ request.unparsed_uri
114
+ elsif request.is_a?(Hash) && request.has_key?(:url) && request[:url].is_a?(URI)
115
+ request[:url].path
116
+ elsif request.is_a?(Hash) && request.has_key?('PATH_INFO')
117
+ request['PATH_INFO']
118
+ else
119
+ request.path
120
+ end
121
+
122
+ path[/^[^?]*/]
123
+ end
124
+ end
125
+
126
+ @@default_signature_class = CanonicalString
127
+
128
+ # Create an AuthHMAC instance using the given credential store
129
+ #
130
+ # Credential Store:
131
+ # * Credential store must respond to the [] method and return a secret for access key id
132
+ #
133
+ # Options:
134
+ # Override default options
135
+ # * <tt>:service_id</tt> - Service ID used in the AUTHORIZATION header string. Default is AuthHMAC.
136
+ # * <tt>:signature_method</tt> - Proc object that takes request and produces the signature string
137
+ # used for authentication. Default is CanonicalString.
138
+ # Examples:
139
+ # my_hmac = AuthHMAC.new('access_id1' => 'secret1', 'access_id2' => 'secret2')
140
+ #
141
+ # cred_store = { 'access_id1' => 'secret1', 'access_id2' => 'secret2' }
142
+ # options = { :service_id => 'MyApp', :signature_method => lambda { |r| MyRequestString.new(r) } }
143
+ # my_hmac = AuthHMAC.new(cred_store, options)
144
+ #
145
+ def initialize(credential_store, options = nil)
146
+ @credential_store = credential_store
147
+
148
+ # Defaults
149
+ @service_id = self.class.name.split(/::/).last
150
+ @signature_class = @@default_signature_class
151
+
152
+ unless options.nil?
153
+ @service_id = options[:service_id] if options.key?(:service_id)
154
+ @signature_class = options[:signature] if options.key?(:signature) && options[:signature].is_a?(Class)
155
+ end
156
+
157
+ @signature_method = lambda { |r| @signature_class.send(:new, r) }
158
+ end
159
+
160
+ # Generates canonical signing string for given request
161
+ #
162
+ # Supports same options as AuthHMAC.initialize for overriding service_id and
163
+ # signature method.
164
+ #
165
+ def self.canonical_string(request, options = nil)
166
+ self.new(nil, options).canonical_string(request)
167
+ end
168
+
169
+ # Generates signature string for a given secret
170
+ #
171
+ # Supports same options as AuthHMAC.initialize for overriding service_id and
172
+ # signature method.
173
+ #
174
+ def self.signature(request, secret, options = nil)
175
+ self.new(nil, options).signature(request, secret)
176
+ end
177
+
178
+ # Signs a request using a given access key id and secret.
179
+ #
180
+ # Supports same options as AuthHMAC.initialize for overriding service_id and
181
+ # signature method.
182
+ #
183
+ def self.sign!(request, access_key_id, secret, options = nil)
184
+ credentials = { access_key_id => secret }
185
+ self.new(credentials, options).sign!(request, access_key_id)
186
+ end
187
+
188
+ # Authenticates a request using HMAC
189
+ #
190
+ # Supports same options as AuthHMAC.initialize for overriding service_id and
191
+ # signature method.
192
+ #
193
+ def self.authenticated?(request, access_key_id, secret, options)
194
+ credentials = { access_key_id => secret }
195
+ self.new(credentials, options).authenticated?(request)
196
+ end
197
+
198
+ # Signs a request using the access_key_id and the secret associated with that id
199
+ # in the credential store.
200
+ #
201
+ # Signing a requests adds an Authorization header to the request in the format:
202
+ #
203
+ # <service_id> <access_key_id>:<signature>
204
+ #
205
+ # where <signature> is the Base64 encoded HMAC-SHA1 of the CanonicalString and the secret.
206
+ #
207
+ def sign!(request, access_key_id)
208
+ secret = @credential_store[access_key_id]
209
+ raise ArgumentError, "No secret found for key id '#{access_key_id}'" if secret.nil?
210
+ request['Authorization'] = authorization(request, access_key_id, secret)
211
+ end
212
+
213
+ # Authenticates a request using HMAC
214
+ #
215
+ # Returns true if the request has an AuthHMAC Authorization header and
216
+ # the access id and HMAC match an id and HMAC produced for the secret
217
+ # in the credential store. Otherwise returns false.
218
+ #
219
+ def authenticated?(request)
220
+ rx = Regexp.new("#{@service_id} ([^:]+):(.+)$")
221
+ if md = rx.match(authorization_header(request))
222
+ access_key_id = md[1]
223
+ hmac = md[2]
224
+ secret = @credential_store[access_key_id]
225
+ !secret.nil? && hmac == signature(request, secret)
226
+ else
227
+ false
228
+ end
229
+ end
230
+
231
+ def signature(request, secret)
232
+ digest = OpenSSL::Digest::Digest.new('sha1')
233
+ string = canonical_string(request)
234
+ Base64.strict_encode64(OpenSSL::HMAC.digest(digest, secret, string)).strip
235
+ end
236
+
237
+ def canonical_string(request)
238
+ @signature_method.call(request)
239
+ end
240
+
241
+ def authorization_header(request)
242
+ find_header(%w(Authorization HTTP_AUTHORIZATION), headers(request))
243
+ end
244
+
245
+ def authorization(request, access_key_id, secret)
246
+ "#{@service_id} #{access_key_id}:#{signature(request, secret)}"
247
+ end
248
+ end
249
+ end
data/lib/captivus.rb ADDED
@@ -0,0 +1,6 @@
1
+ require 'captivus/auth_hmac/version'
2
+
3
+ module Captivus
4
+ end
5
+
6
+ require 'captivus/auth_hmac'
@@ -0,0 +1,302 @@
1
+ require 'captivus/auth_hmac'
2
+ require 'net/http'
3
+ require 'time'
4
+
5
+ describe Captivus::AuthHMAC do
6
+ # Class for doing a custom signature
7
+ class CustomSignature < String
8
+ def initialize(request)
9
+ self << "Custom signature string: #{request.method}"
10
+ end
11
+ end
12
+
13
+ def signature(value, secret)
14
+ digest = OpenSSL::Digest::Digest.new('sha1')
15
+ Base64.encode64(OpenSSL::HMAC.digest(digest, secret, value)).strip
16
+ end
17
+
18
+ let(:request) {
19
+ Net::HTTP::Put.new("/path/to/put?foo=bar&bar=foo",
20
+ 'content-type' => 'text/plain',
21
+ 'content-md5' => 'blahblah',
22
+ 'date' => "Thu, 10 Jul 2008 03:29:56 GMT")
23
+ }
24
+
25
+ describe ".canonical_string" do
26
+ it "should generate a canonical string using default method" do
27
+ Captivus::AuthHMAC.canonical_string(request).should == "PUT\ntext/plain\nblahblah\nThu, 10 Jul 2008 03:29:56 GMT\n/path/to/put"
28
+ end
29
+ end
30
+
31
+ describe ".signature" do
32
+ it "should generate a valid signature string for a secret" do
33
+ Captivus::AuthHMAC.signature(request, 'secret').should == "71wAJM4IIu/3o6lcqx/tw7XnAJs="
34
+ end
35
+ end
36
+
37
+ describe ".sign!" do
38
+ it "should sign using the key passed in as a parameter" do
39
+ Captivus::AuthHMAC.sign!(request, "my-key-id", "secret")
40
+ request['Authorization'].should == "AuthHMAC my-key-id:71wAJM4IIu/3o6lcqx/tw7XnAJs="
41
+ end
42
+
43
+ it "should sign using custom service id" do
44
+ Captivus::AuthHMAC.sign!(request, "my-key-id", "secret", { :service_id => 'MyService' })
45
+ request['Authorization'].should == "MyService my-key-id:71wAJM4IIu/3o6lcqx/tw7XnAJs="
46
+ end
47
+
48
+ it "should sign using custom signature method" do
49
+ options = {
50
+ :service_id => 'MyService',
51
+ :signature => CustomSignature
52
+ }
53
+ Captivus::AuthHMAC.sign!(request, "my-key-id", "secret", options)
54
+ request['Authorization'].should == "MyService my-key-id:/L4N1v1BZSHfAYkQjsvZn696D9c="
55
+ end
56
+
57
+ it 'can sign a faraday request hash' do
58
+ Time.stub(:now).and_return(Time.parse("Thu, 10 Jul 2008 03:29:56 GMT"))
59
+
60
+ env = {
61
+ :method=>:get,
62
+ :body=>"test",
63
+ :url=>URI.parse("http://sushi.com/api/foo.json"),
64
+ :request_headers=>{"Date" => Time.now.utc.httpdate},
65
+ :parallel_manager=>nil,
66
+ :request=>nil,
67
+ :ssl=>{}
68
+ }
69
+ Captivus::AuthHMAC.sign!(env, "access_id", "secret", { :service_id => 'MyService' })
70
+ env['Authorization'].should == 'MyService access_id:ZQnbYwmno+PsaavXzUAdvj/DKvo='
71
+ end
72
+ end
73
+
74
+ describe "#sign!" do
75
+ before(:each) do
76
+ @get_request = Net::HTTP::Get.new("/")
77
+ @put_request = Net::HTTP::Put.new("/path/to/put?foo=bar&bar=foo",
78
+ 'content-type' => 'text/plain',
79
+ 'content-md5' => 'blahblah',
80
+ 'date' => "Thu, 10 Jul 2008 03:29:56 GMT")
81
+ @store = mock('store')
82
+ @store.stub!(:[]).and_return("")
83
+ @authhmac = Captivus::AuthHMAC.new(@store)
84
+ end
85
+
86
+ describe "default AuthHMAC with CanonicalString signature" do
87
+ it "should add an Authorization header" do
88
+ @authhmac.sign!(@get_request, 'key-id')
89
+ @get_request.key?("Authorization").should be_true
90
+ end
91
+
92
+ it "should fetch the secret from the store" do
93
+ @store.should_receive(:[]).with('key-id').and_return('secret')
94
+ @authhmac.sign!(@get_request, 'key-id')
95
+ end
96
+
97
+ it "should prefix the Authorization Header with AuthHMAC" do
98
+ @authhmac.sign!(@get_request, 'key-id')
99
+ @get_request['Authorization'].should match(/^AuthHMAC /)
100
+ end
101
+
102
+ it "should include the key id as the first part of the Authorization header value" do
103
+ @authhmac.sign!(@get_request, 'key-id')
104
+ @get_request['Authorization'].should match(/^AuthHMAC key-id:/)
105
+ end
106
+
107
+ it "should include the base64 encoded HMAC signature as the last part of the header value" do
108
+ @authhmac.sign!(@get_request, 'key-id')
109
+ @get_request['Authorization'].should match(/:[A-Za-z0-9+\/]{26,28}[=]{0,2}$/)
110
+ end
111
+
112
+ it "should create a complete signature" do
113
+ @store.should_receive(:[]).with('my-key-id').and_return('secret')
114
+ @authhmac.sign!(@put_request, "my-key-id")
115
+ @put_request['Authorization'].should == "AuthHMAC my-key-id:71wAJM4IIu/3o6lcqx/tw7XnAJs="
116
+ end
117
+ end
118
+
119
+ describe "custom signatures" do
120
+ before(:each) do
121
+ @options = {
122
+ :service_id => 'MyService',
123
+ :signature => CustomSignature
124
+ }
125
+ @authhmac = Captivus::AuthHMAC.new(@store, @options)
126
+ end
127
+
128
+ it "should prefix the Authorization header with custom service id" do
129
+ @authhmac.sign!(@get_request, 'key-id')
130
+ @get_request['Authorization'].should match(/^MyService /)
131
+ end
132
+
133
+ it "should create a complete signature using options" do
134
+ @store.should_receive(:[]).with('my-key-id').and_return('secret')
135
+ @authhmac.sign!(@put_request, "my-key-id")
136
+ @put_request['Authorization'].should == "MyService my-key-id:/L4N1v1BZSHfAYkQjsvZn696D9c="
137
+ end
138
+ end
139
+ end
140
+
141
+ describe "authenticated?" do
142
+ before(:each) do
143
+ @credentials = {
144
+ "access key 1" => 'secret1',
145
+ "access key 2" => 'secret2'
146
+ }
147
+ @authhmac = Captivus::AuthHMAC.new(@credentials)
148
+ @request = Net::HTTP::Get.new("/path/to/get?foo=bar&bar=foo", 'date' => "Thu, 10 Jul 2008 03:29:56 GMT")
149
+ end
150
+
151
+ it "should return false when there is no Authorization Header" do
152
+ @authhmac.authenticated?(@request).should be_false
153
+ end
154
+
155
+ it "should return false when the Authorization value isn't prefixed with HMAC" do
156
+ @request['Authorization'] = "id:secret"
157
+ @authhmac.authenticated?(@request).should be_false
158
+ end
159
+
160
+ it "should return false when the access key id can't be found" do
161
+ @request['Authorization'] = 'AuthHMAC missing-key:blah'
162
+ @authhmac.authenticated?(@request).should be_false
163
+ end
164
+
165
+ it "should return false when there is no hmac" do
166
+ @request['Authorization'] = 'AuthHMAC missing-key:'
167
+ @authhmac.authenticated?(@request).should be_false
168
+ end
169
+
170
+ it "should return false when the hmac doesn't match" do
171
+ @request['Authorization'] = 'AuthHMAC access key 1:blah'
172
+ @authhmac.authenticated?(@request).should be_false
173
+ end
174
+
175
+ it "should return false if the request was modified after signing" do
176
+ @authhmac.sign!(@request, 'access key 1')
177
+ @request.content_type = 'text/plain'
178
+ @authhmac.authenticated?(@request).should be_false
179
+ end
180
+
181
+ it "should return true when the hmac does match" do
182
+ @authhmac.sign!(@request, 'access key 1')
183
+ @authhmac.authenticated?(@request).should be_true
184
+ end
185
+
186
+ describe "custom signatures" do
187
+ before(:each) do
188
+ @options = {
189
+ :service_id => 'MyService',
190
+ :signature => CustomSignature
191
+ }
192
+ end
193
+
194
+ it "should return false for invalid service id" do
195
+ @authhmac.sign!(@request, 'access key 1')
196
+ @options.delete(:signature)
197
+ Captivus::AuthHMAC.new(@credentials, @options).authenticated?(@request).should be_false
198
+ end
199
+
200
+ it "should return false for request using default CanonicalString signature" do
201
+ @authhmac.sign!(@request, 'access key 1')
202
+ @options.delete(:service_id)
203
+ Captivus::AuthHMAC.new(@credentials, @options).authenticated?(@request).should be_false
204
+ end
205
+
206
+ it "should return true when valid" do
207
+ @authhmac = Captivus::AuthHMAC.new(@credentials, @options)
208
+ @authhmac.sign!(@request, 'access key 1')
209
+ @authhmac.authenticated?(@request).should be_true
210
+ end
211
+ end
212
+ end
213
+
214
+ describe "#sign! with YAML credentials" do
215
+ before(:each) do
216
+ credentials = {
217
+ "access key 1" => 'secret1',
218
+ "access key 2" => 'secret2'
219
+ }
220
+ @authhmac = Captivus::AuthHMAC.new(credentials)
221
+ @request = Net::HTTP::Get.new("/path/to/get?foo=bar&bar=foo", 'date' => "Thu, 10 Jul 2008 03:29:56 GMT")
222
+ end
223
+
224
+ it "should raise an argument error if credentials are missing" do
225
+ lambda { @authhmac.sign!(@request, 'missing') }.should raise_error(ArgumentError)
226
+ end
227
+
228
+ it "should sign with the secret" do
229
+ @authhmac.sign!(@request, "access key 1")
230
+ @request['Authorization'].should == "AuthHMAC access key 1:ovwO0OBERuF3/uR3aowaUCkFMiE="
231
+ end
232
+
233
+ it "should sign with the other secret" do
234
+ @authhmac.sign!(@request, "access key 2")
235
+ @request['Authorization'].should == "AuthHMAC access key 2:vT010RQm4IZ6+UCVpK2/N0FLpLw="
236
+ end
237
+ end
238
+
239
+ describe Captivus::AuthHMAC::CanonicalString do
240
+ it "should include the http verb when it is GET" do
241
+ request = Net::HTTP::Get.new("/")
242
+ Captivus::AuthHMAC::CanonicalString.new(request).should match(/GET/)
243
+ end
244
+
245
+ it "should include the http verb when it is POST" do
246
+ request = Net::HTTP::Post.new("/")
247
+ Captivus::AuthHMAC::CanonicalString.new(request).should match(/POST/)
248
+ end
249
+
250
+ it "should include the content-type" do
251
+ request = Net::HTTP::Put.new("/", {'Content-Type' => 'application/xml'})
252
+ Captivus::AuthHMAC::CanonicalString.new(request).should match(/application\/xml/)
253
+ end
254
+
255
+ it "should include the content-type even if the case is messed up" do
256
+ request = Net::HTTP::Put.new("/", {'cOntent-type' => 'text/html'})
257
+ Captivus::AuthHMAC::CanonicalString.new(request).should match(/text\/html/)
258
+ end
259
+
260
+ it "should include the content-md5" do
261
+ request = Net::HTTP::Put.new("/", {'Content-MD5' => 'skwkend'})
262
+ Captivus::AuthHMAC::CanonicalString.new(request).should match(/skwkend/)
263
+ end
264
+
265
+ it "should include the content-md5 even if the case is messed up" do
266
+ request = Net::HTTP::Put.new("/", {'content-md5' => 'adsada'})
267
+ Captivus::AuthHMAC::CanonicalString.new(request).should match(/adsada/)
268
+ end
269
+
270
+ it "should include the date" do
271
+ date = Time.now.httpdate
272
+ request = Net::HTTP::Put.new("/", {'Date' => date})
273
+ Captivus::AuthHMAC::CanonicalString.new(request).should match(/#{date}/)
274
+ end
275
+
276
+ it "should include the request path" do
277
+ request = Net::HTTP::Get.new("/path/to/file")
278
+ Captivus::AuthHMAC::CanonicalString.new(request).should match(/\/path\/to\/file[^?]?/)
279
+ end
280
+
281
+ it "should ignore the query string of the request path" do
282
+ request = Net::HTTP::Get.new("/other/path/to/file?query=foo")
283
+ Captivus::AuthHMAC::CanonicalString.new(request).should match(/\/other\/path\/to\/file[^?]?/)
284
+ end
285
+
286
+ it "should build the correct string" do
287
+ date = Time.now.httpdate
288
+ request = Net::HTTP::Put.new("/path/to/put?foo=bar&bar=foo",
289
+ 'content-type' => 'text/plain',
290
+ 'content-md5' => 'blahblah',
291
+ 'date' => date)
292
+ Captivus::AuthHMAC::CanonicalString.new(request).should == "PUT\ntext/plain\nblahblah\n#{date}\n/path/to/put"
293
+ end
294
+
295
+ it "should build the correct string when some elements are missing" do
296
+ date = Time.now.httpdate
297
+ request = Net::HTTP::Get.new("/path/to/get?foo=bar&bar=foo",
298
+ 'date' => date)
299
+ Captivus::AuthHMAC::CanonicalString.new(request).should == "GET\n\n\n#{date}\n/path/to/get"
300
+ end
301
+ end
302
+ end
@@ -0,0 +1,4 @@
1
+ require 'captivus'
2
+
3
+ describe Captivus do
4
+ end
@@ -0,0 +1,54 @@
1
+ require 'spec_helper'
2
+ require 'captivus/auth_hmac'
3
+ require 'rack/test'
4
+ require 'net/http'
5
+
6
+ describe Captivus::AuthHMAC do
7
+ include Rack::Test::Methods
8
+
9
+ def app
10
+ lambda do |env|
11
+ [200, {"Content-Type" => "text/html", "Content-Length" => 13}, "Hello, World!"]
12
+ end
13
+ end
14
+
15
+ it 'says hello world' do
16
+ get '/'
17
+ last_response.status.should == 200
18
+ last_response.body.should == 'Hello, World!'
19
+ end
20
+
21
+ it 'can process rack test requests' do
22
+ # HMAC uses date to validate request signature, we need to fix the date so
23
+ # that it matches.
24
+ Delorean.time_travel_to(Date.parse("Thu, 10 Jul 2008 03:29:56 GMT"))
25
+
26
+ env = current_session.__send__(:env_for, '/notify', {}.merge(:method => "POST", :params => {token: 'my-key-id'}))
27
+ signature = Captivus::AuthHMAC.sign!(env, "my-key-id", "secret")
28
+ signature.should == "AuthHMAC my-key-id:5zpt0efY2ck3zO/U1Eh9De/B1KM="
29
+
30
+ back_to_the_present
31
+ end
32
+
33
+ it 'can handle hash requests' do
34
+ Delorean.time_travel_to(Date.parse("Thu, 10 Jul 2008 03:29:56 GMT"))
35
+
36
+ request_hash = {
37
+ 'REQUEST_METHOD' => 'PUT',
38
+ 'content-type' => 'text/plain',
39
+ 'content-md5' => 'blahblah',
40
+ 'date' => "Thu, 10 Jul 2008 03:29:56 GMT",
41
+ 'PATH_INFO' => '/notify'
42
+ }
43
+
44
+ standard_request = Net::HTTP::Put.new("/notify",
45
+ 'content-type' => 'text/plain',
46
+ 'content-md5' => 'blahblah',
47
+ 'date' => "Thu, 10 Jul 2008 03:29:56 GMT")
48
+
49
+ sig = Captivus::AuthHMAC.signature(request_hash, 'secret')
50
+ sig.should == Captivus::AuthHMAC.signature(standard_request, 'secret')
51
+
52
+ back_to_the_present
53
+ end
54
+ end
@@ -0,0 +1,6 @@
1
+ require 'bundler/setup'
2
+ require "delorean"
3
+
4
+ RSpec.configure do |config|
5
+ config.include Delorean
6
+ end
metadata ADDED
@@ -0,0 +1,62 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: captivus-auth_hmac
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Austin Schneider
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-10-18 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description: HMAC based authentication for HTTP
15
+ email:
16
+ - austinthecoder@gmail.com
17
+ executables: []
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - .gitignore
22
+ - Gemfile
23
+ - LICENSE.txt
24
+ - README.md
25
+ - Rakefile
26
+ - captivus-auth_hmac.gemspec
27
+ - lib/captivus.rb
28
+ - lib/captivus/auth_hmac.rb
29
+ - lib/captivus/auth_hmac/version.rb
30
+ - spec/captivus/auth_hmac_spec.rb
31
+ - spec/captivus_spec.rb
32
+ - spec/integration/auth_hmac_spec.rb
33
+ - spec/spec_helper.rb
34
+ homepage: ''
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.24
55
+ signing_key:
56
+ specification_version: 3
57
+ summary: HMAC based authentication for HTTP
58
+ test_files:
59
+ - spec/captivus/auth_hmac_spec.rb
60
+ - spec/captivus_spec.rb
61
+ - spec/integration/auth_hmac_spec.rb
62
+ - spec/spec_helper.rb