hash-auth 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2013 YOURNAME
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,201 @@
1
+ # HashAuth
2
+ [![Code Climate](https://codeclimate.com/github/maxwells/hash-auth.png)](https://codeclimate.com/github/maxwells/hash-auth)
3
+ [![Dependency Status](https://gemnasium.com/maxwells/hash-auth.png)](https://gemnasium.com/maxwells/hash-auth)
4
+ [![Build Status](https://travis-ci.org/maxwells/hash-auth.png?branch=master)](https://travis-ci.org/maxwells/hash-auth)
5
+
6
+ HashAuth allows your Rails application to support incoming and outgoing two-factor authentication via hashing some component of an HTTPS request. Both sides of the request (your Rails app and your client or provider) must have some unique shared secret. This secret is used to create a hash of some portion of the request, ensuring that (if neither side has been compromised) only the other party could have created the request.
7
+
8
+ Solely using a shared key leaves one hole open: the ability for a third party to send a duplicate request if they are playing man in the middle, so it is important to combine the secret key with some unique data (eg. request IP and datetime) to reduce the scope of when and from where a given request is valid. Again, this only applies to duplicate requests.
9
+
10
+ _Note: Only Ruby 1.9.2 and above are supported, due to lack of ordered Hash objects in previous versions._
11
+
12
+ ## Features
13
+ - HashAuth can be configured to support multiple clients, each with their own authentication blocks (ie. customer 1 could use MD5 hash, customer 2 could use hmac-SHA256).
14
+ - Clients can be authenticated as a proxy user upon successful hash authentication (ie. if your controller action depends on having current_user, you can assign an email address to your client and have it log in that user)
15
+ - Custom blocks can be provided to (a) acquire the string to hash, (b) hash the string, or (c) perform a custom action upon authentication from a request from each indivudual client
16
+ - Enhanced security can be enabled by requiring each client to submit a GMT version of their system time to be included in the hash, which will mean any given request is only valid within a predefined window (reduces the possibility of a man in the middle attack through duplicate requests)
17
+
18
+ ## Usage
19
+
20
+ ### Installation
21
+
22
+ 1) Install the HashAuth gem from RubyGems
23
+
24
+ $ gem install hash-auth
25
+
26
+ 2) Add it to your Rails application's Gemfile
27
+
28
+ gem "hash-auth"
29
+
30
+ 3) Install it into your Rails application
31
+
32
+ $ rails g hashauth:install
33
+
34
+ 4) If you need to create your own strategies
35
+
36
+ $ rails g hashauth:strategy [name]
37
+
38
+ ### Configuration
39
+
40
+ The install generator will place an initializer (hashauth.rb) into your config/initializers directory. The following will walk you through the default configuration options and what they mean
41
+
42
+
43
+ **_Adding an authentication strategy._**
44
+
45
+ Generate a new strategy, which will live in lib/hash_auth/strategies
46
+
47
+ rails g hash_auth:strategy name_of_strategy
48
+
49
+ This will generate a template that needs to be filled in with the necessary behavior for authenticating your client. Here is an example strategy
50
+
51
+
52
+ ```ruby
53
+
54
+ module HashAuth
55
+ module Strategies
56
+ class NameOfStrategy < Base
57
+
58
+ def name
59
+ :name_of_strategy
60
+ end
61
+
62
+ ## The string that your client hashes for its signature is a concatenation of parameters in order, joined by '&' and appended with the client's secret key.
63
+ def acquire_string_to_hash(controller, client)
64
+ controller.params.select{|k,v| k != 'controller' && k != 'action' }.map{|k,v| "#{k}=#{v}"}.join('&') + client.customer_key
65
+ end
66
+
67
+ # Client hashes string with SHA256
68
+ def hash_string(string, client)
69
+ Digest::sha2.new(256) << string
70
+ end
71
+
72
+ def on_authentication(client)
73
+ # Do nothing. If you were so inclined, you could use the client information to do something specific to your system (like logging in a proxy user for your API client with your favorite user management system)
74
+ end
75
+
76
+ end
77
+ end
78
+ end
79
+
80
+ ```
81
+
82
+ **_Adding Clients via hardcoding (Config)._**
83
+
84
+
85
+ ```ruby
86
+
87
+ #### Adding a new client
88
+
89
+ # Add a client to a strategy. Any key:value sets can be added to the hash, which will be accessible in your strategy. The required ones are shown below (though there are default options for customer_identifier_param and strategy)
90
+ add_client {
91
+ :customer_key => '1234567890',
92
+ # the shared secret between you and a client
93
+ :customer_identifier => 'my_organization',
94
+ # the unique identifer the client will pass you to identify themselves
95
+ :customer_identifier_param => 'customer_id',
96
+ # the name of the parameter the client will pass their unique identifier in
97
+ :valid_domains => '*my_organization.org',
98
+ # will allow request from anything ending with my_organization.org, can also provide a list
99
+ :strategy => :my_auth_strategy,
100
+ # If no strategy is provided, then the default (HashAuth::Strategies::Default) will be used. If the strategy symbol does not reference a valid strategy, then an exception will be raised
101
+ }
102
+ ```
103
+ **_Adding Clients from an external resource (eg YAML, Database)._**
104
+
105
+ YAML file (config/clients.yml in this example):
106
+
107
+ clients:
108
+ -
109
+ customer_key: 1234567890
110
+ customer_identifier: my_organization
111
+ customer_identifier_param: customer_id
112
+ valid_domains: '*my_organization.org'
113
+ strategy: :default
114
+ custom_key: custom_value
115
+ -
116
+ customer_key: 0987654321
117
+ customer_identifier: your_organization
118
+ customer_identifier_param: customer_id
119
+ valid_domains: ['your_organization.com', 'your_organization.org']
120
+ strategy: :my_auth_strategy
121
+ custom_key: custom_value
122
+
123
+ hash-auth initializer:
124
+
125
+ ```ruby
126
+
127
+ clients = YAML::load( File.open('config/clients.yml') )
128
+ add_clients clients["clients"]
129
+
130
+ ```
131
+
132
+ **_Options in hash-auth initializer_**
133
+
134
+ Any custom client field can be initialized with a default value through method missing
135
+
136
+ HashAuth.configure do
137
+ set_default_authentication_success_status_message {:status => "success" }
138
+ end
139
+
140
+ will allow that value to be used in blocks later without initializing them in every client object. Ie. you could have 5 clients, three of which have a custom failure_json value in their definition and two of which will then use the default.
141
+
142
+ ## In a custom strategy…
143
+
144
+ def self.on_failure(client, controller)
145
+ @failed_authentication_status = {:status => 'failure'}
146
+ end
147
+
148
+ ## In the controller…
149
+ def my_action
150
+ if (@authenticated)
151
+ ... Do necessary stuff
152
+ response = @client.authentication_success_status_message
153
+ else
154
+ response = @failed_authentication_json
155
+ end
156
+
157
+ respond_to do |format|
158
+ format.json { render :json => response }
159
+ end
160
+ end
161
+
162
+ Additionally, the default strategy for every client can be set (if not set, will revert to HashAuth::Strategies::Default)
163
+
164
+ set_default_strategy :strategy_identifier
165
+
166
+
167
+ #### Implementation: _Receiving hashed requests_
168
+
169
+ In whatever controller(s) require hash authentication of requests
170
+
171
+ validates_auth_for :action_one, :action_two
172
+
173
+ The following variables are available in implementing controller actions
174
+
175
+ @client : HashAuth::Client - instance of client (if found, whether or not authenticated)
176
+ @authenticated : Boolean - whether or not the hashed request was considered validated
177
+
178
+
179
+ #### Implementation: _Making hashed requests_
180
+ In whatever controllers, models, or otherwise that require creating hash authenticated requests, use the HashAuth::WebRequest around [REST client](https://github.com/rest-client/rest-client).
181
+
182
+ client = HashAuth.find_client 'my_organization'
183
+ HashAuth::WebRequest.post client, 'localhost:3000/test/one', {:foo => :bar, :bar => :baz}
184
+
185
+ WebRequest supports:
186
+
187
+ def self.get(client, url, parameters = {}, &block)
188
+ def self.post(client, url, payload, headers = {}, &block)
189
+ def self.patch(client, url, payload, headers = {}, &block)
190
+ def self.put(client, url, payload, headers = {}, &block)
191
+ def self.delete(client, url, parameters = {}, &block)
192
+ def self.head(client, url, parameters = {}, &block)
193
+ def self.options(client, url, parameters = {}, &block)
194
+
195
+ See [REST client](https://github.com/rest-client/rest-client) for futher detail.
196
+
197
+
198
+ ## Examples
199
+
200
+
201
+ This project rocks and uses MIT-LICENSE.
data/Rakefile ADDED
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env rake
2
+ require 'rspec/core/rake_task'
3
+
4
+ begin
5
+ require 'bundler/setup'
6
+ rescue LoadError
7
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
8
+ end
9
+ begin
10
+ require 'rdoc/task'
11
+ rescue LoadError
12
+ require 'rdoc/rdoc'
13
+ require 'rake/rdoctask'
14
+ RDoc::Task = Rake::RDocTask
15
+ end
16
+
17
+ RDoc::Task.new(:rdoc) do |rdoc|
18
+ rdoc.rdoc_dir = 'rdoc'
19
+ rdoc.title = 'Specifind'
20
+ rdoc.options << '--line-numbers'
21
+ rdoc.rdoc_files.include('README.rdoc')
22
+ rdoc.rdoc_files.include('lib/**/*.rb')
23
+ end
24
+
25
+
26
+ desc "Run all metrics"
27
+ task :metrics do
28
+ puts "Generating Metrics with metric_fu.\nCheck your browser for output."
29
+ `metric_fu -r`
30
+ end
31
+
32
+ Bundler::GemHelper.install_tasks
33
+
34
+ desc "Run all specs"
35
+ RSpec::Core::RakeTask.new(:spec)
36
+
37
+ task :default => :spec
@@ -0,0 +1,11 @@
1
+ class HashAuth::InstallGenerator < ::Rails::Generators::Base
2
+ include Rails::Generators::Migration
3
+
4
+ source_root File.expand_path('../templates', __FILE__)
5
+ desc "Installs HashAuth."
6
+
7
+ def install
8
+ template "initializer.rb", "config/initializers/hash-auth.rb"
9
+ end
10
+
11
+ end
@@ -0,0 +1,12 @@
1
+ class HashAuth::StrategyGenerator < ::Rails::Generators::NamedBase
2
+ include Rails::Generators::Migration
3
+
4
+ source_root File.expand_path('../templates', __FILE__)
5
+
6
+ desc "Generates a HashAuth strategy."
7
+
8
+ def strategy
9
+ template "strategy.rb", "lib/hash-auth/#{file_name}.rb"
10
+ end
11
+
12
+ end
@@ -0,0 +1,26 @@
1
+ HashAuth.configure do
2
+
3
+ ## Block to allow dynamic loading of customer keys (Optional)
4
+ #### Could be from YAML
5
+ #### Could be from DB
6
+
7
+ # add_clients do |clients|
8
+ # YAML::load( File.open('../clients.yml') )
9
+ # end
10
+
11
+
12
+ ## Any attributes can be added to a client object at initialization.
13
+ #### The default can be added to HashAuth configuration and will be picked up in every strategy automagically
14
+ #### Default values must be set with set_default_* methods and will be picked up by [client].* methods
15
+ # eg.
16
+ # set_default_foo_bar 'bar baz'
17
+ #
18
+ # client.foo_bar will return 'bar baz' unless specifically set for that client
19
+
20
+
21
+ ## Set default strategy by handle
22
+ # set_default_strategy :strategy_fifty_one
23
+
24
+
25
+
26
+ end
@@ -0,0 +1,47 @@
1
+ require 'digest'
2
+
3
+ module HashAuth
4
+ module Strategies
5
+ class <%= class_name %> < Base
6
+ # provide the HashAuth system with a handle for this strategy
7
+ def self.identifier
8
+ :<%= file_name %>
9
+ end
10
+
11
+ # upon receiving a hashed request, extract the string that needs to be hashed and compared to the signature passed
12
+ def self.acquire_string_to_hash(controller, client)
13
+
14
+ end
15
+
16
+ # how the querystring should be hashed (eg. hmac-sha1, md5)
17
+ def self.hash_string(client, string)
18
+
19
+ end
20
+
21
+ # determine equality of the target string, as calculated by acquire_string_to_hash -> hash_string, with the actual signature passed by client
22
+ def self.verify_hash(target_string, client, controller)
23
+
24
+ end
25
+
26
+ # anything special that needs to be done upon successful authentication of a request (eg. log in proxy user for a given client)
27
+ def self.on_authentication(client, controller)
28
+
29
+ end
30
+
31
+ # on_failure is triggered during the before_filter of an action that requires hash authentication
32
+ # if any of the cases are met (these are the options for type):
33
+ # - :no_matching_client
34
+ # - :invalid_domain
35
+ # - :invalid_hash
36
+ def self.on_failure(client, controller, type)
37
+
38
+ end
39
+
40
+ # sign_request should sign the outgoing request. Given the different objects available at request
41
+ # creation time versus receipt time, this method needs to be included in addition to acquire_string_to_hash
42
+ def self.sign_request(client, verb, params)
43
+
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,34 @@
1
+ module HashAuth
2
+ class Client
3
+ # The hash passed in to a new client will initialize this Client object
4
+ # with getters and setters for each key in the hash. The default value
5
+ # for each getter will be the associated value passed in the hash
6
+ def initialize(hash)
7
+ hash.each do |key,value|
8
+ add_instance_getters_and_setters key
9
+ send "#{key}=", value
10
+ end
11
+ end
12
+
13
+ # Add instance specific getters and setters for the name passed in,
14
+ # so as to allow different Client objects to have different properties
15
+ # that are accessible by . notation
16
+ def add_instance_getters_and_setters(var)
17
+ singleton = (class << self; self end)
18
+ singleton.send :define_method, var.to_sym do
19
+ instance_variable_get "@#{var}"
20
+ end
21
+ singleton.send :define_method, "#{var}=".to_sym do |val|
22
+ instance_variable_set "@#{var}", val
23
+ end
24
+ end
25
+
26
+ def method_missing(method, *args, &block)
27
+ # Check config for default value
28
+ default = "default_#{method}"
29
+ if HashAuth.configuration.respond_to? default
30
+ HashAuth.configuration.send default
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,110 @@
1
+ module HashAuth
2
+ class MissingConfiguration < StandardError
3
+ def initialize
4
+ super("Configuration for hash-auth missing. Do you have a hash-auth initializer?")
5
+ end
6
+ end
7
+
8
+ def self.configure(&block)
9
+ @config = Config::Builder.new(&block).build
10
+ end
11
+
12
+ def self.configuration
13
+ @config || (raise MissingConfiguration.new)
14
+ end
15
+
16
+ def self.configuration=(val)
17
+ @config = val
18
+ end
19
+
20
+ def self.clients=(val)
21
+ @clients = val
22
+ end
23
+
24
+ def self.clients
25
+ @clients
26
+ end
27
+
28
+ def self.find_client(name)
29
+ @clients.select{|c| c.customer_identifier == name}[0]
30
+ end
31
+
32
+ def self.strategies
33
+ return @strategies if @strategies
34
+
35
+ constants = HashAuth::Strategies.constants.select { |c| Class === HashAuth::Strategies.const_get(c) }
36
+ @strategies = constants.map{ |c| HashAuth::Strategies.const_get(c) }.select{|c| c != HashAuth::Strategies::Base}
37
+ @strategies
38
+ end
39
+
40
+ def self.find_strategy(name)
41
+ strategy = self.strategies.select{|s| s.identifier == name}[0]
42
+ raise "Strategy specified with name = #{name} does not exist" unless strategy
43
+ strategy
44
+ end
45
+
46
+ class Config
47
+ class Builder
48
+ def initialize(&block)
49
+ @config = Config.new
50
+ instance_eval(&block)
51
+ end
52
+
53
+ def build
54
+ @config
55
+ end
56
+
57
+ def set_default_strategy(val)
58
+ strategy = HashAuth.find_strategy val
59
+ @config.instance_variable_set("@default_strategy", strategy)
60
+ end
61
+
62
+ def add_client(client)
63
+ (HashAuth.clients ||= []) << create_client_from_hash_if_valid(client)
64
+ end
65
+
66
+ # add_clients calls add_client on a list of client hashes
67
+ #
68
+ # @param [Hash] clients - an Array of client hashes
69
+ def add_clients(clients)
70
+ clients.each do |client|
71
+ add_client client
72
+ end
73
+ end
74
+
75
+ def create_client_from_hash_if_valid(client)
76
+ [:customer_key, :customer_identifier, :valid_domains].each do |required_val|
77
+ raise "Client hash is missing #{required_val}" unless client[required_val]
78
+ end
79
+ client[:strategy] = HashAuth.find_strategy(client[:strategy]) if client[:strategy]
80
+ client[:valid_domains] = [client[:valid_domains]] unless client[:valid_domains].kind_of? Array
81
+ HashAuth::Client.new client
82
+ end
83
+
84
+ def method_missing(method, *args, &block)
85
+ match = /set_(default_.*)/.match method.to_s
86
+ if match
87
+ default_var_name = match[1]
88
+ @config.instance_variable_set("@#{default_var_name}", args[0])
89
+ if @config.respond_to?("#{default_var_name}".to_sym) == false
90
+ singleton = (class << @config; self end)
91
+ singleton.send :define_method, "#{default_var_name}".to_sym do instance_variable_get("@#{default_var_name}") end
92
+ end
93
+ end
94
+ end
95
+ end
96
+
97
+ def default_customer_identifier_param
98
+ @default_customer_identifier_param || "customer_id"
99
+ end
100
+
101
+ def default_signature_param
102
+ @default_signature_param || "signature"
103
+ end
104
+
105
+ def default_strategy
106
+ @default_strategy || HashAuth::Strategies::Default
107
+ end
108
+
109
+ end
110
+ end
@@ -0,0 +1,52 @@
1
+ module HashAuth
2
+ module Controllers
3
+ module Helpers
4
+ extend ActiveSupport::Concern
5
+
6
+ module ClassMethods
7
+ def initialize_for_hash_auth(actions_requiring_hash_verification)
8
+ before_filter :verify_hash, :only => actions_requiring_hash_verification
9
+ end
10
+ end
11
+
12
+ protected
13
+ def verify_hash
14
+ @client = extract_client_from_request
15
+ return HashAuth.configuration.default_strategy.on_failure(nil, self, :no_matching_client) unless @client
16
+
17
+ valid_domain = check_host(request.host)
18
+ return @client.strategy.on_failure(@client, self, :invalid_domain) unless valid_domain
19
+
20
+ string_to_hash = @client.strategy.acquire_string_to_hash self, @client
21
+ target_string = @client.strategy.hash_string @client, string_to_hash
22
+ @authenticated = @client.strategy.verify_hash(target_string, @client, self) && valid_domain
23
+
24
+ if @authenticated
25
+ @client.strategy.on_authentication @client, self
26
+ else
27
+ @client.strategy.on_failure(@client, self, :invalid_hash)
28
+ end
29
+ end
30
+
31
+ def extract_client_from_request
32
+ HashAuth.clients.each do |c|
33
+ return c if params[c.customer_identifier_param] == c.customer_identifier
34
+ end
35
+ nil
36
+ end
37
+
38
+ def check_host(host)
39
+ @client.valid_domains.each do |d|
40
+ match = regexp_from_host(d).match(host)
41
+ return true if match != nil
42
+ end
43
+ false
44
+ end
45
+
46
+ def regexp_from_host(host)
47
+ Regexp.new '^'+host.gsub('.','\.').gsub('*', '.*') + '$'
48
+ end
49
+
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,5 @@
1
+ module HashAuth
2
+ module Controllers
3
+ autoload :Helpers, 'hash-auth/controllers/helpers'
4
+ end
5
+ end
@@ -0,0 +1,10 @@
1
+ require 'active_record/railtie'
2
+ require 'action_controller'
3
+
4
+ module HashAuth
5
+ class Railtie < Rails::Railtie
6
+ if defined?(ActionController::Base)
7
+ ActionController::Base.send :include, HashAuth
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,47 @@
1
+ require 'digest'
2
+
3
+ module HashAuth
4
+ module Strategies
5
+ class Base
6
+ # provide the HashAuth system with a handle for this strategy
7
+ def self.identifier
8
+ raise "identifier method not implemented in #{self.class.name}"
9
+ end
10
+
11
+ # upon receiving a hashed request, extract the string that needs to be hashed and compared to the signature passed
12
+ def self.acquire_string_to_hash(controller, client)
13
+ raise "acquire_string_to_hash method not implemented in #{self.class.name}"
14
+ end
15
+
16
+ # how the querystring should be hashed (eg. hmac-sha1, md5)
17
+ def self.hash_string(client, string)
18
+ raise "hash_string method not implemented in #{self.class.name}"
19
+ end
20
+
21
+ # determine equality of the target string, as calculated by acquire_string_to_hash -> hash_string, with the actual signature passed by client
22
+ def self.verify_hash(target_string, client, controller)
23
+ raise "verify_hash method not implemented in #{self.class.name}"
24
+ end
25
+
26
+ # anything special that needs to be done upon successful authentication of a request (eg. log in proxy user for a given client)
27
+ def self.on_authentication(client, controller)
28
+ raise "on_authentication method not implemented in #{self.class.name}"
29
+ end
30
+
31
+ # on_failure is triggered during the before_filter of an action that requires hash authentication
32
+ # if any of the cases are met (these are the options for type):
33
+ # - :no_matching_client
34
+ # - :invalid_domain
35
+ # - :invalid_hash
36
+ def self.on_failure(client, controller, type)
37
+ raise "on_failure method not implemented in #{self.class.name}"
38
+ end
39
+
40
+ # sign_request should sign the outgoing request. Given the different objects available at request
41
+ # creation time versus receipt time, this method needs to be included in addition to acquire_string_to_hash
42
+ def self.sign_request(client, verb, params)
43
+ raise "sign_request method not implemented in #{self.class.name}"
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,39 @@
1
+ module HashAuth
2
+ module Strategies
3
+ class Default < Base
4
+
5
+ def self.identifier
6
+ :default
7
+ end
8
+
9
+ def self.acquire_string_to_hash(controller, client)
10
+ params = controller.params.select{|k,v| k != 'controller' && k != 'action' && k != client.signature_param }.map{|k,v| "#{k}=#{v}"}.join('&')
11
+ params + client.customer_key.to_s
12
+ end
13
+
14
+ def self.hash_string(client, string)
15
+ Digest::SHA2.new(256) << string
16
+ end
17
+
18
+ def self.verify_hash(target_string, client, controller)
19
+ return false if controller.params[client.signature_param] == nil
20
+ target_string == controller.params[client.signature_param]
21
+ end
22
+
23
+ def self.on_authentication(client, controller)
24
+ # Do nothing
25
+ end
26
+
27
+ def self.on_failure(client, controller, type)
28
+ controller.instance_variable_set '@failure_message', 'Not a valid client' if type == :no_matching_client
29
+ controller.instance_variable_set '@failure_message', 'Request coming from invalid domain' if type == :invalid_domain
30
+ controller.instance_variable_set '@failure_message', 'Signature hash is invalid' if type == :invalid_hash
31
+ end
32
+
33
+ def self.sign_request(client, verb, params)
34
+ self.hash_string(client, params.map{|k,v| "#{k}=#{v}"}.join('&') + client.customer_key.to_s)
35
+ end
36
+
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,6 @@
1
+ module HashAuth
2
+ module Strategies
3
+ autoload :Base, 'hash-auth/strategies/base'
4
+ autoload :Default, 'hash-auth/strategies/default'
5
+ end
6
+ end
@@ -0,0 +1,3 @@
1
+ module HashAuth
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,65 @@
1
+ require 'rest-client'
2
+
3
+ module HashAuth
4
+ class WebRequest
5
+
6
+ def self.get(client, url, headers = {}, &block)
7
+ self.delegate_to_rest_client_passive :get, client, url, headers, &block
8
+ end
9
+
10
+ def self.post(client, url, payload, headers = {}, &block)
11
+ self.delegate_to_rest_client_active :post, client, url, payload, headers, &block
12
+ end
13
+
14
+ def self.patch(client, url, payload, headers = {}, &block)
15
+ self.delegate_to_rest_client_active :post, client, url, payload, headers, &block
16
+ end
17
+
18
+ def self.put(client, url, payload, headers = {}, &block)
19
+ self.delegate_to_rest_client_active :post, client, url, payload, headers, &block
20
+ end
21
+
22
+ def self.delete(client, url, headers = {}, &block)
23
+ self.delegate_to_rest_client_passive :delete, client, url, headers, &block
24
+ end
25
+
26
+ def self.head(client, url, headers = {}, &block)
27
+ self.delegate_to_rest_client_passive :head, client, url, headers, &block
28
+ end
29
+
30
+ def self.options(client, url, headers = {}, &block)
31
+ self.delegate_to_rest_client_passive :options, client, url, headers, &block
32
+ end
33
+
34
+ def self.delegate_to_rest_client_passive(action, client, url, headers, &block)
35
+ headers = self.sign_and_identify client, action, headers
36
+ RestClient.send action, url, headers, &block
37
+ end
38
+
39
+ def self.delegate_to_rest_client_active(action, client, url, payload, headers, &block)
40
+ payload = self.sign_and_identify client, action, payload
41
+ RestClient.send action, url, payload, headers, &block
42
+ end
43
+
44
+ def self.sign_and_identify(client, verb, params)
45
+ params = self.add_client_to_params(client, params)
46
+ params = self.add_signature_to_params(client, verb, params)
47
+ if [:get, :delete, :head, :options].include? verb
48
+ {:params => params}
49
+ else
50
+ params
51
+ end
52
+ end
53
+
54
+ def self.add_client_to_params(client, params)
55
+ params[client.customer_identifier_param.to_sym] = client.customer_identifier
56
+ params
57
+ end
58
+
59
+ def self.add_signature_to_params(client, verb, params)
60
+ params[client.signature_param.to_sym] = client.strategy.sign_request(client, verb, params)
61
+ params
62
+ end
63
+
64
+ end
65
+ end
data/lib/hash-auth.rb ADDED
@@ -0,0 +1,27 @@
1
+ require 'hash-auth/config'
2
+
3
+ module HashAuth
4
+ extend ActiveSupport::Concern
5
+ extend ActiveSupport::Autoload
6
+
7
+ included do
8
+ #puts "HashAuth included"
9
+ end
10
+ autoload :Controllers, 'hash-auth/controllers'
11
+ autoload :Strategies, 'hash-auth/strategies'
12
+ autoload :Client, 'hash-auth/client'
13
+ autoload :WebRequest, 'hash-auth/web_request'
14
+
15
+ module ClassMethods
16
+ def validates_auth_for(*methods, &block)
17
+ self.send :include, Controllers::Helpers
18
+ initialize_for_hash_auth methods
19
+ end
20
+ end
21
+
22
+ def self.configured?
23
+ @config.present?
24
+ end
25
+ end
26
+
27
+ require 'hash-auth/railtie'
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :hash-auth do
3
+ # # Task goes here
4
+ # end
@@ -0,0 +1,41 @@
1
+ require File.expand_path('../../spec_helper', __FILE__)
2
+
3
+ describe TestRailsApp::TestController do
4
+
5
+ it "extracts client from request" do
6
+ controller.params = Request.parse_params 'a=b&c=d&customer_id=my_organization'
7
+ client = controller.extract_client_from_request_helper
8
+ client.customer_key.should == 1234567890
9
+ end
10
+
11
+ it "checks the request host for a clients valid domain" do
12
+ controller.params = Request.parse_params 'a=b&c=d&customer_id=my_organization'
13
+ controller.instance_variable_set '@client', controller.extract_client_from_request_helper
14
+ controller.check_host_helper('localhost').should == true
15
+ controller.check_host_helper('localhostwithstuffafterit').should == false
16
+ controller.check_host_helper('prependinglocalhost').should == false
17
+ controller.check_host_helper('localSTUFFhost').should == false
18
+ end
19
+
20
+ it "checks the request host for client's valid domain with wildcarding" do
21
+ controller.params = Request.parse_params 'a=b&c=d&customer_id=your_organization'
22
+ controller.instance_variable_set '@client', controller.extract_client_from_request_helper
23
+ controller.check_host_helper('maps.google.com').should == true
24
+ controller.check_host_helper('google.com').should == true
25
+ controller.check_host_helper('google.coma').should == false
26
+ controller.check_host_helper('google.co.uk').should == false
27
+ controller.check_host_helper('hello.org').should == true
28
+ controller.check_host_helper('org').should == false
29
+ end
30
+
31
+ it "executes on_failure block when request parameters do not match a client" do
32
+ controller.params = Request.parse_params 'a=b&c=d&customer_id=not_an_organization'
33
+ controller.verify_hash_helper
34
+ controller.instance_variable_get('@failure_message').should_not == nil
35
+ end
36
+
37
+ it "responds on_failure when authentication fails" do
38
+ controller.params = Request.parse_params 'a=b&c=d&customer_id=test&signature=abcde'
39
+ expect{controller.verify_hash_helper}.to raise_error(OnFailureError)
40
+ end
41
+ end
@@ -0,0 +1,168 @@
1
+ #### Faking a rails application that is configured with HashAuth for spec purposes
2
+
3
+ class OnFailureError < Exception
4
+ end
5
+
6
+ module HashAuth
7
+ module Strategies
8
+ class New < Base
9
+
10
+ def self.identifier
11
+ :new
12
+ end
13
+
14
+ def self.acquire_string_to_hash(controller, client)
15
+ controller.params.select{|k,v| k != 'controller' && k != 'action' && k != client.signature_parameter }.map{|k,v| "#{k}=#{v}"}.join('&')
16
+ end
17
+
18
+ def self.hash_string(client, string)
19
+ Digest::MD5.digest string
20
+ end
21
+
22
+ def self.verify_hash(target_string, client, controller)
23
+ raise 'Parameters do not contain this client\'s signature_parameter' if controller.params[client.signature_parameter] == nil
24
+ target_string == controller.params[client.signature_parameter]
25
+ end
26
+
27
+ def self.on_authentication(client, controller)
28
+ # Do nothing
29
+ end
30
+
31
+ def self.on_failure(client, controller, type)
32
+ raise OnFailureError, "Failure to authenticate"
33
+ # Do nothingå
34
+ end
35
+
36
+ end
37
+ end
38
+ end
39
+
40
+ clients = [
41
+ {
42
+ :customer_key => 1234567890,
43
+ :customer_identifier => 'my_organization',
44
+ :customer_identifier_param => 'customer_id',
45
+ :valid_domains => 'localhost',
46
+ :strategy => :default
47
+ },
48
+ {
49
+ :customer_key => 987654321,
50
+ :customer_identifier => 'your_organization',
51
+ :customer_identifier_param => 'customer_id',
52
+ :valid_domains => ['*google.com', '*.org'],
53
+ :strategy => :default
54
+ },
55
+ {
56
+ :customer_key => 'zyxwvut',
57
+ :customer_identifier => 'test',
58
+ :valid_domains => '*',
59
+ :strategy => :new
60
+ },
61
+ {
62
+ :customer_key => 9988776655,
63
+ :customer_identifier => 'no_matching_client',
64
+ :customer_identifier_param => 'customer_id',
65
+ :valid_domains => 'localhost',
66
+ :strategy => :default
67
+ },
68
+ {
69
+ :customer_key => 'something other than will be on server',
70
+ :customer_identifier => 'incorrect_hash',
71
+ :customer_identifier_param => 'customer_id',
72
+ :valid_domains => 'localhost',
73
+ :strategy => :default
74
+ }
75
+ ]
76
+
77
+ HashAuth.configure do
78
+
79
+ ## Block to allow dynamic loading of customer keys (Optional)
80
+ #### Could be from YAML
81
+ #### Could be from DB
82
+
83
+ #set_default_customer_identifier_param
84
+
85
+ add_clients clients
86
+
87
+ set_default_signature_parameter 'signature'
88
+
89
+ end
90
+
91
+ module TestRailsApp
92
+
93
+ class Application < Rails::Application
94
+ # app config here
95
+ # config.secret_token = '572c86f5ede338bd8aba8dae0fd3a326aabababc98d1e6ce34b9f5'
96
+ routes.draw do
97
+ match "test_rails_app/test/one" => "test#one"
98
+ match "/test/two" => "test#two"
99
+ match "/test/three" => "test#three"
100
+ end
101
+ end
102
+
103
+ class ApplicationController < ActionController::Base
104
+ # setup
105
+ end
106
+
107
+ class TestController < ApplicationController
108
+ validates_auth_for :one, :two
109
+
110
+ def one
111
+ end
112
+
113
+ def two
114
+ end
115
+
116
+ def three
117
+ end
118
+
119
+ def extract_client_from_request_helper
120
+ extract_client_from_request
121
+ end
122
+
123
+ def check_host_helper(host)
124
+ check_host(host)
125
+ end
126
+
127
+ def verify_hash_helper
128
+ verify_hash
129
+ end
130
+
131
+ end
132
+
133
+ require 'rspec/rails'
134
+
135
+ end
136
+
137
+ # Faking controller/action requests
138
+ class Request
139
+
140
+ def self.parse_params(string)
141
+ h = {}
142
+ s = string.split('&').map{|set| set.split '=' }.each do |p|
143
+ h[p[0]] = p[1]
144
+ end
145
+ h
146
+ end
147
+
148
+ def initialize(hash)
149
+ hash.each do |key,value|
150
+ add_instance_getters_and_setters key
151
+ send "#{key}=", value
152
+ end
153
+ end
154
+
155
+ # Add instance specific getters and setters for the name passed in,
156
+ # so as to allow different Client objects to have different properties
157
+ # that are accessible by . notation
158
+ def add_instance_getters_and_setters(var)
159
+ singleton = (class << self; self end)
160
+ singleton.send :define_method, var.to_sym do
161
+ instance_variable_get "@#{var}"
162
+ end
163
+ singleton.send :define_method, "#{var}=".to_sym do |val|
164
+ instance_variable_set "@#{var}", val
165
+ end
166
+ end
167
+
168
+ end
@@ -0,0 +1,23 @@
1
+ require File.expand_path('../../spec_helper', __FILE__)
2
+ require 'active_support/all'
3
+ require 'hash-auth'
4
+
5
+ describe HashAuth::Client do
6
+
7
+ it "can be instantiated with a hash that adds getters and setters to the instance" do
8
+ hash = {:a => :b, :c => :d}
9
+ c = HashAuth::Client.new hash
10
+ c.a.should == :b
11
+ c.c.should == :d
12
+ end
13
+
14
+ it "does not add getters and setters to the entire Client class" do
15
+ hash = {:a => :b, :c => :d}
16
+ c = HashAuth::Client.new hash
17
+ hash = {:b => :a, :d => :c}
18
+ d = HashAuth::Client.new hash
19
+ c.respond_to?(:a).should == true
20
+ d.respond_to?(:a).should == false
21
+ end
22
+
23
+ end
@@ -0,0 +1,41 @@
1
+ require File.expand_path('../../spec_helper', __FILE__)
2
+
3
+ prev_config = HashAuth.configuration
4
+
5
+ describe HashAuth::Config do
6
+
7
+ after :all do
8
+ HashAuth.configuration = prev_config
9
+ end
10
+
11
+ it "sets default_customer_identifier_param (via method missing)" do
12
+ HashAuth.configure { set_default_customer_identifier_param 'customer_identifier' }
13
+ HashAuth.configuration.default_customer_identifier_param.should == 'customer_identifier'
14
+ end
15
+
16
+ it "sets default signature param (via method missing)" do
17
+ HashAuth.configure { set_default_signature_param 'random_signature_param' }
18
+ HashAuth.configuration.default_signature_param.should == 'random_signature_param'
19
+ end
20
+
21
+ it "sets default strategy" do
22
+ expect{HashAuth.configure { set_default_strategy :my_new_strategy }}.to raise_error
23
+ HashAuth.configure { set_default_strategy :new }
24
+ HashAuth.configuration.default_strategy.should == HashAuth::Strategies::New
25
+ end
26
+
27
+ it "adds a new client" do
28
+ client = {
29
+ :customer_key => 'ABCDEFG',
30
+ :customer_identifier => 'globocorp',
31
+ :customer_identifier_param => 'id',
32
+ :valid_domains => ['*'],
33
+ }
34
+ num_clients = HashAuth.clients.length
35
+ HashAuth.configure { add_client client }
36
+ HashAuth.clients.length.should == num_clients + 1
37
+ end
38
+
39
+ it "takes a block to determine what to do when authentication fails to find a matching client"
40
+
41
+ end
@@ -0,0 +1,9 @@
1
+ require File.expand_path('../../spec_helper', __FILE__)
2
+
3
+ describe HashAuth::Railtie do
4
+
5
+ it "includes HashAuth in ActionController::Base" do
6
+ ActionController::Base.included_modules.include?(HashAuth).should == true
7
+ end
8
+
9
+ end
@@ -0,0 +1,74 @@
1
+ require File.expand_path('../../spec_helper', __FILE__)
2
+
3
+ # HashAuth::WebRequest specs rely on having the dummy app running on localhost:3000.
4
+ # This will remain true until a test server is put up. These specs are commented out in
5
+ # commits so that Travis ci can show passing status until then
6
+ #
7
+ # These tests rely on the default strategy
8
+ #
9
+ #
10
+ # Update: added new specs to double check that the request parameters are properly encoded
11
+
12
+ describe HashAuth::WebRequest do
13
+
14
+ after :each do
15
+ ObjectSpace.garbage_collect
16
+ end
17
+
18
+ # it "generates a properly signed query string that gets validated by server" do
19
+ # client = HashAuth.find_client 'my_organization'
20
+ # response = JSON.parse(HashAuth::WebRequest.get client, 'localhost:3000/test/one', {:foo => :bar, :bar => :baz})
21
+ # response['message'].should == 'ok'
22
+ # end
23
+
24
+ # it "receives invalid domain failure when query string is invalid" do
25
+ # client = HashAuth.find_client 'your_organization'
26
+ # response = JSON.parse(HashAuth::WebRequest.post client, 'localhost:3000/test/one', {:foo => :bar, :bar => :baz})
27
+ # response['message'].should == 'Request coming from invalid domain'
28
+ # end
29
+
30
+ # it "receives no matching client failure method when query string is invalid" do
31
+ # client = HashAuth.find_client 'no_matching_client'
32
+ # response = JSON.parse(HashAuth::WebRequest.post client, 'localhost:3000/test/one', {})
33
+ # response['message'].should == 'Not a valid client'
34
+ # end
35
+
36
+ # it "receives incorrect hash method when query string is invalid" do
37
+ # client = HashAuth.find_client 'incorrect_hash'
38
+ # response = JSON.parse(HashAuth::WebRequest.get client, 'localhost:3000/test/one', {:foo => :bar, :bar => :baz})
39
+ # response['message'].should == 'Signature hash is invalid'
40
+ # end
41
+
42
+ it "forms proper urls for requests for get requests" do
43
+ client = HashAuth.find_client 'my_organization'
44
+ expect{response = JSON.parse(HashAuth::WebRequest.get client, 'localhost:3000/test/one', {:foo => :bar, :bar => :baz})}.to raise_error
45
+ request = nil
46
+ ObjectSpace.each_object(RestClient::Request){|r| request = r}
47
+ string_to_hash = "foo=bar&bar=baz&#{client.customer_identifier_param}=#{client.customer_identifier}"
48
+ hashed_string = Digest::SHA2.new(256) << string_to_hash + client.customer_key.to_s
49
+ /.*\?(.*)/.match(request.instance_variable_get('@url'))[1].should == "#{string_to_hash}&#{client.signature_param}=#{hashed_string}"
50
+ end
51
+
52
+ # it "forms proper urls for requests for post requests" do
53
+ # client = HashAuth.find_client 'my_organization'
54
+ # expect{response = JSON.parse(HashAuth::WebRequest.post client, 'localhost:3000/test/one', {:foo => :bar, :bar => :baz})}.to raise_error
55
+ # request = nil
56
+ # ObjectSpace.each_object(RestClient::Request){|r| request = r}
57
+ # string_to_hash = "foo=bar&bar=baz&#{client.customer_identifier_param}=#{client.customer_identifier}"
58
+ # hashed_string = Digest::SHA2.new(256) << string_to_hash + client.customer_key.to_s
59
+ # request.instance_variable_get('@headers').should == "#{string_to_hash}&#{client.signature_param}=#{hashed_string}"
60
+ # end
61
+
62
+ # it "can get from server"
63
+
64
+ # it "can post to server"
65
+
66
+ # it "can put to server"
67
+
68
+ # it "can option to server"
69
+
70
+ # it "can head to server"
71
+
72
+ # it "can delete to server"
73
+
74
+ end
@@ -0,0 +1,8 @@
1
+ require 'rails'
2
+ require 'active_support/all'
3
+ require 'action_controller/railtie'
4
+ require 'hash-auth'
5
+ require 'fake-rails-app'
6
+ RSpec.configure do |config|
7
+ config.order = "random"
8
+ end
metadata ADDED
@@ -0,0 +1,117 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: hash-auth
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Max Lahey
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-04-02 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rails
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 3.2.11
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: 3.2.11
30
+ - !ruby/object:Gem::Dependency
31
+ name: rest-client
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ~>
36
+ - !ruby/object:Gem::Version
37
+ version: 1.6.7
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ version: 1.6.7
46
+ description: HashAuth allows your Rails application to support incoming and outgoing
47
+ two-factor authentication via hashing some component of an HTTPS request. Both sides
48
+ of the request (your Rails app and your client or provider) must have some unique
49
+ shared secret. This secret is used to create a hash of some portion of the request,
50
+ ensuring that (if neither side has been compromised) only the other party could
51
+ have created the request.
52
+ email:
53
+ - maxwellslahey@gmail.com
54
+ executables: []
55
+ extensions: []
56
+ extra_rdoc_files: []
57
+ files:
58
+ - lib/generators/hash_auth/install_generator.rb
59
+ - lib/generators/hash_auth/strategy_generator.rb
60
+ - lib/generators/hash_auth/templates/initializer.rb
61
+ - lib/generators/hash_auth/templates/strategy.rb
62
+ - lib/hash-auth/client.rb
63
+ - lib/hash-auth/config.rb
64
+ - lib/hash-auth/controllers/helpers.rb
65
+ - lib/hash-auth/controllers.rb
66
+ - lib/hash-auth/railtie.rb
67
+ - lib/hash-auth/strategies/base.rb
68
+ - lib/hash-auth/strategies/default.rb
69
+ - lib/hash-auth/strategies.rb
70
+ - lib/hash-auth/version.rb
71
+ - lib/hash-auth/web_request.rb
72
+ - lib/hash-auth.rb
73
+ - lib/tasks/hash-auth_tasks.rake
74
+ - MIT-LICENSE
75
+ - Rakefile
76
+ - README.md
77
+ - spec/controllers/helper_spec.rb
78
+ - spec/fake-rails-app.rb
79
+ - spec/lib/client_spec.rb
80
+ - spec/lib/config_spec.rb
81
+ - spec/lib/railtie_spec.rb
82
+ - spec/lib/web_request_spec.rb
83
+ - spec/spec_helper.rb
84
+ homepage: http://maxwells.github.com
85
+ licenses: []
86
+ post_install_message:
87
+ rdoc_options: []
88
+ require_paths:
89
+ - lib
90
+ required_ruby_version: !ruby/object:Gem::Requirement
91
+ none: false
92
+ requirements:
93
+ - - ! '>='
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ required_rubygems_version: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ! '>='
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ requirements: []
103
+ rubyforge_project:
104
+ rubygems_version: 1.8.23
105
+ signing_key:
106
+ specification_version: 3
107
+ summary: Rails gem to authenticate HTTP requests by hashing request components and
108
+ passing as parameter
109
+ test_files:
110
+ - spec/controllers/helper_spec.rb
111
+ - spec/fake-rails-app.rb
112
+ - spec/lib/client_spec.rb
113
+ - spec/lib/config_spec.rb
114
+ - spec/lib/railtie_spec.rb
115
+ - spec/lib/web_request_spec.rb
116
+ - spec/spec_helper.rb
117
+ has_rdoc: