├── 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 |
--------------------------------------------------------------------------------