jmoses_api-auth 1.0.4

Sign up to get free protection for your applications and to get access to all the features.
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