HashAuth
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.
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.
Note: Only Ruby 1.9.2 and above are supported, due to lack of ordered Hash objects in previous versions.
Features
- 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).
- 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)
- 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
- 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)
Usage
Installation
1) Install the HashAuth gem from RubyGems
$ gem install hash-auth
2) Add it to your Rails application's Gemfile
gem "hash-auth"
3) Install it into your Rails application
$ rails g hashauth:install
4) If you need to create your own strategies
$ rails g hashauth:strategy [name]
Configuration
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
Adding an authentication strategy.
Generate a new strategy, which will live in lib/hash_auth/strategies
rails g hash_auth:strategy name_of_strategy
This will generate a template that needs to be filled in with the necessary behavior for authenticating your client. Here is an example strategy
module HashAuth
module Strategies
class NameOfStrategy < Base
def name
:name_of_strategy
end
## 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.
def acquire_string_to_hash(controller, client)
controller.params.select{|k,v| k != 'controller' && k != 'action' }.map{|k,v| "#{k}=#{v}"}.join('&') + client.customer_key
end
# Client hashes string with SHA256
def hash_string(string, client)
Digest::sha2.new(256) << string
end
def on_authentication(client)
# 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)
end
end
end
end
Adding Clients via hardcoding (Config).
#### Adding a new client
# 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)
add_client {
:customer_key => '1234567890',
# the shared secret between you and a client
:customer_identifier => 'my_organization',
# the unique identifer the client will pass you to identify themselves
:customer_identifier_param => 'customer_id',
# the name of the parameter the client will pass their unique identifier in
:valid_domains => '*my_organization.org',
# will allow request from anything ending with my_organization.org, can also provide a list
:strategy => :my_auth_strategy,
# 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
}
Adding Clients from an external resource (eg YAML, Database).
YAML file (config/clients.yml in this example):
clients:
-
customer_key: 1234567890
customer_identifier: my_organization
customer_identifier_param: customer_id
valid_domains: '*my_organization.org'
strategy: :default
custom_key: custom_value
-
customer_key: 0987654321
customer_identifier: your_organization
customer_identifier_param: customer_id
valid_domains: ['your_organization.com', 'your_organization.org']
strategy: :my_auth_strategy
custom_key: custom_value
hash-auth initializer:
clients = YAML::load( File.open('config/clients.yml') )
add_clients clients["clients"]
Options in hash-auth initializer
Any custom client field can be initialized with a default value through method missing
HashAuth.configure do
{:status => "success" }
end
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.
## In a custom strategy…
def self.on_failure(client, controller)
@failed_authentication_status = {:status => 'failure'}
end
## In the controller…
def my_action
if (@authenticated)
... Do necessary stuff
response = @client.authentication_success_status_message
else
response = @failed_authentication_json
end
respond_to do |format|
format.json { render :json => response }
end
end
Additionally, the default strategy for every client can be set (if not set, will revert to HashAuth::Strategies::Default)
set_default_strategy :strategy_identifier
Implementation: Receiving hashed requests
In whatever controller(s) require hash authentication of requests
validates_auth_for :action_one, :action_two
The following variables are available in implementing controller actions
@client : HashAuth::Client - instance of client (if found, whether or not authenticated)
@authenticated : Boolean - whether or not the hashed request was considered validated
Implementation: Making hashed requests
In whatever controllers, models, or otherwise that require creating hash authenticated requests, use the HashAuth::WebRequest around REST client.
client = HashAuth.find_client 'my_organization'
HashAuth::WebRequest.post client, 'localhost:3000/test/one', {:foo => :bar, :bar => :baz}
WebRequest supports:
def self.get(client, url, parameters = {}, &block)
def self.post(client, url, payload, headers = {}, &block)
def self.patch(client, url, payload, headers = {}, &block)
def self.put(client, url, payload, headers = {}, &block)
def self.delete(client, url, parameters = {}, &block)
def self.head(client, url, parameters = {}, &block)
def self.options(client, url, parameters = {}, &block)
See REST client for futher detail.
Examples
This project rocks and uses MIT-LICENSE.