Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Initial commit

  • Loading branch information...
commit 523526930341a3a82d082781e627ab21cfdf2658 0 parents
Pascal Hurni authored
20 LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2010 Pascal Hurni
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+'Software'), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
135 README.rdoc
@@ -0,0 +1,135 @@
+= authlogic-api
+
+http://github.com/phurni/authlogic_api
+
+== DESCRIPTION:
+
+This is a plugin for Authlogic to allow API requests to be authenticated automatically by using
+an api_key/signature mechanism. The plugin will automatically compute the hashed sum of the
+request params and compare it to the passed signature.
+
+== REQUIREMENTS:
+
+* authlogic
+
+== INSTALL:
+
+* script/plugin install git://github.com/phurni/authlogic_api.git
+* optionally create a migration for an application_accounts table.
+* create an application session model and configure it with Authlogic.
+
+== EXAMPLE:
+
+You certainly want to separate User sessions from Application sessions. You'll have to create say an ApplicationSession like this:
+ class ApplicationSession < Authlogic::Session::Base
+ authenticate_with ApplicationAccount
+
+ api_key_param 'app_key'
+ end
+
+Because Authlogic will infer the model holding the credentials from the session class name, which would result in _Application_,
+we explicitely tell it to use our ApplicationAccount model.
+
+Then to enable API access, we tell AuthlogicApi the name of the param key which will get the application id.
+
+The ApplicationAccount model will look like:
+ class ApplicationAccount < ActiveRecord::Base
+ acts_as_authentic do |config|
+ end # the configuration block is optional
+ end
+
+The table should have a _api_key_ field and also a _api_secret_ field. You can change these default names with the config
+block on the acts_as_authentic call.
+
+Everything is now setup to authenticate API request. Note that it's up to you to create your controllers,
+configure them to respond with your prefered data format and all the other stuff.
+
+Read the AuthlogicApi::ActsAsAuthentic::Config portion for config options.
+
+=== Client side
+
+I'll describe here what the client has to do to create requests that will be authenticated by the AuthlogicApi backend.
+
+==== GET requests
+
+Every parameter given in the query string will be summed and hashed to form a signature. Here is the pseudo-code to use:
+ args = array of arguments for the request, each argument is a key/value pair
+ args += app_key
+ sorted_args = alphabetically_sort_array_by_keys(args);
+ request_str = concatenate_in_order(sorted_args);
+ signature = md5(concatenate(request_str, secret))
+
+Let's take an example:
+ here is the original URL of the request:
+ http://montain.mil/private/rockets/launch?rocket_id=42&speed=5
+
+ The args with the app_key:
+ rocket_id: 42
+ speed: 5
+ app_key: A6G87bqY
+
+ The sorted args
+ app_key: A6G87bqY
+ rocket_id: 42
+ speed: 5
+
+ The request string (note that there is no delimiter between key/value, neither between arguments)
+ app_keyA6G87bqYrocket_id42speed5
+
+ Now the client has to know the application secret, it must add it to the request string:
+ app_keyA6G87bqYrocket_id42speed5SECRET
+
+ Hash this with MD5:
+ 5266bf02b183ffac3898b802e62d45d6
+
+ here is the URL for the signed request:
+ http://montain.mil/private/rockets/launch?rocket_id=42&speed=5&app_key=A6G87bqY&signature=5266bf02b183ffac3898b802e62d45d6
+
+==== POST requests
+
+The principle is the same, but only the POST data is hased. The app_key and the signature are passed into the query string.
+
+Example:
+ here is the original URL of the request:
+ http://montain.mil/private/rockets/launch
+
+ the POST data:
+ <?xml version="1.0" encoding="UTF-8"?>
+ <rocket>
+ <id>42</id>
+ <speed>5</speed>
+ </rocket>
+
+ the MD5 hash of the post data:
+ 82f5aef6d4a4ad710a60b74e0355b74d
+
+ here is the URL for the signed request:
+ http://montain.mil/private/rockets/launch?app_key=A6G87bqY&signature=82f5aef6d4a4ad710a60b74e0355b74d
+
+ Note that the POST data is left untouched.
+
+
+== LICENSE:
+
+(The MIT License)
+
+Copyright (c) 2010 Pascal Hurni
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+'Software'), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
5 lib/authlogic_api.rb
@@ -0,0 +1,5 @@
+require "authlogic_api/acts_as_authentic"
+require "authlogic_api/session"
+
+ActiveRecord::Base.send(:include, AuthlogicApi::ActsAsAuthentic)
+Authlogic::Session::Base.send(:include, AuthlogicApi::Session)
68 lib/authlogic_api/acts_as_authentic.rb
@@ -0,0 +1,68 @@
+module AuthlogicApi
+ # Handles configuration for the _api_key_ and _api_secret_ fields of your ApplicationAccount model.
+ #
+ module ActsAsAuthentic
+ def self.included(klass)
+ klass.class_eval do
+ extend Config
+ add_acts_as_authentic_module(Methods)
+ end
+ end
+
+ module Config
+ # The name of the api key field in the database.
+ #
+ # * <tt>Default:</tt> :api_key or :application_key, if they exist
+ # * <tt>Accepts:</tt> Symbol
+ def api_key_field(value = nil)
+ rw_config(:api_key_field, value, first_column_to_exist(nil, :api_key, :application_key))
+ end
+ alias_method :api_key_field=, :api_key_field
+
+ # The name of the api secret field in the database.
+ #
+ # * <tt>Default:</tt> :api_secret or :application_secret, if they exist
+ # * <tt>Accepts:</tt> Symbol
+ def api_secret_field(value = nil)
+ rw_config(:api_secret_field, value, first_column_to_exist(nil, :api_secret, :application_secret))
+ end
+ alias_method :api_secret_field=, :api_secret_field
+
+ # Switch to control wether the _api_key_ and _api_secret_ fields should be automatically generated for you.
+ # Note that the generation is done in a before_validation callback, and if you already populated these fields they
+ # will not be overridden.
+ #
+ # * <tt>Default:</tt> true
+ # * <tt>Accepts:</tt> Boolean
+ def enable_api_fields_generation(value = nil)
+ rw_config(:enable_api_fields_generation, value, true)
+ end
+ alias_method :enable_api_fields_generation=, :enable_api_fields_generation
+
+ end
+
+ module Methods
+ def self.included(klass)
+ klass.class_eval do
+ before_validation :generate_key_and_secret, :if => :enable_api_fields_generation?
+ end
+ end
+
+ private
+
+ def has_api_columns?
+ self.class.api_key_field && self.class.api_secret_field
+ end
+
+ def enable_api_fields_generation?
+ self.class.enable_api_fields_generation && has_api_columns?
+ end
+
+ def generate_key_and_secret
+ send("#{self.class.api_key_field}=", Authlogic::Random.friendly_token) unless send(self.class.api_key_field)
+ send("#{self.class.api_secret_field}=", Authlogic::Random.hex_token) unless send(self.class.api_secret_field)
+ end
+ end
+
+ end
+end
163 lib/authlogic_api/session.rb
@@ -0,0 +1,163 @@
+module AuthlogicApi
+ # Note that because authenticating through an API is a single access authentication, many of the magic columns are
+ # not updated. Here is a list of the magic columns with their update state:
+ # login_count Never increased because there's no explicit login
+ # failed_login_count Updated. That is every signature mismatch will increase this value.
+ # last_request_at Updated.
+ # current_login_at Left unchanged.
+ # last_login_at Left unchanged.
+ # current_login_ip Left unchanged.
+ # last_login_ip Left unchanged.
+ #
+ # AuthlogicApi adds some more magic columns to fill the gap, here they are:
+ # request_count Increased every time a request is made.
+ # Counts also invalid requests, so this is the total count.
+ # To have the count of valid requests use : request_count - failed_login_count
+ # last_request_ip Updates with the request remote_ip for each request.
+ #
+ module Session
+ def self.included(klass)
+ klass.class_eval do
+ extend Config
+ include Methods
+ end
+ end
+
+ module Config
+ # Defines the param key name where the api_key will be received.
+ #
+ # You *must* define this to enable API authentication.
+ #
+ # * <tt>Default:</tt> nil
+ # * <tt>Accepts:</tt> String
+ def api_key_param(value = nil)
+ rw_config(:api_key_param, value, nil)
+ end
+ alias_method :api_key_param=, :api_key_param
+
+ # Defines the param key name where the signature will be received.
+ #
+ # * <tt>Default:</tt> 'signature'
+ # * <tt>Accepts:</tt> String
+ def api_signature_param(value = nil)
+ rw_config(:api_signature_param, value, 'signature')
+ end
+ alias_method :api_signature_param=, :api_signature_param
+
+ # To be able to authenticate the incoming request, AuthlogicApi has to find a valid api_key in your system.
+ # This config setting let's you choose which method to call on your model to get an application model object.
+ #
+ # Let's say you have an ApplicationSession that is authenticating an ApplicationAccount. By default ApplicationSession will
+ # call ApplicationAccount.find_by_api_key(api_key).
+ #
+ # * <tt>Default:</tt> :find_by_api_key
+ # * <tt>Accepts:</tt> Symbol or String
+ def find_by_api_key_method(value = nil)
+ rw_config(:find_by_api_key_method, value, :find_by_api_key)
+ end
+ alias_method :find_by_api_key_method=, :find_by_api_key_method
+
+ # The generation of the request signature is selectable by this config setting.
+ # You may either directly override the Methods#generate_api_signature method on the Session class,
+ # or use this config to select another method.
+ #
+ # The default implementation of #generate_api_signature is the following:
+ # def generate_api_signature(secret)
+ # Digest::MD5.hexdigest(build_api_payload + secret)
+ # end
+ #
+ # Note the call to #build_api_payload, which is another method you may override to customize
+ # your own way of building the payload that will be signed.
+ # WARNING: The current implementation of #build_api_payload is Rails oriented. Override if you use another framework.
+ #
+ # * <tt>Default:</tt> :generate_api_signature
+ # * <tt>Accepts:</tt> Symbol
+ def generate_api_signature_method(value = nil)
+ rw_config(:generate_api_signature_method, value, :generate_api_signature)
+ end
+ alias_method :generate_api_signature_method=, :generate_api_signature_method
+ end
+
+ module Methods
+ def self.included(klass)
+ klass.class_eval do
+ attr_accessor :single_access
+ persist :persist_by_api, :if => :authenticating_with_api?
+ validate :validate_by_api, :if => :authenticating_with_api?
+ after_persisting :set_api_magic_columns, :if => :authenticating_with_api?
+ end
+ end
+
+ # Hooks into credentials to print out meaningful credentials for API authentication.
+ def credentials
+ authenticating_with_api? ? {:api_key => api_key} : super
+ end
+
+ private
+ def persist_by_api
+ self.unauthorized_record = search_for_record(self.class.find_by_api_key_method, api_key)
+ self.single_access = valid?
+ end
+
+ def validate_by_api
+ self.attempted_record = search_for_record(self.class.find_by_api_key_method, api_key)
+ if attempted_record.blank?
+ generalize_credentials_error_messages? ?
+ add_general_credentials_error :
+ errors.add(api_key_param, I18n.t('error_messages.api_key_not_found', :default => "is not valid"))
+ return
+ end
+
+ signature = send(self.class.generate_api_signature_method, attempted_record.send(klass.api_secret_field))
+ if api_signature != signature
+ self.invalid_password = true # magic columns housekeeping
+ generalize_credentials_error_messages? ?
+ add_general_credentials_error :
+ errors.add(api_signature_param, I18n.t('error_messages.invalid_signature', :default => "is not valid"))
+ return
+ end
+ end
+
+ def authenticating_with_api?
+ !api_key.blank? && !api_signature.blank?
+ end
+
+ def api_key
+ controller.params[api_key_param]
+ end
+
+ def api_signature
+ controller.params[api_signature_param]
+ end
+
+ def api_key_param
+ self.class.api_key_param
+ end
+
+ def api_signature_param
+ self.class.api_signature_param
+ end
+
+ # WARNING: Rails specfic way of building payload
+ def build_api_payload
+ request = controller.request
+ if request.post? || request.put?
+ request.raw_post
+ else
+ params = request.query_parameters.reject {|key, value| key.to_s == api_signature_param}
+ params.sort_by {|key, value| key.to_s.underscore}.join('')
+ end
+ end
+
+ def generate_api_signature(secret)
+ Digest::MD5.hexdigest(build_api_payload + secret)
+ end
+
+ def set_api_magic_columns
+ record.request_count = (record.request_count.blank? ? 1 : record.request_count + 1) if record.respond_to?(:request_count)
+ record.last_request_ip = controller.request.remote_ip if record.respond_to?(:last_request_ip)
+ end
+
+ end
+ end
+end
2  rails/init.rb
@@ -0,0 +1,2 @@
+# Include hook code here
+require 'authlogic_api'
Please sign in to comment.
Something went wrong with that request. Please try again.