jmoses_api-auth 1.0.4

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/.document ADDED
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
data/.gitignore ADDED
@@ -0,0 +1,44 @@
1
+ .rvmrc
2
+
3
+ # rcov generated
4
+ coverage
5
+
6
+ # rdoc generated
7
+ rdoc
8
+
9
+ # yard generated
10
+ doc
11
+ .yardoc
12
+
13
+ # bundler
14
+ .bundle
15
+
16
+ # jeweler generated
17
+ pkg
18
+
19
+ # Have editor/IDE/OS specific files you need to ignore? Consider using a global gitignore:
20
+ #
21
+ # * Create a file at ~/.gitignore
22
+ # * Include files you want ignored
23
+ # * Run: git config --global core.excludesfile ~/.gitignore
24
+ #
25
+ # After doing this, these files will be ignored in all your git projects,
26
+ # saving you from having to 'pollute' every project you touch with them
27
+ #
28
+ # Not sure what to needs to be ignored for particular editors/OSes? Here's some ideas to get you started. (Remember, remove the leading # of the line)
29
+ #
30
+ # For MacOS:
31
+ #
32
+ #.DS_Store
33
+ #
34
+ # For TextMate
35
+ #*.tmproj
36
+ #tmtags
37
+ #
38
+ # For emacs:
39
+ #*~
40
+ #\#*
41
+ #.\#*
42
+ #
43
+ # For vim:
44
+ #*.swp
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --colour
2
+ --format nested
3
+ --backtrace
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,46 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ api-auth (1.0.3)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ actionpack (2.3.14)
10
+ activesupport (= 2.3.14)
11
+ rack (~> 1.1.0)
12
+ activeresource (2.3.14)
13
+ activesupport (= 2.3.14)
14
+ activesupport (2.3.14)
15
+ amatch (0.2.10)
16
+ tins (~> 0.3)
17
+ curb (0.8.1)
18
+ diff-lcs (1.1.3)
19
+ mime-types (1.17.2)
20
+ rack (1.1.3)
21
+ rake (0.9.2.2)
22
+ rest-client (1.6.7)
23
+ mime-types (>= 1.16)
24
+ rspec (2.4.0)
25
+ rspec-core (~> 2.4.0)
26
+ rspec-expectations (~> 2.4.0)
27
+ rspec-mocks (~> 2.4.0)
28
+ rspec-core (2.4.0)
29
+ rspec-expectations (2.4.0)
30
+ diff-lcs (~> 1.1.2)
31
+ rspec-mocks (2.4.0)
32
+ tins (0.5.5)
33
+
34
+ PLATFORMS
35
+ ruby
36
+
37
+ DEPENDENCIES
38
+ actionpack (~> 2.3.2)
39
+ activeresource (~> 2.3.2)
40
+ activesupport (~> 2.3.2)
41
+ amatch
42
+ api-auth!
43
+ curb (~> 0.8.1)
44
+ rake
45
+ rest-client (~> 1.6.0)
46
+ rspec (~> 2.4.0)
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2012 Gemini SBS LLC
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,185 @@
1
+ # ApiAuth #
2
+
3
+ Logins and passwords are for humans. Communication between applications need to
4
+ be protected through different means.
5
+
6
+ ApiAuth is a Ruby gem designed to be used both in your client and server
7
+ HTTP-based applications. It implements the same authentication methods (HMAC-SHA1)
8
+ used by Amazon Web Services.
9
+
10
+ The gem will sign your requests on the client side and authenticate that
11
+ signature on the server side. If your server resources are implemented as a
12
+ Rails ActiveResource, it will integrate with that. It will even generate the
13
+ secret keys necessary for your clients to sign their requests.
14
+
15
+ Since it operates entirely using HTTP headers, the server component does not
16
+ have to be written in the same language as the clients.
17
+
18
+ ## How it works ##
19
+
20
+ 1. A canonical string is first created using your HTTP headers containing the
21
+ content-type, content-MD5, request URI and the timestamp. If content-type or
22
+ content-MD5 are not present, then a blank string is used in their place. If the
23
+ timestamp isn't present, a valid HTTP date is automatically added to the
24
+ request. The canonical string string is computed as follows:
25
+
26
+ canonical_string = 'content-type,content-MD5,request URI,timestamp'
27
+
28
+ 2. This string is then used to create the signature which is a Base64 encoded
29
+ SHA1 HMAC, using the client's private secret key.
30
+
31
+ 3. This signature is then added as the `Authorization` HTTP header in the form:
32
+
33
+ Authorization = APIAuth 'client access id':'signature from step 2'
34
+
35
+ 5. On the server side, the SHA1 HMAC is computed in the same way using the
36
+ request headers and the client's secret key, which is known to only
37
+ the client and the server but can be looked up on the server using the client's
38
+ access id that was attached in the header. The access id can be any integer or
39
+ string that uniquely identifies the client. The signed request expires after 15
40
+ minutes in order to avoid replay attacks.
41
+
42
+
43
+ ## References ##
44
+
45
+ * [Hash functions](http://en.wikipedia.org/wiki/Cryptographic_hash_function)
46
+ * [SHA-1 Hash function](http://en.wikipedia.org/wiki/SHA-1)
47
+ * [HMAC algorithm](http://en.wikipedia.org/wiki/HMAC)
48
+ * [RFC 2104 (HMAC)](http://tools.ietf.org/html/rfc2104)
49
+
50
+ ## Install ##
51
+
52
+ The gem doesn't have any dependencies outside of having a working OpenSSL
53
+ configuration for your Ruby VM. To install:
54
+
55
+ [sudo] gem install api-auth
56
+
57
+ Please note the dash in the name versus the underscore.
58
+
59
+ ## Clients ##
60
+
61
+ ApiAuth supports many popular HTTP clients. Support for other clients can be
62
+ added as a request driver.
63
+
64
+ Here is the current list of supported request objects:
65
+
66
+ * Net::HTTP
67
+ * ActionController::Request
68
+ * Curb (Curl::Easy)
69
+ * RestClient
70
+
71
+ ### HTTP Client Objects ###
72
+
73
+ Here's a sample implementation of signing a request created with RestClient. For
74
+ more examples, please check out the ApiAuth Spec where every supported HTTP
75
+ client is tested.
76
+
77
+ Assuming you have a client access id and secret as follows:
78
+
79
+ ``` ruby
80
+ @access_id = "1044"
81
+ @secret_key = ApiAuth.generate_secret_key
82
+ ```
83
+
84
+ A typical RestClient PUT request may look like:
85
+
86
+ ``` ruby
87
+ headers = { 'Content-MD5' => "e59ff97941044f85df5297e1c302d260",
88
+ 'Content-Type' => "text/plain",
89
+ 'Date' => "Mon, 23 Jan 1984 03:29:56 GMT" }
90
+ @request = RestClient::Request.new(:url => "/resource.xml?foo=bar&bar=foo",
91
+ :headers => headers,
92
+ :method => :put)
93
+ ```
94
+
95
+ To sign that request, simply call the `sign!` method as follows:
96
+
97
+ ``` ruby
98
+ @signed_request = ApiAuth.sign!(@request, @access_id, @secret_key)
99
+ ```
100
+
101
+ The proper `Authorization` request header has now been added to that request
102
+ object and it's ready to be transmitted. It's recommended that you sign the
103
+ request as one of the last steps in building the request to ensure the headers
104
+ don't change after the signing process which would cause the authentication
105
+ check to fail on the server side.
106
+
107
+ ### ActiveResource Clients ###
108
+
109
+ ApiAuth can transparently protect your ActiveResource communications with a
110
+ single configuration line:
111
+
112
+ ``` ruby
113
+ class MyResource < ActiveResource::Base
114
+ with_api_auth(access_id, secret_key)
115
+ end
116
+ ```
117
+
118
+ This will automatically sign all outgoing ActiveResource requests from your app.
119
+
120
+ ## Server ##
121
+
122
+ ApiAuth provides some built in methods to help you generate API keys for your
123
+ clients as well as verifying incoming API requests.
124
+
125
+ To generate a Base64 encoded API key for a client:
126
+
127
+ ``` ruby
128
+ ApiAuth.generate_secret_key
129
+ ```
130
+
131
+ To validate whether or not a request is authentic:
132
+
133
+ ``` ruby
134
+ ApiAuth.authentic?(signed_request, secret_key)
135
+ ```
136
+
137
+ If your server is a Rails app, the signed request will be the `request` object.
138
+
139
+ In order to obtain the secret key for the client, you first need to look up the
140
+ client's access_id. ApiAuth can pull that from the request headers for you:
141
+
142
+ ``` ruby
143
+ ApiAuth.access_id(signed_request)
144
+ ```
145
+
146
+ Once you've looked up the client's record via the access id, you can then verify
147
+ whether or not the request is authentic. Typically, the access id for the client
148
+ will be their record's primary key in the DB that stores the record or some other
149
+ public unique identifier for the client.
150
+
151
+ Here's a sample method that can be used in a `before_filter` if your server is a
152
+ Rails app:
153
+
154
+ ``` ruby
155
+ before_filter :api_authenticate
156
+
157
+ def api_authenticate
158
+ @current_account = Account.find_by_access_id(ApiAuth.access_id(request))
159
+ return ApiAuth.authentic?(request, @current_account.secret_key) unless @current_account.nil?
160
+ false
161
+ end
162
+ ```
163
+
164
+ ## Development ##
165
+
166
+ ApiAuth uses bundler for gem dependencies and RSpec for testing. Developing the
167
+ gem requires that you have all supported HTTP clients installed. Bundler will
168
+ take care of all that for you.
169
+
170
+ To run the tests:
171
+
172
+ rake spec
173
+
174
+ If you'd like to add support for additional HTTP clients, check out the already
175
+ implemented drivers in `lib/api_auth/request_drivers` for reference. All of
176
+ the public methods for each driver are required to be implemented by your driver.
177
+
178
+ ## Authors ##
179
+
180
+ * [Mauricio Gomes](http://github.com/mgomes)
181
+ * [Kevin Glowacz](http://github.com/kjg)
182
+
183
+ ## Copyright ##
184
+
185
+ Copyright (c) 2012 Gemini SBS LLC. See LICENSE.txt for further details.
data/Rakefile ADDED
@@ -0,0 +1,22 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
3
+
4
+ require 'rake'
5
+ require 'rspec/core'
6
+ require 'rspec/core/rake_task'
7
+
8
+ RSpec::Core::RakeTask.new(:spec) do |spec|
9
+ spec.pattern = FileList['spec/**/*_spec.rb']
10
+ end
11
+
12
+ task :default => :spec
13
+
14
+ require 'rake/rdoctask'
15
+ Rake::RDocTask.new do |rdoc|
16
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
17
+
18
+ rdoc.rdoc_dir = 'rdoc'
19
+ rdoc.title = "test #{version}"
20
+ rdoc.rdoc_files.include('README*')
21
+ rdoc.rdoc_files.include('lib/**/*.rb')
22
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.0.4
data/api_auth.gemspec ADDED
@@ -0,0 +1,26 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = %q{jmoses_api-auth}
6
+ s.summary = %q{Simple HMAC authentication for your APIs (fork by jmoses)}
7
+ s.description = %q{Full HMAC auth implementation for use in your gems and Rails apps.}
8
+ s.homepage = %q{https://github.com/jmoses/api_auth}
9
+ s.version = File.read(File.join(File.dirname(__FILE__), 'VERSION'))
10
+ s.authors = ['Jon Moses', "Mauricio Gomes"]
11
+ s.email = ['jon@burningbush.us', "mauricio@edge14.com"]
12
+
13
+ s.add_development_dependency "rake"
14
+ s.add_development_dependency "amatch"
15
+ s.add_development_dependency "rspec", "~> 2.4.0"
16
+ s.add_development_dependency "actionpack", "~> 2.3.2"
17
+ s.add_development_dependency "activesupport", "~> 2.3.2"
18
+ s.add_development_dependency "activeresource", "~> 2.3.2"
19
+ s.add_development_dependency "rest-client", "~> 1.6.0"
20
+ s.add_development_dependency "curb", "~> 0.8.1"
21
+
22
+ s.files = `git ls-files`.split("\n")
23
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
24
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
25
+ s.require_paths = ["lib"]
26
+ end
data/lib/api-auth.rb ADDED
@@ -0,0 +1,2 @@
1
+ # So you can require "api-auth" instead of "api_auth"
2
+ require "api_auth"
@@ -0,0 +1,97 @@
1
+ # api-auth is Ruby gem designed to be used both in your client and server
2
+ # HTTP-based applications. It implements the same authentication methods (HMAC)
3
+ # used by Amazon Web Services.
4
+
5
+ # The gem will sign your requests on the client side and authenticate that
6
+ # signature on the server side. If your server resources are implemented as a
7
+ # Rails ActiveResource, it will integrate with that. It will even generate the
8
+ # secret keys necessary for your clients to sign their requests.
9
+ module ApiAuth
10
+
11
+ class << self
12
+
13
+ include Helpers
14
+
15
+ # Signs an HTTP request using the client's access id and secret key.
16
+ # Returns the HTTP request object with the modified headers.
17
+ #
18
+ # request: The request can be a Net::HTTP, ActionController::Request,
19
+ # Curb (Curl::Easy) or a RestClient object.
20
+ #
21
+ # access_id: The public unique identifier for the client
22
+ #
23
+ # secret_key: assigned secret key that is known to both parties
24
+ def sign!(request, access_id, secret_key)
25
+ headers = Headers.new(request)
26
+ headers.calculate_md5
27
+ headers.set_date
28
+ headers.sign_header auth_header(request, access_id, secret_key)
29
+ end
30
+
31
+ # Determines if the request is authentic given the request and the client's
32
+ # secret key. Returns true if the request is authentic and false otherwise.
33
+ def authentic?(request, secret_key)
34
+ return false if secret_key.nil?
35
+
36
+ return !md5_mismatch?(request) && signatures_match?(request, secret_key) && !request_too_old?(request)
37
+ end
38
+
39
+ # Returns the access id from the request's authorization header
40
+ def access_id(request)
41
+ headers = Headers.new(request)
42
+ if match_data = parse_auth_header(headers.authorization_header)
43
+ return match_data[1]
44
+ end
45
+
46
+ nil
47
+ end
48
+
49
+ # Generates a Base64 encoded, randomized secret key
50
+ #
51
+ # Store this key along with the access key that will be used for
52
+ # authenticating the client
53
+ def generate_secret_key
54
+ random_bytes = OpenSSL::Random.random_bytes(512)
55
+ b64_encode(Digest::SHA2.new(512).digest(random_bytes))
56
+ end
57
+
58
+ private
59
+
60
+ def request_too_old?(request)
61
+ headers = Headers.new(request)
62
+ # 900 seconds is 15 minutes
63
+ Time.parse(headers.timestamp).utc < (Time.now.utc - 900)
64
+ end
65
+
66
+ def md5_mismatch?(request)
67
+ headers = Headers.new(request)
68
+ headers.md5_mismatch?
69
+ end
70
+
71
+ def signatures_match?(request, secret_key)
72
+ headers = Headers.new(request)
73
+ if match_data = parse_auth_header(headers.authorization_header)
74
+ hmac = match_data[2]
75
+ return hmac == hmac_signature(request, secret_key)
76
+ end
77
+ false
78
+ end
79
+
80
+ def hmac_signature(request, secret_key)
81
+ headers = Headers.new(request)
82
+ canonical_string = headers.canonical_string
83
+ digest = OpenSSL::Digest::Digest.new('sha1')
84
+ b64_encode(OpenSSL::HMAC.digest(digest, secret_key, canonical_string))
85
+ end
86
+
87
+ def auth_header(request, access_id, secret_key)
88
+ "APIAuth #{access_id}:#{hmac_signature(request, secret_key)}"
89
+ end
90
+
91
+ def parse_auth_header(auth_header)
92
+ Regexp.new("APIAuth ([^:]+):(.+)$").match(auth_header)
93
+ end
94
+
95
+ end # class methods
96
+
97
+ end # ApiAuth
@@ -0,0 +1,9 @@
1
+ module ApiAuth
2
+
3
+ # :nodoc:
4
+ class ApiAuthError < StandardError; end
5
+
6
+ # Raised when the HTTP request object passed is not supported
7
+ class UnknownHTTPRequest < ApiAuthError; end
8
+
9
+ end
@@ -0,0 +1,82 @@
1
+ module ApiAuth
2
+
3
+ # Builds the canonical string given a request object.
4
+ class Headers
5
+
6
+ include RequestDrivers
7
+
8
+ def initialize(request)
9
+ @original_request = request
10
+
11
+ case request.class.to_s
12
+ when /Net::HTTP/
13
+ @request = NetHttpRequest.new(request)
14
+ when /RestClient/
15
+ @request = RestClientRequest.new(request)
16
+ when /Curl::Easy/
17
+ @request = CurbRequest.new(request)
18
+ when /ActionController::Request/
19
+ @request = ActionControllerRequest.new(request)
20
+ when /ActionController::TestRequest/
21
+ if defined?(ActionDispatch)
22
+ @request = ActionDispatchRequest.new(request)
23
+ else
24
+ @request = ActionControllerRequest.new(request)
25
+ end
26
+ when /ActionDispatch::Request/
27
+ @request = ActionDispatchRequest.new(request)
28
+ when /Rack::Request/
29
+ @request = RackRequest.new(request)
30
+ else
31
+ raise UnknownHTTPRequest, "#{request.class.to_s} is not yet supported."
32
+ end
33
+ true
34
+ end
35
+
36
+ # Returns the request timestamp
37
+ def timestamp
38
+ @request.timestamp
39
+ end
40
+
41
+ # Returns the canonical string computed from the request's headers
42
+ def canonical_string
43
+ [ @request.content_type,
44
+ @request.content_md5,
45
+ @request.request_uri.gsub(/http:\/\/[^(,|\?|\/)]*/,''), # remove host
46
+ @request.timestamp
47
+ ].join(",")
48
+ end
49
+
50
+ # Returns the authorization header from the request's headers
51
+ def authorization_header
52
+ @request.authorization_header
53
+ end
54
+
55
+ def set_date
56
+ @request.set_date if @request.timestamp.blank?
57
+ end
58
+
59
+ def calculate_md5
60
+ @request.populate_content_md5 if @request.content_md5.blank?
61
+ end
62
+
63
+ def md5_mismatch?
64
+ if @request.content_md5.blank?
65
+ false
66
+ else
67
+ @request.md5_mismatch?
68
+ end
69
+ end
70
+
71
+ # Sets the request's authorization header with the passed in value.
72
+ # The header should be the ApiAuth HMAC signature.
73
+ #
74
+ # This will return the original request object with the signed Authorization
75
+ # header already in place.
76
+ def sign_header(header)
77
+ @request.set_auth_header header
78
+ end
79
+
80
+ end
81
+
82
+ end
@@ -0,0 +1,18 @@
1
+ module ApiAuth
2
+
3
+ module Helpers # :nodoc:
4
+
5
+ def b64_encode(string)
6
+ Base64.strict_encode64(string)
7
+ end
8
+
9
+ # Capitalizes the keys of a hash
10
+ def capitalize_keys(hsh)
11
+ capitalized_hash = {}
12
+ hsh.each_pair {|k,v| capitalized_hash[k.to_s.upcase] = v }
13
+ capitalized_hash
14
+ end
15
+
16
+ end
17
+
18
+ end