├── LICENSE ├── README.rdoc ├── lib ├── authlogic_api.rb └── authlogic_api │ ├── acts_as_authentic.rb │ └── session.rb └── rails └── init.rb /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010 Pascal Hurni 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 NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 18 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 20 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | = authlogic-api 2 | 3 | http://github.com/phurni/authlogic_api 4 | 5 | == DESCRIPTION: 6 | 7 | This is a plugin for Authlogic to allow API requests to be authenticated automatically by using 8 | an api_key/signature mechanism. The plugin will automatically compute the hashed sum of the 9 | request params and compare it to the passed signature. 10 | 11 | == REQUIREMENTS: 12 | 13 | * authlogic 14 | 15 | == INSTALL: 16 | 17 | * script/plugin install git://github.com/phurni/authlogic_api.git 18 | * optionally create a migration for an application_accounts table. 19 | * create an application session model and configure it with Authlogic. 20 | 21 | == EXAMPLE: 22 | 23 | You certainly want to separate User sessions from Application sessions. You'll have to create say an ApplicationSession like this: 24 | class ApplicationSession < Authlogic::Session::Base 25 | authenticate_with ApplicationAccount 26 | 27 | api_key_param 'app_key' 28 | end 29 | 30 | Because Authlogic will infer the model holding the credentials from the session class name, which would result in _Application_, 31 | we explicitely tell it to use our ApplicationAccount model. 32 | 33 | Then to enable API access, we tell AuthlogicApi the name of the param key which will get the application id. 34 | 35 | The ApplicationAccount model will look like: 36 | class ApplicationAccount < ActiveRecord::Base 37 | acts_as_authentic do |config| 38 | end # the configuration block is optional 39 | end 40 | 41 | The table should have a _api_key_ field and also a _api_secret_ field. You can change these default names with the config 42 | block on the acts_as_authentic call. 43 | 44 | Everything is now setup to authenticate API request. Note that it's up to you to create your controllers, 45 | configure them to respond with your prefered data format and all the other stuff. 46 | 47 | Read the AuthlogicApi::ActsAsAuthentic::Config portion for config options. 48 | 49 | === Client side 50 | 51 | I'll describe here what the client has to do to create requests that will be authenticated by the AuthlogicApi backend. 52 | 53 | ==== GET requests 54 | 55 | Every parameter given in the query string will be summed and hashed to form a signature. Here is the pseudo-code to use: 56 | args = array of arguments for the request, each argument is a key/value pair 57 | args += app_key 58 | sorted_args = alphabetically_sort_array_by_keys(args); 59 | request_str = concatenate_in_order(sorted_args); 60 | signature = md5(concatenate(request_str, secret)) 61 | 62 | Let's take an example: 63 | here is the original URL of the request: 64 | http://montain.mil/private/rockets/launch?rocket_id=42&speed=5 65 | 66 | The args with the app_key: 67 | rocket_id: 42 68 | speed: 5 69 | app_key: A6G87bqY 70 | 71 | The sorted args 72 | app_key: A6G87bqY 73 | rocket_id: 42 74 | speed: 5 75 | 76 | The request string (note that there is no delimiter between key/value, neither between arguments) 77 | app_keyA6G87bqYrocket_id42speed5 78 | 79 | Now the client has to know the application secret, it must add it to the request string: 80 | app_keyA6G87bqYrocket_id42speed5SECRET 81 | 82 | Hash this with MD5: 83 | 5266bf02b183ffac3898b802e62d45d6 84 | 85 | here is the URL for the signed request: 86 | http://montain.mil/private/rockets/launch?rocket_id=42&speed=5&app_key=A6G87bqY&signature=5266bf02b183ffac3898b802e62d45d6 87 | 88 | ==== POST requests 89 | 90 | The principle is the same, but only the POST data is hased. The app_key and the signature are passed into the query string. 91 | 92 | Example: 93 | here is the original URL of the request: 94 | http://montain.mil/private/rockets/launch 95 | 96 | the POST data: 97 | 98 | 99 | 42 100 | 5 101 | 102 | 103 | Now the client has to know the application secret, it must add it to the POST data: 104 | 105 | 106 | 42 107 | 5 108 | 109 | SECRET 110 | 111 | the MD5 hash of the post data with secret: 112 | 82f5aef6d4a4ad710a60b74e0355b74d 113 | 114 | here is the URL for the signed request: 115 | http://montain.mil/private/rockets/launch?app_key=A6G87bqY&signature=82f5aef6d4a4ad710a60b74e0355b74d 116 | 117 | Note that the POST data is left untouched. (Do not send the secret) 118 | 119 | 120 | == LICENSE: 121 | 122 | (The MIT License) 123 | 124 | Copyright (c) 2010 Pascal Hurni 125 | 126 | Permission is hereby granted, free of charge, to any person obtaining 127 | a copy of this software and associated documentation files (the 128 | 'Software'), to deal in the Software without restriction, including 129 | without limitation the rights to use, copy, modify, merge, publish, 130 | distribute, sublicense, and/or sell copies of the Software, and to 131 | permit persons to whom the Software is furnished to do so, subject to 132 | the following conditions: 133 | 134 | The above copyright notice and this permission notice shall be 135 | included in all copies or substantial portions of the Software. 136 | 137 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 138 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 139 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 140 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 141 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 142 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 143 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 144 | -------------------------------------------------------------------------------- /lib/authlogic_api.rb: -------------------------------------------------------------------------------- 1 | require "authlogic_api/acts_as_authentic" 2 | require "authlogic_api/session" 3 | 4 | ActiveRecord::Base.send(:include, AuthlogicApi::ActsAsAuthentic) 5 | Authlogic::Session::Base.send(:include, AuthlogicApi::Session) -------------------------------------------------------------------------------- /lib/authlogic_api/acts_as_authentic.rb: -------------------------------------------------------------------------------- 1 | module AuthlogicApi 2 | # Handles configuration for the _api_key_ and _api_secret_ fields of your ApplicationAccount model. 3 | # 4 | module ActsAsAuthentic 5 | def self.included(klass) 6 | klass.class_eval do 7 | extend Config 8 | add_acts_as_authentic_module(Methods) 9 | end 10 | end 11 | 12 | module Config 13 | # The name of the api key field in the database. 14 | # 15 | # * Default: :api_key or :application_key, if they exist 16 | # * Accepts: Symbol 17 | def api_key_field(value = nil) 18 | rw_config(:api_key_field, value, first_column_to_exist(nil, :api_key, :application_key)) 19 | end 20 | alias_method :api_key_field=, :api_key_field 21 | 22 | # The name of the api secret field in the database. 23 | # 24 | # * Default: :api_secret or :application_secret, if they exist 25 | # * Accepts: Symbol 26 | def api_secret_field(value = nil) 27 | rw_config(:api_secret_field, value, first_column_to_exist(nil, :api_secret, :application_secret)) 28 | end 29 | alias_method :api_secret_field=, :api_secret_field 30 | 31 | # Switch to control wether the _api_key_ and _api_secret_ fields should be automatically generated for you. 32 | # Note that the generation is done in a before_validation callback, and if you already populated these fields they 33 | # will not be overridden. 34 | # 35 | # * Default: true 36 | # * Accepts: Boolean 37 | def enable_api_fields_generation(value = nil) 38 | rw_config(:enable_api_fields_generation, value, true) 39 | end 40 | alias_method :enable_api_fields_generation=, :enable_api_fields_generation 41 | 42 | end 43 | 44 | module Methods 45 | def self.included(klass) 46 | klass.class_eval do 47 | before_validation :generate_key_and_secret, :if => :enable_api_fields_generation? 48 | end 49 | end 50 | 51 | private 52 | 53 | def has_api_columns? 54 | self.class.api_key_field && self.class.api_secret_field 55 | end 56 | 57 | def enable_api_fields_generation? 58 | self.class.enable_api_fields_generation && has_api_columns? 59 | end 60 | 61 | def generate_key_and_secret 62 | send("#{self.class.api_key_field}=", Authlogic::Random.friendly_token) unless send(self.class.api_key_field) 63 | send("#{self.class.api_secret_field}=", Authlogic::Random.hex_token) unless send(self.class.api_secret_field) 64 | end 65 | end 66 | 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /lib/authlogic_api/session.rb: -------------------------------------------------------------------------------- 1 | module AuthlogicApi 2 | # Note that because authenticating through an API is a single access authentication, many of the magic columns are 3 | # not updated. Here is a list of the magic columns with their update state: 4 | # login_count Never increased because there's no explicit login 5 | # failed_login_count Updated. That is every signature mismatch will increase this value. 6 | # last_request_at Updated. 7 | # current_login_at Left unchanged. 8 | # last_login_at Left unchanged. 9 | # current_login_ip Left unchanged. 10 | # last_login_ip Left unchanged. 11 | # 12 | # AuthlogicApi adds some more magic columns to fill the gap, here they are: 13 | # request_count Increased every time a request is made. 14 | # Counts also invalid requests, so this is the total count. 15 | # To have the count of valid requests use : request_count - failed_login_count 16 | # last_request_ip Updates with the request remote_ip for each request. 17 | # 18 | module Session 19 | def self.included(klass) 20 | klass.class_eval do 21 | extend Config 22 | include Methods 23 | end 24 | end 25 | 26 | module Config 27 | # Defines the param key name where the api_key will be received. 28 | # 29 | # You *must* define this to enable API authentication. 30 | # 31 | # * Default: nil 32 | # * Accepts: String 33 | def api_key_param(value = nil) 34 | rw_config(:api_key_param, value, nil) 35 | end 36 | alias_method :api_key_param=, :api_key_param 37 | 38 | # Defines the param key name where the signature will be received. 39 | # 40 | # * Default: 'signature' 41 | # * Accepts: String 42 | def api_signature_param(value = nil) 43 | rw_config(:api_signature_param, value, 'signature') 44 | end 45 | alias_method :api_signature_param=, :api_signature_param 46 | 47 | # To be able to authenticate the incoming request, AuthlogicApi has to find a valid api_key in your system. 48 | # This config setting let's you choose which method to call on your model to get an application model object. 49 | # 50 | # Let's say you have an ApplicationSession that is authenticating an ApplicationAccount. By default ApplicationSession will 51 | # call ApplicationAccount.find_by_api_key(api_key). 52 | # 53 | # * Default: :find_by_api_key 54 | # * Accepts: Symbol or String 55 | def find_by_api_key_method(value = nil) 56 | rw_config(:find_by_api_key_method, value, :find_by_api_key) 57 | end 58 | alias_method :find_by_api_key_method=, :find_by_api_key_method 59 | 60 | # The generation of the request signature is selectable by this config setting. 61 | # You may either directly override the Methods#generate_api_signature method on the Session class, 62 | # or use this config to select another method. 63 | # 64 | # The default implementation of #generate_api_signature is the following: 65 | # def generate_api_signature(secret) 66 | # Digest::MD5.hexdigest(build_api_payload + secret) 67 | # end 68 | # 69 | # Note the call to #build_api_payload, which is another method you may override to customize 70 | # your own way of building the payload that will be signed. 71 | # WARNING: The current implementation of #build_api_payload is Rails oriented. Override if you use another framework. 72 | # 73 | # * Default: :generate_api_signature 74 | # * Accepts: Symbol 75 | def generate_api_signature_method(value = nil) 76 | rw_config(:generate_api_signature_method, value, :generate_api_signature) 77 | end 78 | alias_method :generate_api_signature_method=, :generate_api_signature_method 79 | end 80 | 81 | module Methods 82 | def self.included(klass) 83 | klass.class_eval do 84 | attr_accessor :single_access 85 | persist :persist_by_api, :if => :authenticating_with_api? 86 | validate :validate_by_api, :if => :authenticating_with_api? 87 | after_persisting :set_api_magic_columns, :if => :authenticating_with_api? 88 | end 89 | end 90 | 91 | # Hooks into credentials to print out meaningful credentials for API authentication. 92 | def credentials 93 | authenticating_with_api? ? {:api_key => api_key} : super 94 | end 95 | 96 | private 97 | def persist_by_api 98 | self.unauthorized_record = search_for_record(self.class.find_by_api_key_method, api_key) 99 | self.single_access = valid? 100 | end 101 | 102 | def validate_by_api 103 | self.attempted_record = search_for_record(self.class.find_by_api_key_method, api_key) 104 | if attempted_record.blank? 105 | generalize_credentials_error_messages? ? 106 | add_general_credentials_error : 107 | errors.add(api_key_param, I18n.t('error_messages.api_key_not_found', :default => "is not valid")) 108 | return 109 | end 110 | 111 | signature = send(self.class.generate_api_signature_method, attempted_record.send(klass.api_secret_field)) 112 | if api_signature != signature 113 | self.invalid_password = true # magic columns housekeeping 114 | generalize_credentials_error_messages? ? 115 | add_general_credentials_error : 116 | errors.add(api_signature_param, I18n.t('error_messages.invalid_signature', :default => "is not valid")) 117 | return 118 | end 119 | end 120 | 121 | def authenticating_with_api? 122 | !api_key.blank? && !api_signature.blank? 123 | end 124 | 125 | def api_key 126 | controller.params[api_key_param] 127 | end 128 | 129 | def api_signature 130 | controller.params[api_signature_param] 131 | end 132 | 133 | def api_key_param 134 | self.class.api_key_param 135 | end 136 | 137 | def api_signature_param 138 | self.class.api_signature_param 139 | end 140 | 141 | # WARNING: Rails specfic way of building payload 142 | def build_api_payload 143 | request = controller.request 144 | if request.post? || request.put? 145 | request.raw_post 146 | else 147 | params = request.query_parameters.reject {|key, value| key.to_s == api_signature_param} 148 | params.sort_by {|key, value| key.to_s.underscore}.join('') 149 | end 150 | end 151 | 152 | def generate_api_signature(secret) 153 | Digest::MD5.hexdigest(build_api_payload + secret) 154 | end 155 | 156 | def set_api_magic_columns 157 | record.request_count = (record.request_count.blank? ? 1 : record.request_count + 1) if record.respond_to?(:request_count) 158 | record.last_request_ip = controller.request.remote_ip if record.respond_to?(:last_request_ip) 159 | end 160 | 161 | end 162 | end 163 | end -------------------------------------------------------------------------------- /rails/init.rb: -------------------------------------------------------------------------------- 1 | # Include hook code here 2 | require 'authlogic_api' 3 | --------------------------------------------------------------------------------