captivus-auth_hmac 0.0.1

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/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