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 +20 -0
- data/README.md +201 -0
- data/Rakefile +37 -0
- data/lib/generators/hash_auth/install_generator.rb +11 -0
- data/lib/generators/hash_auth/strategy_generator.rb +12 -0
- data/lib/generators/hash_auth/templates/initializer.rb +26 -0
- data/lib/generators/hash_auth/templates/strategy.rb +47 -0
- data/lib/hash-auth/client.rb +34 -0
- data/lib/hash-auth/config.rb +110 -0
- data/lib/hash-auth/controllers/helpers.rb +52 -0
- data/lib/hash-auth/controllers.rb +5 -0
- data/lib/hash-auth/railtie.rb +10 -0
- data/lib/hash-auth/strategies/base.rb +47 -0
- data/lib/hash-auth/strategies/default.rb +39 -0
- data/lib/hash-auth/strategies.rb +6 -0
- data/lib/hash-auth/version.rb +3 -0
- data/lib/hash-auth/web_request.rb +65 -0
- data/lib/hash-auth.rb +27 -0
- data/lib/tasks/hash-auth_tasks.rake +4 -0
- data/spec/controllers/helper_spec.rb +41 -0
- data/spec/fake-rails-app.rb +168 -0
- data/spec/lib/client_spec.rb +23 -0
- data/spec/lib/config_spec.rb +41 -0
- data/spec/lib/railtie_spec.rb +9 -0
- data/spec/lib/web_request_spec.rb +74 -0
- data/spec/spec_helper.rb +8 -0
- metadata +117 -0
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,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,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,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,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
|
data/spec/spec_helper.rb
ADDED
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:
|