king_hmac 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/MIT-LICENSE +20 -0
- data/README.rdoc +145 -0
- data/Rakefile +48 -0
- data/VERSION +1 -0
- data/init.rb +1 -0
- data/king_hmac.gemspec +60 -0
- data/lib/king_hmac/auth.rb +143 -0
- data/lib/king_hmac/cannonical_string.rb +69 -0
- data/lib/king_hmac/headers.rb +28 -0
- data/lib/king_hmac/rack/middleware.rb +44 -0
- data/lib/king_hmac/rails/active_resource.rb +99 -0
- data/lib/king_hmac/rails/controller.rb +63 -0
- data/lib/king_hmac.rb +7 -0
- data/spec/fixtures/credentials.yml +2 -0
- data/spec/king_hmac/king_hmac_spec.rb +506 -0
- data/spec/spec_helper.rb +21 -0
- metadata +90 -0
data/.gitignore
ADDED
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Georg Leciejewski
|
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.rdoc
ADDED
@@ -0,0 +1,145 @@
|
|
1
|
+
= king-hmac
|
2
|
+
|
3
|
+
This gem started with a copy & disection of auth-hmac gem v1.1.1
|
4
|
+
|
5
|
+
king-hmac is a Ruby implementation of HMAC[http://en.wikipedia.org/wiki/HMAC]
|
6
|
+
based authentication of HTTP requests. HMAC authentication involves a client and
|
7
|
+
server having a shared secret key. When sending the request the client, signs
|
8
|
+
the request using the secret key. This involves building a canonical
|
9
|
+
representation of the request and then generating a HMAC of the request using
|
10
|
+
the secret. The generated HMAC is then sent as part of the request.
|
11
|
+
|
12
|
+
When the server receives the request it builds the same canonical representation
|
13
|
+
and generates a HMAC using it's copy of the secret key, if the HMAC produced by
|
14
|
+
the server matches the HMAC sent by the client, the server can be assured that
|
15
|
+
the client also possesses the shared secret key.
|
16
|
+
|
17
|
+
HMAC based authentication also provides message integrity checking because the
|
18
|
+
HMAC is based on a combination of the shared secret and the content of the
|
19
|
+
request. So if any part of the request that is used to build the canonical
|
20
|
+
representation is modified by a malicious party or in transit the authentication
|
21
|
+
will then fail.
|
22
|
+
|
23
|
+
KingHmac::Auth is loosely based on the Amazon Web Services authentication scheme
|
24
|
+
but without the Amazon specific components, i.e. it is HMAC for the rest of us.
|
25
|
+
|
26
|
+
|
27
|
+
== INSTALL:
|
28
|
+
Gem hosted on gemcutter.org
|
29
|
+
sudo gem install king-hmac
|
30
|
+
|
31
|
+
== Source Code
|
32
|
+
See http://github.com/salesking/king-hmac
|
33
|
+
The source repository:
|
34
|
+
git clone git://github.com/salesking/king-hmac.git
|
35
|
+
|
36
|
+
== When to use it?
|
37
|
+
|
38
|
+
HMAC Authentication is best used as authentication for communication between
|
39
|
+
applications such as web services. It provides better security than HTTP Basic
|
40
|
+
authentication without the need to set up SSL. Of course if you need to protect
|
41
|
+
the confidentiality of the data then you need SSL, but if you just want to
|
42
|
+
authenticate requests without sending credentials in the clear KingHmac::Auth
|
43
|
+
is a good choice.
|
44
|
+
|
45
|
+
== Usage
|
46
|
+
|
47
|
+
The simplest way to use KingHmac::Auth is with the KingHmac::Auth.sign! and
|
48
|
+
KingHmac::Auth.authenticate? methods. KingHmac::Auth.sign! takes a HTTP request
|
49
|
+
object, an access id and a secret key and signs the request with the access_id
|
50
|
+
and secret key.
|
51
|
+
|
52
|
+
On the server side you can then authenticate these requests using the
|
53
|
+
KingHmac::Auth.authenticated? method. This takes the same arguments as the sign!
|
54
|
+
method but returns true if the request has been signed with the access id and
|
55
|
+
secret or false if it hasn't.
|
56
|
+
|
57
|
+
If you have more than one set of credentials you might find it useful to create
|
58
|
+
an instance of the KingHmac::Auth class, passing your credentials as a Hash of
|
59
|
+
access id => secret keys, like so:
|
60
|
+
@hmac_auth = KingHmac::Auth.new('access_id1' => 'secret1', 'access_id2' => 'secret2')
|
61
|
+
|
62
|
+
You can then use the instance methods of the @hmac_auth object to sign and
|
63
|
+
authenticate requests, for example:
|
64
|
+
@hmac_auth.sign!(request, "access_id1")
|
65
|
+
|
66
|
+
Sign +request+ with "access_id1" and it's corresponding secret key. Similarly
|
67
|
+
authentication is done like so:
|
68
|
+
@hmac_auth.authenticated?(request)
|
69
|
+
which will return true if the request has been signed with one of the access id
|
70
|
+
and secret key pairs provided in the constructor.
|
71
|
+
|
72
|
+
=== Supported HTTP request objects
|
73
|
+
|
74
|
+
KingHmac::Auth will do its best to figure out which type it is an handle it
|
75
|
+
accordingly.
|
76
|
+
* Net::HTTP::HTTPRequest
|
77
|
+
* CGI::Request
|
78
|
+
* Webrick HTTP request object.
|
79
|
+
* Rack::Request
|
80
|
+
|
81
|
+
=== access id
|
82
|
+
The access_id is used to identify the secret key that was used to sign the
|
83
|
+
request. Think of it as like a user name, it allows you to hand out different
|
84
|
+
keys to different clients and authenticate each of them individually. The
|
85
|
+
access_id is sent in the clear so you should avoid making it an important string.
|
86
|
+
|
87
|
+
=== secret key
|
88
|
+
The secret key is the shared secret between the client and the server.
|
89
|
+
You should make this sufficiently random so that is can't be guessed or exposed
|
90
|
+
to dictionary attacks.
|
91
|
+
The follow code will give you a pretty good secret key:
|
92
|
+
|
93
|
+
random = File.read('/dev/random', 512)
|
94
|
+
secret_key = Base64.encode64(Digest::SHA2.new(512).digest(random))
|
95
|
+
|
96
|
+
=== Rails Integration
|
97
|
+
|
98
|
+
KingHmac::Auth supports authentication within Rails controllers and signing of
|
99
|
+
requests generated by Active Resource. See
|
100
|
+
KingHmac::Rails::ControllerFilter::ClassMethods and
|
101
|
+
KingHmac::Rails::ActiveResourceExtension::BaseHmac::ClassMethods for details.
|
102
|
+
|
103
|
+
== How does it work?
|
104
|
+
|
105
|
+
When creating a signature for a HTTP request KingHmac::Auth first generates a
|
106
|
+
canonical representation of the request.
|
107
|
+
|
108
|
+
This canonical string is created like so:
|
109
|
+
|
110
|
+
canonical_string = HTTP-Verb + "\n" +
|
111
|
+
Content-Type + "\n" +
|
112
|
+
Content-MD5 + "\n" +
|
113
|
+
Date + "\n" +
|
114
|
+
request-uri;
|
115
|
+
|
116
|
+
Where Content-Type, Content-MD5 and Date are all taken from the headers of the
|
117
|
+
request. If Content-Type or Content-MD5 are not present, they are substituted
|
118
|
+
with an empty string. If Date is not present it is added to the request headers
|
119
|
+
with the value +Time.now.httpdate+. +request-uri+ is the path component of the
|
120
|
+
request, without any query string, i.e. everything up to the ?.
|
121
|
+
|
122
|
+
This string is then used with the secret to generate a SHA1 HMAC using the
|
123
|
+
following:
|
124
|
+
|
125
|
+
OpenSSL::HMAC.digest(OpenSSL::Digest::Digest.new('sha1'), secret_key, canonical_string)
|
126
|
+
|
127
|
+
The result is then Base64 encoded and added to the headers of the request as the
|
128
|
+
+Authorization+ header in the format:
|
129
|
+
Authorization: KingHmac::Auth <access_id>:<base64 encoded king-hmac>
|
130
|
+
|
131
|
+
When authenaticating a request, KingHmac::Auth looks for the Authorization
|
132
|
+
header in the above format, parses out the components, regenerates a HMAC for
|
133
|
+
the request, using the secret key identified by the access id and then compares
|
134
|
+
the generated HMAC with the one provided by the client. If they match the
|
135
|
+
request is authenticated.
|
136
|
+
|
137
|
+
Using these details it is possible to build code that will sign and authenticate
|
138
|
+
KingHmac::Auth style requests in other languages.
|
139
|
+
|
140
|
+
|
141
|
+
== Authors and Contributors
|
142
|
+
|
143
|
+
This gem started with a copy & disection of auth-king-hmac gem v1.1.1.
|
144
|
+
Most of this doc was written by Sean Geoghegan
|
145
|
+
auth-king-hmac was developed by Sean Geoghegan http://rubyforge.org/projects/auth-king-hmac && by Peerworks[http://peerworks.org].
|
data/Rakefile
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
require 'rake/rdoctask'
|
4
|
+
require 'spec/rake/spectask'
|
5
|
+
|
6
|
+
begin
|
7
|
+
require 'jeweler'
|
8
|
+
Jeweler::Tasks.new do |gem|
|
9
|
+
gem.name = "king_hmac"
|
10
|
+
gem.summary = %Q{A Ruby Gem for authenticating HTTP requests using a HMAC}
|
11
|
+
gem.description = %Q{A Ruby Gem for authenticating HTTP requests using a HMAC}
|
12
|
+
gem.email = "gl@salesking.eu"
|
13
|
+
gem.homepage = "http://github.com/salesking/king_hmac"
|
14
|
+
gem.authors = ["Georg Leciejewski"]
|
15
|
+
gem.add_development_dependency "rspec", ">= 0"
|
16
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
17
|
+
end
|
18
|
+
Jeweler::GemcutterTasks.new
|
19
|
+
rescue LoadError
|
20
|
+
puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
|
21
|
+
end
|
22
|
+
|
23
|
+
desc 'Default: run specs.'
|
24
|
+
task :default => :spec
|
25
|
+
|
26
|
+
spec_files = Rake::FileList["spec/**/*_spec.rb"]
|
27
|
+
|
28
|
+
desc "Run specs"
|
29
|
+
Spec::Rake::SpecTask.new do |t|
|
30
|
+
t.spec_files = spec_files
|
31
|
+
t.spec_opts = ["-c"]
|
32
|
+
end
|
33
|
+
|
34
|
+
desc "Generate code coverage"
|
35
|
+
Spec::Rake::SpecTask.new(:coverage) do |t|
|
36
|
+
t.spec_files = spec_files
|
37
|
+
t.rcov = true
|
38
|
+
t.rcov_opts = ['--exclude', 'spec,/var/lib/gems']
|
39
|
+
end
|
40
|
+
|
41
|
+
desc 'Generate king_hmac documentation.'
|
42
|
+
Rake::RDocTask.new(:rdoc) do |rdoc|
|
43
|
+
rdoc.rdoc_dir = 'rdoc'
|
44
|
+
rdoc.title = 'king_hmac'
|
45
|
+
rdoc.options << '--line-numbers' << '--inline-source'
|
46
|
+
rdoc.rdoc_files.include('README')
|
47
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
48
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
1.0.0
|
data/init.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "#{File.dirname(__FILE__)}/lib/king_hmac"
|
data/king_hmac.gemspec
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{king_hmac}
|
8
|
+
s.version = "1.0.0"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Georg Leciejewski"]
|
12
|
+
s.date = %q{2010-04-10}
|
13
|
+
s.description = %q{A Ruby Gem for authenticating HTTP requests using a HMAC}
|
14
|
+
s.email = %q{gl@salesking.eu}
|
15
|
+
s.extra_rdoc_files = [
|
16
|
+
"README.rdoc"
|
17
|
+
]
|
18
|
+
s.files = [
|
19
|
+
".gitignore",
|
20
|
+
"MIT-LICENSE",
|
21
|
+
"README.rdoc",
|
22
|
+
"Rakefile",
|
23
|
+
"VERSION",
|
24
|
+
"init.rb",
|
25
|
+
"king_hmac.gemspec",
|
26
|
+
"lib/king_hmac.rb",
|
27
|
+
"lib/king_hmac/auth.rb",
|
28
|
+
"lib/king_hmac/cannonical_string.rb",
|
29
|
+
"lib/king_hmac/headers.rb",
|
30
|
+
"lib/king_hmac/rack/middleware.rb",
|
31
|
+
"lib/king_hmac/rails/active_resource.rb",
|
32
|
+
"lib/king_hmac/rails/controller.rb",
|
33
|
+
"spec/fixtures/credentials.yml",
|
34
|
+
"spec/king_hmac/king_hmac_spec.rb",
|
35
|
+
"spec/spec_helper.rb"
|
36
|
+
]
|
37
|
+
s.homepage = %q{http://github.com/salesking/king_hmac}
|
38
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
39
|
+
s.require_paths = ["lib"]
|
40
|
+
s.rubygems_version = %q{1.3.6}
|
41
|
+
s.summary = %q{A Ruby Gem for authenticating HTTP requests using a HMAC}
|
42
|
+
s.test_files = [
|
43
|
+
"spec/king_hmac/king_hmac_spec.rb",
|
44
|
+
"spec/spec_helper.rb"
|
45
|
+
]
|
46
|
+
|
47
|
+
if s.respond_to? :specification_version then
|
48
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
49
|
+
s.specification_version = 3
|
50
|
+
|
51
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
52
|
+
s.add_development_dependency(%q<rspec>, [">= 0"])
|
53
|
+
else
|
54
|
+
s.add_dependency(%q<rspec>, [">= 0"])
|
55
|
+
end
|
56
|
+
else
|
57
|
+
s.add_dependency(%q<rspec>, [">= 0"])
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
@@ -0,0 +1,143 @@
|
|
1
|
+
module KingHmac
|
2
|
+
# This module provides a HMAC Authentication method for HTTP requests. It should work with
|
3
|
+
# net/http request classes and CGIRequest classes and hence Rails.
|
4
|
+
#
|
5
|
+
# It is loosely based on the Amazon Web Services Authentication mechanism but
|
6
|
+
# generalized to be useful to any application that requires HMAC based authentication.
|
7
|
+
# As a result of the generalization, it won't work with AWS because it doesn't support
|
8
|
+
# the Amazon extension headers.
|
9
|
+
#
|
10
|
+
# === References
|
11
|
+
# Cryptographic Hash functions:: http://en.wikipedia.org/wiki/Cryptographic_hash_function
|
12
|
+
# SHA-1 Hash function:: http://en.wikipedia.org/wiki/SHA-1
|
13
|
+
# HMAC algorithm:: http://en.wikipedia.org/wiki/HMAC
|
14
|
+
# RFC 2104:: http://tools.ietf.org/html/rfc2104
|
15
|
+
#
|
16
|
+
class Auth
|
17
|
+
|
18
|
+
include KingHmac::Headers
|
19
|
+
|
20
|
+
|
21
|
+
@@default_signature_class = KingHmac::CanonicalString
|
22
|
+
|
23
|
+
# Create an KingHmac::Auth instance using the given credential store
|
24
|
+
#
|
25
|
+
# Credential Store:
|
26
|
+
# * Credential store must respond to the [] method and return a secret for access key id
|
27
|
+
#
|
28
|
+
# Options:
|
29
|
+
# Override default options
|
30
|
+
# * <tt>:service_id</tt> - Service ID used in the AUTHORIZATION header string. Default is KingHmac::Auth.
|
31
|
+
# * <tt>:signature_method</tt> - Proc object that takes request and produces the signature string
|
32
|
+
# used for authentication. Default is CanonicalString.
|
33
|
+
# Examples:
|
34
|
+
# my_hmac = KingHmac::Auth.new('access_id1' => 'secret1', 'access_id2' => 'secret2')
|
35
|
+
#
|
36
|
+
# cred_store = { 'access_id1' => 'secret1', 'access_id2' => 'secret2' }
|
37
|
+
# options = { :service_id => 'MyApp', :signature_method => lambda { |r| MyRequestString.new(r) } }
|
38
|
+
# my_hmac = KingHmac::Auth.new(cred_store, options)
|
39
|
+
#
|
40
|
+
def initialize(credential_store, options = nil)
|
41
|
+
@credential_store = credential_store
|
42
|
+
|
43
|
+
# Defaults
|
44
|
+
@service_id = self.class.name
|
45
|
+
@signature_class = @@default_signature_class
|
46
|
+
|
47
|
+
unless options.nil?
|
48
|
+
@service_id = options[:service_id] if options.key?(:service_id)
|
49
|
+
@signature_class = options[:signature] if options.key?(:signature) && options[:signature].is_a?(Class)
|
50
|
+
end
|
51
|
+
|
52
|
+
@signature_method = lambda { |r| @signature_class.send(:new, r) }
|
53
|
+
end
|
54
|
+
|
55
|
+
# Generates canonical signing string for given request
|
56
|
+
#
|
57
|
+
# Supports same options as KingHmac::Auth.initialize for overriding service_id and
|
58
|
+
# signature method.
|
59
|
+
#
|
60
|
+
def self.canonical_string(request, options = nil)
|
61
|
+
self.new(nil, options).canonical_string(request)
|
62
|
+
end
|
63
|
+
|
64
|
+
# Generates signature string for a given secret
|
65
|
+
#
|
66
|
+
# Supports same options as KingHmac::Auth.initialize for overriding service_id and
|
67
|
+
# signature method.
|
68
|
+
#
|
69
|
+
def self.signature(request, secret, options = nil)
|
70
|
+
self.new(nil, options).signature(request, secret)
|
71
|
+
end
|
72
|
+
|
73
|
+
# Signs a request using a given access key id and secret.
|
74
|
+
#
|
75
|
+
# Supports same options as KingHmac::Auth.initialize for overriding service_id and
|
76
|
+
# signature method.
|
77
|
+
#
|
78
|
+
def self.sign!(request, access_key_id, secret, options = nil)
|
79
|
+
credentials = { access_key_id => secret }
|
80
|
+
self.new(credentials, options).sign!(request, access_key_id)
|
81
|
+
end
|
82
|
+
|
83
|
+
# Authenticates a request using HMAC
|
84
|
+
#
|
85
|
+
# Supports same options as KingHmac::Auth.initialize for overriding service_id and
|
86
|
+
# signature method.
|
87
|
+
#
|
88
|
+
def self.authenticated?(request, access_key_id, secret, options)
|
89
|
+
credentials = { access_key_id => secret }
|
90
|
+
self.new(credentials, options).authenticated?(request)
|
91
|
+
end
|
92
|
+
|
93
|
+
# Signs a request using the access_key_id and the secret associated with that id
|
94
|
+
# in the credential store.
|
95
|
+
#
|
96
|
+
# Signing a requests adds an Authorization header to the request in the format:
|
97
|
+
#
|
98
|
+
# <service_id> <access_key_id>:<signature>
|
99
|
+
#
|
100
|
+
# where <signature> is the Base64 encoded HMAC-SHA1 of the CanonicalString and the secret.
|
101
|
+
#
|
102
|
+
def sign!(request, access_key_id)
|
103
|
+
secret = @credential_store[access_key_id]
|
104
|
+
raise ArgumentError, "No secret found for key id '#{access_key_id}'" if secret.nil?
|
105
|
+
request['Authorization'] = authorization(request, access_key_id, secret)
|
106
|
+
end
|
107
|
+
|
108
|
+
# Authenticates a request using HMAC
|
109
|
+
#
|
110
|
+
# Returns true if the request has an KingHmac::Auth Authorization header and
|
111
|
+
# the access id and HMAC match an id and HMAC produced for the secret
|
112
|
+
# in the credential store. Otherwise returns false.
|
113
|
+
#
|
114
|
+
def authenticated?(request)
|
115
|
+
rx = Regexp.new("#{@service_id} ([^:]+):(.+)$")
|
116
|
+
if md = rx.match(authorization_header(request))
|
117
|
+
access_key_id = md[1]
|
118
|
+
hmac = md[2]
|
119
|
+
secret = @credential_store[access_key_id]
|
120
|
+
!secret.nil? && hmac == signature(request, secret)
|
121
|
+
else
|
122
|
+
false
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def signature(request, secret)
|
127
|
+
digest = OpenSSL::Digest::Digest.new('sha1')
|
128
|
+
Base64.encode64(OpenSSL::HMAC.digest(digest, secret, canonical_string(request))).strip
|
129
|
+
end
|
130
|
+
|
131
|
+
def canonical_string(request)
|
132
|
+
@signature_method.call(request)
|
133
|
+
end
|
134
|
+
|
135
|
+
def authorization_header(request)
|
136
|
+
find_header(%w(Authorization HTTP_AUTHORIZATION), headers(request))
|
137
|
+
end
|
138
|
+
|
139
|
+
def authorization(request, access_key_id, secret)
|
140
|
+
"#{@service_id} #{access_key_id}:#{signature(request, secret)}"
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module KingHmac
|
2
|
+
# Build a Canonical String for a HTTP request.
|
3
|
+
#
|
4
|
+
# A Canonical String has the following format:
|
5
|
+
#
|
6
|
+
# CanonicalString = HTTP-Verb + "\n" +
|
7
|
+
# Content-Type + "\n" +
|
8
|
+
# Content-MD5 + "\n" +
|
9
|
+
# Date + "\n" +
|
10
|
+
# request-uri;
|
11
|
+
#
|
12
|
+
#
|
13
|
+
# If the Date header doesn't exist, one will be generated since
|
14
|
+
# Net/HTTP will generate one if it doesn't exist and it will be
|
15
|
+
# used on the server side to do authentication.
|
16
|
+
#
|
17
|
+
class CanonicalString < String # :nodoc:
|
18
|
+
include Headers
|
19
|
+
|
20
|
+
def initialize(request)
|
21
|
+
self << request_method(request) + "\n"
|
22
|
+
self << header_values(headers(request)) + "\n"
|
23
|
+
self << request_path(request)
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
def request_method(request)
|
28
|
+
if request.respond_to?(:request_method) && request.request_method.is_a?(String)
|
29
|
+
request.request_method
|
30
|
+
elsif request.respond_to?(:method) && request.method.is_a?(String)
|
31
|
+
request.method
|
32
|
+
elsif request.respond_to?(:env) && request.env
|
33
|
+
request.env['REQUEST_METHOD']
|
34
|
+
else
|
35
|
+
raise ArgumentError, "Don't know how to get the request method from #{request.inspect}"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def header_values(headers)
|
40
|
+
[ content_type(headers),
|
41
|
+
content_md5(headers),
|
42
|
+
(date(headers) or headers['Date'] = Time.now.utc.httpdate)
|
43
|
+
].join("\n")
|
44
|
+
end
|
45
|
+
|
46
|
+
def content_type(headers)
|
47
|
+
find_header(%w(CONTENT-TYPE CONTENT_TYPE HTTP_CONTENT_TYPE), headers)
|
48
|
+
end
|
49
|
+
|
50
|
+
def date(headers)
|
51
|
+
find_header(%w(DATE HTTP_DATE), headers)
|
52
|
+
end
|
53
|
+
|
54
|
+
def content_md5(headers)
|
55
|
+
find_header(%w(CONTENT-MD5 CONTENT_MD5), headers)
|
56
|
+
end
|
57
|
+
|
58
|
+
def request_path(request)
|
59
|
+
# Try unparsed_uri in case it is a Webrick request
|
60
|
+
path = if request.respond_to?(:unparsed_uri)
|
61
|
+
request.unparsed_uri
|
62
|
+
else
|
63
|
+
request.path
|
64
|
+
end
|
65
|
+
|
66
|
+
path[/^[^?]*/]
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module KingHmac
|
2
|
+
module Headers # :nodoc:
|
3
|
+
# Gets the headers for a request.
|
4
|
+
#
|
5
|
+
# Attempts to deal with known HTTP header representations in Ruby.
|
6
|
+
# Currently handles net/http and Rails.
|
7
|
+
#
|
8
|
+
def headers(request)
|
9
|
+
if request.respond_to?(:headers)
|
10
|
+
request.headers
|
11
|
+
elsif request.respond_to?(:env) && request.env
|
12
|
+
request.env
|
13
|
+
elsif request.respond_to?(:[])
|
14
|
+
request
|
15
|
+
else
|
16
|
+
raise ArgumentError, "Don't know how to get the headers from #{request.inspect}"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def find_header(keys, headers)
|
21
|
+
val = keys.map do |key|
|
22
|
+
headers[key]
|
23
|
+
end.compact.first
|
24
|
+
var = 'adsf'
|
25
|
+
val
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'rack/request'
|
2
|
+
module KingHmac
|
3
|
+
module Rack
|
4
|
+
class Middleware
|
5
|
+
|
6
|
+
# === Parameter
|
7
|
+
# opts<Hash>::
|
8
|
+
# === opts params:
|
9
|
+
# keys<Hash{String=>String}>:: Must be an array of accesskey=> secret
|
10
|
+
# respond to the [] method and return a secret for access key id
|
11
|
+
# only<Array[String]>:: path's to protect
|
12
|
+
def initialize(app, opts={})
|
13
|
+
@app = app
|
14
|
+
@opts = opts
|
15
|
+
@plain_error = "HMAC Authentication failed. Get yourself a valid HMAC Key .. Dude .. or ask your admin to get you some credentials"
|
16
|
+
@hmac_auth = KingHmac::Auth.new(@opts['keys'])
|
17
|
+
end
|
18
|
+
|
19
|
+
def call(env)
|
20
|
+
path = env['PATH_INFO']
|
21
|
+
do_hmac_check = @opts['only'].detect{|i| path.include?(i) }
|
22
|
+
if do_hmac_check
|
23
|
+
unless hmac_authenticated?(::Rack::Request.new(env))
|
24
|
+
headers = {'Content-Type' => "text/plain",
|
25
|
+
'Content-Length' => "#{@plain_error.length}",
|
26
|
+
'WWW-Authenticate' => 'AuthHMAC'
|
27
|
+
}
|
28
|
+
[401, headers, [@plain_error]]
|
29
|
+
else #valid credentials
|
30
|
+
@app.call(env)
|
31
|
+
end
|
32
|
+
else # unprotected
|
33
|
+
@app.call(env)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def hmac_authenticated?(request)
|
38
|
+
@hmac_auth.authenticated?(request)
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
module ActiveResourceExtension # :nodoc:
|
2
|
+
module BaseHmac # :nodoc:
|
3
|
+
def self.included(base)
|
4
|
+
base.extend(ClassMethods)
|
5
|
+
|
6
|
+
base.class_inheritable_accessor :hmac_access_id
|
7
|
+
base.class_inheritable_accessor :hmac_secret
|
8
|
+
base.class_inheritable_accessor :use_hmac
|
9
|
+
base.class_inheritable_accessor :hmac_options
|
10
|
+
end
|
11
|
+
|
12
|
+
module ClassMethods
|
13
|
+
# Call with an Active Resource class definition to sign
|
14
|
+
# all HTTP requests sent by that class with the provided
|
15
|
+
# credentials.
|
16
|
+
#
|
17
|
+
# Can be called with either a hash or two separate parameters
|
18
|
+
# like so:
|
19
|
+
#
|
20
|
+
# class MyResource < ActiveResource::Base
|
21
|
+
# with_auth_hmac("my_access_id", "my_secret")
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# or
|
25
|
+
#
|
26
|
+
# class MyOtherResource < ActiveResource::Base
|
27
|
+
# with_auth_hmac("my_access_id" => "my_secret")
|
28
|
+
# end
|
29
|
+
#
|
30
|
+
#
|
31
|
+
# This has only been tested with Rails 2.1 and since it is virtually a monkey
|
32
|
+
# patch of the internals of ActiveResource it might not work with past or
|
33
|
+
# future versions.
|
34
|
+
#
|
35
|
+
def with_auth_hmac(access_id, secret = nil, options = nil)
|
36
|
+
if access_id.is_a?(Hash)
|
37
|
+
self.hmac_access_id = access_id.keys.first
|
38
|
+
self.hmac_secret = access_id[self.hmac_access_id]
|
39
|
+
else
|
40
|
+
self.hmac_access_id = access_id
|
41
|
+
self.hmac_secret = secret
|
42
|
+
end
|
43
|
+
self.use_hmac = true
|
44
|
+
self.hmac_options = options
|
45
|
+
|
46
|
+
class << self
|
47
|
+
alias_method_chain :connection, :hmac
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def connection_with_hmac(refresh = false) # :nodoc:
|
52
|
+
c = connection_without_hmac(refresh)
|
53
|
+
c.hmac_access_id = self.hmac_access_id
|
54
|
+
c.hmac_secret = self.hmac_secret
|
55
|
+
c.use_hmac = self.use_hmac
|
56
|
+
c.hmac_options = self.hmac_options
|
57
|
+
c
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
module InstanceMethods # :nodoc:
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
module Connection # :nodoc:
|
66
|
+
def self.included(base)
|
67
|
+
base.send :alias_method_chain, :request, :hmac
|
68
|
+
base.class_eval do
|
69
|
+
attr_accessor :hmac_secret, :hmac_access_id, :use_hmac, :hmac_options
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def request_with_hmac(method, path, *arguments)
|
74
|
+
if use_hmac && hmac_access_id && hmac_secret
|
75
|
+
arguments.last['Date'] = Time.now.httpdate if arguments.last['Date'].nil?
|
76
|
+
temp = "Net::HTTP::#{method.to_s.capitalize}".constantize.new(path, arguments.last)
|
77
|
+
AuthHMAC.sign!(temp, hmac_access_id, hmac_secret, hmac_options)
|
78
|
+
arguments.last['Authorization'] = temp['Authorization']
|
79
|
+
end
|
80
|
+
|
81
|
+
request_without_hmac(method, path, *arguments)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
unless defined?(ActiveResource)
|
86
|
+
begin
|
87
|
+
require 'rubygems'
|
88
|
+
gem 'activeresource'
|
89
|
+
require 'activeresource'
|
90
|
+
rescue
|
91
|
+
nil
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
if defined?(ActiveResource)
|
96
|
+
ActiveResource::Base.send(:include, BaseHmac)
|
97
|
+
ActiveResource::Connection.send(:include, Connection)
|
98
|
+
end
|
99
|
+
end
|