hash_auth_wsc 1.0.0

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 7fd427ccf3bc9919060a4e3e6c4083a79045ba23
4
+ data.tar.gz: 1eef01183586b3b45f068d21b9f94420045eff40
5
+ SHA512:
6
+ metadata.gz: 5174b8844bcceee2d49107b965905feb5c58f921e4ba9acd1d202d2cc604e40a6be0fa42f5b6df4c32bf45d966d6abf76111df2c1be52949bd489f3206015c2a
7
+ data.tar.gz: 8f1202ea2ba891b2412b83b7d1036078b821746ac7ff2961272bc9e012b38857162d3414bf1679bff71275460a047ad07fd0d1b146d0df397a43be0cde10e3c1
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,236 @@
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
+ - Requests can be filtered by remote ip address specifically or by the reverse dns lookup of the remote ip address
18
+
19
+ ## Usage
20
+
21
+ ### Installation
22
+
23
+ 1) Install the HashAuth gem from RubyGems
24
+
25
+ $ gem install hash-auth
26
+
27
+ 2) Add it to your Rails application's Gemfile
28
+
29
+ gem "hash-auth"
30
+
31
+ 3) Install it into your Rails application
32
+
33
+ $ rails g hashauth:install
34
+
35
+ 4) If you need to create your own strategies
36
+
37
+ $ rails g hashauth:strategy [name]
38
+
39
+ ### Configuration
40
+
41
+ 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
42
+
43
+
44
+ **_Adding an authentication strategy._**
45
+
46
+ Generate a new strategy, which will live in lib/hash_auth/strategies
47
+
48
+ rails g hash_auth:strategy name_of_strategy
49
+
50
+ This will generate a template that needs to be filled in with the necessary behavior for authenticating your client. Here is an example strategy
51
+
52
+
53
+ ```ruby
54
+
55
+ module HashAuth
56
+ module Strategies
57
+ class NameOfStrategy < Base
58
+
59
+ def name
60
+ :name_of_strategy
61
+ end
62
+
63
+ ## 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.
64
+ def acquire_string_to_hash(controller, client)
65
+ controller.params.select{|k,v| k != 'controller' && k != 'action' }.map{|k,v| "#{k}=#{v}"}.join('&') + client.customer_key
66
+ end
67
+
68
+ # Client hashes string with SHA256
69
+ def hash_string(string, client)
70
+ Digest::sha2.new(256) << string
71
+ end
72
+
73
+ def on_authentication(client)
74
+ # 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)
75
+ end
76
+
77
+ end
78
+ end
79
+ end
80
+
81
+ ```
82
+
83
+ **_Adding Clients via hardcoding (Config)._**
84
+
85
+
86
+ ```ruby
87
+
88
+ #### Adding a new client
89
+
90
+ # 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)
91
+ add_client {
92
+ :customer_key => '1234567890',
93
+ # the shared secret between you and a client
94
+ :customer_identifier => 'my_organization',
95
+ # the unique identifer the client will pass you to identify themselves
96
+ :customer_identifier_param => 'customer_id',
97
+ # the name of the parameter the client will pass their unique identifier in
98
+ :valid_domains => '*my_organization.org',
99
+ # will allow request from anything ending with my_organization.org, can also provide a list. Note: this only works if reverse_dns ip filtering, so the reverse dns lookup for a clients IP must match up. (domain_auth :reverse_dns)
100
+ :valid_ips => '192.168.1.1',
101
+ # will allow request from a specific ip or list of ips. Note: only works if ip filtering is enabled (domain_auth :ip)
102
+ :strategy => :my_auth_strategy,
103
+ # 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
104
+ }
105
+ ```
106
+ **_Adding Clients from an external resource (eg YAML, Database)._**
107
+
108
+ YAML file (config/clients.yml in this example):
109
+
110
+ clients:
111
+ -
112
+ customer_key: 1234567890
113
+ customer_identifier: my_organization
114
+ customer_identifier_param: customer_id
115
+ valid_domains: '*my_organization.org'
116
+ valid_ips: '192.168.1.1'
117
+ strategy: :default
118
+ custom_key: custom_value
119
+ -
120
+ customer_key: 0987654321
121
+ customer_identifier: your_organization
122
+ customer_identifier_param: customer_id
123
+ valid_domains: ['your_organization.com', 'your_organization.org']
124
+ valid_ips: ['192.168.1.1', '192.168.1.2']
125
+ strategy: :my_auth_strategy
126
+ custom_key: custom_value
127
+
128
+ hash-auth initializer:
129
+
130
+ ```ruby
131
+
132
+ clients = YAML::load( File.open('config/clients.yml') )
133
+ add_clients clients["clients"]
134
+
135
+ ```
136
+
137
+ **_Options in hash-auth initializer_**
138
+
139
+ * IP Filtering *
140
+ Enable ip or reverse dns filtering
141
+
142
+ ```ruby
143
+ HashAuth.configure do
144
+ domain_auth :ip
145
+ end
146
+
147
+ # or
148
+
149
+ HashAuth.configure do
150
+ domain_auth :reverse_dns
151
+ end
152
+ ```
153
+
154
+ Note: if a client has no valid_ips or valid_domains associated with it, then no filtering will occur.
155
+
156
+ Reverse DNS filtering caches the result. You have the option to namespace how that data gets stored.
157
+
158
+ ```ruby
159
+ HashAuth.configure do
160
+ cache_store_namespace "foo"
161
+ end
162
+
163
+ # will store the reverse dns lookup associated with an ip address
164
+ # as "foo-#{ip}" in the Rails.cache
165
+ # Example: 192.168.1.1 whould become foo-192.168.1.1
166
+ # Defaults to "hash-auth"
167
+ ```
168
+
169
+ * Custom & Default Values*
170
+
171
+ Any custom client field can be initialized with a default value through method missing (set_default_*)
172
+
173
+ HashAuth.configure do
174
+ set_default_authentication_success_status_message {:status => "success" }
175
+ end
176
+
177
+ 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.
178
+
179
+ ## In a custom strategy…
180
+
181
+ def self.on_failure(client, controller)
182
+ @failed_authentication_status = {:status => 'failure'}
183
+ end
184
+
185
+ ## In the controller…
186
+ def my_action
187
+ if (@authenticated)
188
+ ... Do necessary stuff
189
+ response = @client.authentication_success_status_message
190
+ else
191
+ response = @failed_authentication_json
192
+ end
193
+
194
+ respond_to do |format|
195
+ format.json { render :json => response }
196
+ end
197
+ end
198
+
199
+ Additionally, the default strategy for every client can be set (if not set, will revert to HashAuth::Strategies::Default)
200
+
201
+ set_default_strategy :strategy_identifier
202
+
203
+
204
+ #### Implementation: _Receiving hashed requests_
205
+
206
+ In whatever controller(s) require hash authentication of requests
207
+
208
+ validates_auth_for :action_one, :action_two
209
+
210
+ The following variables are available in implementing controller actions
211
+
212
+ @client : HashAuth::Client - instance of client (if found, whether or not authenticated)
213
+ @authenticated : Boolean - whether or not the hashed request was considered validated
214
+
215
+
216
+ #### Implementation: _Making hashed requests_
217
+ 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).
218
+
219
+ client = HashAuth.find_client 'my_organization'
220
+ HashAuth::WebRequest.post client, 'localhost:3000/test/one', {:foo => :bar, :bar => :baz}
221
+
222
+ WebRequest supports:
223
+
224
+ def self.get(client, url, parameters = {}, &block)
225
+ def self.post(client, url, payload, headers = {}, &block)
226
+ def self.patch(client, url, payload, headers = {}, &block)
227
+ def self.put(client, url, payload, headers = {}, &block)
228
+ def self.delete(client, url, parameters = {}, &block)
229
+ def self.head(client, url, parameters = {}, &block)
230
+ def self.options(client, url, parameters = {}, &block)
231
+
232
+ See [REST client](https://github.com/rest-client/rest-client) for futher detail.
233
+
234
+ ## License
235
+
236
+ 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,29 @@
1
+ Dir["lib/hash-auth/*.rb"].each {|file| require "hash-auth/#{File.basename file, '.rb'}" }
2
+
3
+ HashAuth.configure do
4
+
5
+ ## Block to allow dynamic loading of customer keys (Optional)
6
+ #### Could be from YAML
7
+ #### Could be from DB
8
+
9
+ # add_clients do |clients|
10
+ # YAML::load( File.open('../clients.yml') )
11
+ # end
12
+
13
+ ## Any attributes can be added to a client object at initialization.
14
+ #### The default can be added to HashAuth configuration and will be picked up in every strategy automagically
15
+ #### Default values must be set with set_default_* methods and will be picked up by [client].* methods
16
+ # eg.
17
+ # set_default_foo_bar 'bar baz'
18
+ #
19
+ # client.foo_bar will return 'bar baz' unless specifically set for that client
20
+
21
+ ## Set default strategy by handle
22
+ # set_default_strategy :strategy_fifty_one
23
+
24
+ ## Set ip specific filtering
25
+ # domain_auth :ip
26
+ ## OR set ip filtering by reverse_dns (uses reverse dns lookup for the remote_ip provided in a request)
27
+ # domain_auth :reverse_dns
28
+
29
+ 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,126 @@
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
+ client[:valid_ips] = [client[:valid_ips]] unless client[:valid_ips].kind_of? Array
82
+ HashAuth::Client.new client
83
+ end
84
+
85
+ def method_missing(method, *args, &block)
86
+ match = /set_(default_.*)/.match method.to_s
87
+ if match
88
+ default_var_name = match[1]
89
+ @config.instance_variable_set("@#{default_var_name}", args[0])
90
+ if @config.respond_to?("#{default_var_name}".to_sym) == false
91
+ singleton = (class << @config; self end)
92
+ singleton.send :define_method, "#{default_var_name}".to_sym do instance_variable_get("@#{default_var_name}") end
93
+ end
94
+ else
95
+ var_name = method
96
+ @config.instance_variable_set("@#{var_name}", args[0])
97
+ if @config.respond_to?("#{var_name}".to_sym) == false
98
+ singleton = (class << @config; self end)
99
+ singleton.send :define_method, "#{var_name}".to_sym do instance_variable_get("@#{var_name}") end
100
+ end
101
+ end
102
+ end
103
+ end
104
+
105
+ def default_customer_identifier_param
106
+ @default_customer_identifier_param || "customer_id"
107
+ end
108
+
109
+ def default_signature_param
110
+ @default_signature_param || "signature"
111
+ end
112
+
113
+ def default_strategy
114
+ @default_strategy || HashAuth::Strategies::Default
115
+ end
116
+
117
+ def cache_store_namespace
118
+ @cache_store_namespace.to_s || "hash-auth"
119
+ end
120
+
121
+ def domain_auth
122
+ @domain_auth || :none
123
+ end
124
+
125
+ end
126
+ end
@@ -0,0 +1,79 @@
1
+ require 'resolv'
2
+
3
+ module HashAuth
4
+ module Controllers
5
+ module Helpers
6
+ extend ActiveSupport::Concern
7
+
8
+ module ClassMethods
9
+ def initialize_for_hash_auth(actions_requiring_hash_verification)
10
+ if actions_requiring_hash_verification == [:all]
11
+ before_filter :verify_hash
12
+ else
13
+ before_filter :verify_hash, :only => actions_requiring_hash_verification
14
+ end
15
+ end
16
+ end
17
+
18
+ protected
19
+ def verify_hash
20
+ @client = extract_client_from_request
21
+ return HashAuth.configuration.default_strategy.on_failure(nil, self, :no_matching_client) unless @client
22
+
23
+ case HashAuth.configuration.domain_auth
24
+ when :ip
25
+ return @client.strategy.on_failure(@client, self, :invalid_ip) unless check_ip(request.remote_ip)
26
+ when :reverse_dns
27
+ domain = extract_domain(request.remote_ip)
28
+ return @client.strategy.on_failure(@client, self, :invalid_domain) unless check_host(domain)
29
+ end
30
+
31
+ string_to_hash = @client.strategy.acquire_string_to_hash self, @client
32
+ target_string = @client.strategy.hash_string @client, string_to_hash
33
+ @authenticated = @client.strategy.verify_hash(target_string, @client, self)
34
+
35
+ return @client.strategy.on_authentication @client, self if @authenticated
36
+ @client.strategy.on_failure(@client, self, :invalid_hash)
37
+ end
38
+
39
+ def extract_client_from_request
40
+ HashAuth.clients.each do |c|
41
+ return c if params[c.customer_identifier_param] == c.customer_identifier
42
+ end
43
+ nil
44
+ end
45
+
46
+ def extract_domain(ip)
47
+ cache_address = "#{HashAuth.configuration.cache_store_namespace}-#{ip}"
48
+ cached_result = Rails.cache.read cache_address
49
+ return cached_result unless cached_result == nil
50
+ hostname = Resolv.new.getname(ip)
51
+ Rails.cache.write cache_address, hostname
52
+ hostname
53
+ end
54
+
55
+ def check_ip(ip)
56
+ return true if @client.valid_ips.length == 0
57
+ @client.valid_ips.each do |valid_ip|
58
+ match = regexp_from_string(valid_ip).match(ip)
59
+ return true if match != nil
60
+ end
61
+ false
62
+ end
63
+
64
+ def check_host(host)
65
+ return true if @client.valid_domains.length == 0
66
+ @client.valid_domains.each do |d|
67
+ match = regexp_from_string(d).match(host)
68
+ return true if match != nil
69
+ end
70
+ false
71
+ end
72
+
73
+ def regexp_from_string(host)
74
+ Regexp.new '^'+host.gsub('.','\.').gsub('*', '.*') + '$'
75
+ end
76
+
77
+ end
78
+ end
79
+ 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,47 @@
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| !['controller', 'action', 'format', client.signature_param].include? k }.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 = Digest::SHA256.new
16
+ digest.hexdigest string
17
+ end
18
+
19
+ def self.verify_hash(target_string, client, controller)
20
+ return false if controller.params[client.signature_param] == nil
21
+ target_string == controller.params[client.signature_param]
22
+ end
23
+
24
+ def self.on_authentication(client, controller)
25
+ # Do nothing
26
+ end
27
+
28
+ def self.on_failure(client, controller, type)
29
+ case type
30
+ when :no_matching_client
31
+ controller.instance_variable_set '@failure_message', 'Not a valid client'
32
+ when :invalid_domain
33
+ controller.instance_variable_set '@failure_message', 'Request coming from invalid domain'
34
+ when :invalid_hash
35
+ controller.instance_variable_set '@failure_message', 'Signature hash is invalid'
36
+ when :invalid_ip
37
+ controller.instance_variable_set '@failure_message', 'Request coming from invalid IP'
38
+ end
39
+ end
40
+
41
+ def self.sign_request(client, verb, params)
42
+ self.hash_string(client, params.map{|k,v| "#{k}=#{v}"}.join('&') + client.customer_key.to_s)
43
+ end
44
+
45
+ end
46
+ end
47
+ 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 = "1.0.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
File without changes
@@ -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,108 @@
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
+ # Faking controller/action requests
78
+ class RequestHelper
79
+
80
+ def self.parse_params(string)
81
+ h = {}
82
+ s = string.split('&').map{|set| set.split '=' }.each do |p|
83
+ h[p[0]] = p[1]
84
+ end
85
+ h
86
+ end
87
+
88
+ def initialize(hash)
89
+ hash.each do |key,value|
90
+ add_instance_getters_and_setters key
91
+ send "#{key}=", value
92
+ end
93
+ end
94
+
95
+ # Add instance specific getters and setters for the name passed in,
96
+ # so as to allow different Client objects to have different properties
97
+ # that are accessible by . notation
98
+ def add_instance_getters_and_setters(var)
99
+ singleton = (class << self; self end)
100
+ singleton.send :define_method, var.to_sym do
101
+ instance_variable_get "@#{var}"
102
+ end
103
+ singleton.send :define_method, "#{var}=".to_sym do |val|
104
+ instance_variable_set "@#{var}", val
105
+ end
106
+ end
107
+
108
+ end
@@ -0,0 +1,10 @@
1
+ require 'rails'
2
+ require 'active_support/all'
3
+ require 'action_controller/railtie'
4
+ require 'hash-auth'
5
+ require 'rails-helpers'
6
+ require File.expand_path('../../test/dummy/spec/spec_helper', __FILE__)
7
+ require File.expand_path('../../test/dummy/spec/controllers/helper_spec', __FILE__)
8
+ RSpec.configure do |config|
9
+ config.order = "random"
10
+ end
metadata ADDED
@@ -0,0 +1,110 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: hash_auth_wsc
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Max Lahey
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-06-07 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 4.2.6
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 4.2.6
27
+ - !ruby/object:Gem::Dependency
28
+ name: rest-client
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 1.6.7
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 1.6.7
41
+ description: HashAuth allows your Rails application to support incoming and outgoing
42
+ two-factor authentication via hashing some component of an HTTPS request. Both sides
43
+ of the request (your Rails app and your client or provider) must have some unique
44
+ shared secret. This secret is used to create a hash of some portion of the request,
45
+ ensuring that (if neither side has been compromised) only the other party could
46
+ have created the request.
47
+ email:
48
+ - maxwellslahey@gmail.com
49
+ executables: []
50
+ extensions: []
51
+ extra_rdoc_files: []
52
+ files:
53
+ - MIT-LICENSE
54
+ - README.md
55
+ - Rakefile
56
+ - lib/generators/hash_auth/install_generator.rb
57
+ - lib/generators/hash_auth/strategy_generator.rb
58
+ - lib/generators/hash_auth/templates/initializer.rb
59
+ - lib/generators/hash_auth/templates/strategy.rb
60
+ - lib/hash-auth.rb
61
+ - lib/hash-auth/client.rb
62
+ - lib/hash-auth/config.rb
63
+ - lib/hash-auth/controllers.rb
64
+ - lib/hash-auth/controllers/helpers.rb
65
+ - lib/hash-auth/railtie.rb
66
+ - lib/hash-auth/strategies.rb
67
+ - lib/hash-auth/strategies/base.rb
68
+ - lib/hash-auth/strategies/default.rb
69
+ - lib/hash-auth/version.rb
70
+ - lib/hash-auth/web_request.rb
71
+ - lib/tasks/hash-auth_tasks.rake
72
+ - spec/controllers/helper_spec.rb
73
+ - spec/lib/client_spec.rb
74
+ - spec/lib/config_spec.rb
75
+ - spec/lib/railtie_spec.rb
76
+ - spec/lib/web_request_spec.rb
77
+ - spec/rails-helpers.rb
78
+ - spec/spec_helper.rb
79
+ homepage: http://maxwells.github.com
80
+ licenses: []
81
+ metadata: {}
82
+ post_install_message:
83
+ rdoc_options: []
84
+ require_paths:
85
+ - lib
86
+ required_ruby_version: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: '0'
91
+ required_rubygems_version: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ requirements: []
97
+ rubyforge_project:
98
+ rubygems_version: 2.5.1
99
+ signing_key:
100
+ specification_version: 4
101
+ summary: Rails gem to authenticate HTTP requests by hashing request components and
102
+ passing as parameter
103
+ test_files:
104
+ - spec/controllers/helper_spec.rb
105
+ - spec/lib/client_spec.rb
106
+ - spec/lib/config_spec.rb
107
+ - spec/lib/railtie_spec.rb
108
+ - spec/lib/web_request_spec.rb
109
+ - spec/rails-helpers.rb
110
+ - spec/spec_helper.rb