merb-auth-core 0.9.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2008 Adam French, Daniel Neighman
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.textile ADDED
@@ -0,0 +1,338 @@
1
+ h2. merb-auth-core
2
+
3
+ MerbAuth is an authentication framework for use with the
4
+ Merb web framework.
5
+
6
+ MerbAuth does not try to dictate what you should use as a user model, or how
7
+ it should authenticate. Instead it focuses on the logic required to check
8
+ that an object passes authentication, and store authenticated objects in the
9
+ session. This is in fact the guiding principle of MerbAuth. The Session is
10
+ used as the place for authentication, with a sprinkling of controller helpers.
11
+ This makes sense to talk about an authenticated session. For example, inside
12
+ your controller:
13
+
14
+ * session.authenticated?
15
+ returns true if the session has been
16
+ authenticated. False otherwise # session.authenticate(controller)
17
+ authenticates the session based on customizable user defined rules
18
+
19
+ * session.user
20
+ returns the currently authenticated user object
21
+
22
+ * session.user=
23
+ manually sets the currently authenticated user object
24
+
25
+ * session.abandon!
26
+ sets the session to unauthenticated, and clears all session data
27
+
28
+ MerbAuth makes use of Merb's exception handling facilities which return correct
29
+ HTTP status codes when a 200 OK would be inappropriate. To fail a login, or
30
+ to force a login at any point in your controller code, simply raise an
31
+ Unauthenticated exception, with an optional message and the user will be
32
+ presented with login page. The login page is in fact the html view for
33
+ Extensions#unauthenticated
34
+
35
+ To protect your controllers, add a simple @before@ filter to your controller.
36
+
37
+ <code> before :ensure_authenticated </code>
38
+
39
+ It is possible to use MerbAuth with any object as a _user_ object, provided
40
+ that object does not evaluate to false and it can be serialized in an out of the session.
41
+ For this reason, merb-auth-core does not try to implement even a simple login
42
+ form for you, since it may not meet your requirements.
43
+
44
+ h3. How Does It Authenticate my arbitrary user?
45
+
46
+ This is very similar to the BootLoader process in Merbs initialization.
47
+ You declare a class that inherits from Merb::Authentication::Strategy and
48
+ define an instance method @run!@
49
+
50
+ <pre><code>
51
+ class PasswordStrategy &lt Merb::Authentication::Strategy
52
+
53
+ def run!
54
+ if params[:login] && params[:password]
55
+ user_class.authenticate(params[:login], params[:password])
56
+ end
57
+ end
58
+ end
59
+
60
+ </code></pre>
61
+
62
+ bq. This login strategy uses the @authenticate@ method on the User class to
63
+ retrieve a user by @login@ and @password@. Remember, you can put as much logic
64
+ here as you require. The strategy uses an instance variable so the full power of
65
+ classes are available to your strategy.
66
+
67
+ The strategy provides access to the current request giving you access
68
+ to the params hash, session etc.
69
+
70
+ To pass authentication, simply return a non-nil
71
+ non-false value from the @run!@ method. Any false or nil value will cause
72
+ that strategy to fail. Then the next strategy will be tried :) wait... what?
73
+
74
+ You can add as many strategies as you like and they will be tried one after
75
+ another until either one is found that works (login), or none of them have
76
+ passed (failed attempt).
77
+
78
+ <pre><code>
79
+ class PasswordLoginBasicAuth &lt Merb::Authentication::Strategy
80
+ def run!
81
+ if params[:api_key] && params[:api_token]
82
+ Machine.api_authenticate(params[:api_key], params[:api_token])
83
+ end
84
+ end
85
+ end
86
+ </code></pre>
87
+
88
+ Now that we have two, they will be executed in the order that they were declared
89
+ when we call @session.authenticate!(self)@. The first one that
90
+ returns a value that doesn't evaluate to false, will be considered the winner.
91
+
92
+ h3. Customizing the user_class
93
+
94
+ Notice the @user_class@ method in the above strategy examples. This is a convenience method on a strategy
95
+ to provide you with the user_class to use for this strategy. You can overwrite
96
+ this method on a per strategy basis to use different user model types.
97
+
98
+ By default the strategy#user_class method will defer to Merb::Authentication#user_class. You can
99
+ set which is the "default class" that Merb::Authentication will use in the provided strategies by
100
+ setting the class inside your model declaration.
101
+
102
+ <pre><code>
103
+ class Person
104
+ include DataMapper::Resource
105
+
106
+ Merb::Authentication.user_class = self
107
+
108
+ #...
109
+ end
110
+ </code></pre>
111
+
112
+ This will cascade throughout the default strategies, and your own strategies vai that
113
+ Merb::Authentication::Strategy#user_class method.
114
+
115
+ There is no default class set for Activation by default
116
+
117
+ h3. Strategies and Inheritance
118
+
119
+ Strategies may be inherited multiple times to make the job of combining similar aspects easier.
120
+ You can inherit as many levels as you like and at any point you may mark a strategy as _abstract_
121
+
122
+ An abstract strategy just means that it will not be run when it comes time to authenticate.
123
+ Instead it's good to put common logic in and then inherit from it to keep your strategies DRY.
124
+
125
+ To mark a class as abstract, use the @abstract!@ class method.
126
+
127
+ <pre><code>
128
+ class AbstractStrategy < Merb::Authentication::Strategy
129
+ abstract!
130
+ end
131
+ </code></pre>
132
+
133
+ At any point you can activate a registered strategy. You don't need to register
134
+ your strategies, you just declare them, but plugin developers make life easier when they do.
135
+
136
+ To activate a registered strategy:
137
+ <pre><code>
138
+ Merb::Authentication.activate!(:defualt_password_form)
139
+ </code></pre>
140
+
141
+ You can easily mix this in with your own strategies. In you lib/authentication/strategies.rb
142
+ <pre><code>
143
+ class MyStrategy < Merb::Authentication::Strategy
144
+ def run!
145
+ #...
146
+ end
147
+ end
148
+
149
+ Merb::Authentication.activate!(:default_openid)
150
+
151
+ class AnotherStrategy < Merb::Authentication::Strategy
152
+ def run!
153
+ #...
154
+ end
155
+ end
156
+ </code></pre>
157
+
158
+ This will collect them in order of decleration. i.e.:
159
+ MyStrategy, Merb::Authentication::Strategies::Basic::OpenID, AnotherStrategy
160
+
161
+ h3. Customizing the order of the strategies
162
+
163
+ By default, strategies are run in the order they are declared. It's possible
164
+ to customize the order that the strategies are called.
165
+
166
+ @Merb::Authentication.default_strategy_order@ will return an array of
167
+ the strategy classes in the order that they will be run.
168
+ You can customize this by setting the default_strategy_order array
169
+ manually.
170
+
171
+ @Authenticateion.default_strategy_order.order = [Second, First, Fourth]@
172
+
173
+ It's possible to leave some out, and re-order existing ones. It will error
174
+ out if you specify one that doesn't exist though.
175
+
176
+ h3. Specifying selected strategies per action
177
+
178
+ It's possible to configure each call to @ensure_authenticated@ with a custom list
179
+ of strategies to run. These will be run in order and should have an instance method
180
+ of #run!
181
+
182
+ <pre><code>
183
+ class ApiMethods < Application
184
+ before :ensure_authenticated, :with => [
185
+ Merb::Authenticated::Strategies::Basic::Form,
186
+ Merb::Authenticated::Strategies::Basic::BasicAuth,
187
+ Merb::Authenticated::Strategies::Basic::OpenID,
188
+ ]
189
+ before :machine_only, :only => [:create]
190
+
191
+ def index
192
+ display @stuff
193
+ end
194
+
195
+ def create
196
+ stuff = Stuff.create(params[:stuff])
197
+ display stuff
198
+ end
199
+
200
+ private
201
+ def machine_only
202
+ ensure_authentiated Merb::Authenticated::Strategies::Basic::OAuth, Merb::Authenticated::Strategies::Basic::BasicAuth
203
+ end
204
+ end
205
+ </code></pre>
206
+
207
+ You can see in this example that you can specify a list of strategies to use.
208
+ These will be executed in the order of the array passed in, with the default order
209
+ ignored completely.
210
+
211
+ h3. Where should Strategies be defined?
212
+
213
+ You should store your strategies in
214
+ <pre><code>
215
+ lib
216
+ `-- authentication
217
+ |-- setup.rb
218
+ `-- strategies.rb
219
+ </code></pre>
220
+
221
+ This is a good place to put everything together so you can see what you're doing at a glance.
222
+
223
+ h3. What Strategies are there?
224
+
225
+ See merb-auth-more
226
+
227
+ h3. Storing you user object into the session
228
+
229
+ You need to tell MerbAuth how to serialize your object into
230
+ and out of the session. If possible try not to store large or complex
231
+ data items in the session.
232
+
233
+ To configure your user object to go in and out of the session, here's how you
234
+ could do it.
235
+
236
+ <pre><code>
237
+ class Merb::Authentication
238
+
239
+ # return the value you want stored in the session
240
+ def store_user(user)
241
+ return nil unless user
242
+ user.id
243
+ end
244
+
245
+ # session info is the data you stored in the session previously
246
+ def fetch_user(session_info)
247
+ User.get(session_info)
248
+ end
249
+ end
250
+ </code></pre>
251
+
252
+ h3. Registering Strategies
253
+
254
+ Intended for plugin developers as a way to make it easy to use
255
+ strategies there is the possibility to register a strategy without loading it.
256
+
257
+ <pre><code>
258
+ Authentication.register(:my_strategy, "/absolute/path/to/strategy.rb")
259
+ </code></pre>
260
+
261
+ This then allows developers to use
262
+
263
+ <pre><code>
264
+ Authentication.activate!(:my_strategy)
265
+ </pre></code>
266
+
267
+ h3. Providing feedback to users (Error Messages)
268
+
269
+ There's at least 4 ways to provide feedback to users for failed logins.
270
+
271
+ * Overwrite Merb::Authentication#error_message The return of this method is
272
+ the default message that is passed to the Unauthenticated exception. Overwrite
273
+ this to provide a very basic catch all message.
274
+ * Provide a default message when you declare your before filter.
275
+ <pre><code>
276
+ before :ensure_authenticated, :with => [Openid, :message => "Could not log you in with open ID"]
277
+ # OR
278
+ before :ensure_authentication, :with => {:message => "Sorry Buddy... You Lose"}
279
+ </code></pre>
280
+ When you pass a message, it will replace the Merb::Authentication#error_message default for this
281
+ action
282
+ * Use an after filter for your login action. This can be used to set your messaging system. For example:
283
+ <pre><code>
284
+ after :set_login_message, :only => [:create]
285
+
286
+ private
287
+ def set_login_message
288
+ if session.authenticated?
289
+ flash[:message] = "Welcome"
290
+ else
291
+ flash[:error] = "Bad.. You Fail"
292
+ end
293
+ end
294
+ </code></pre>
295
+ * Use the authentications error messaging inside your strategies to set error messages there.
296
+ You can add to these errors just like adding to DataMappers validation errors.
297
+
298
+ <pre><code>
299
+ session.authentication.errors.add("Label", "You Fail")
300
+ </code></pre>
301
+ Add as many as you like, ask @session.authentication.errors.on(:label)@ to get specific errors etc
302
+ Really... They're just like the DataMapper validation errors. The bonus of using this system
303
+ is that you can add messages inside your Strategies, and then in your views you can do this:
304
+ <pre><code>
305
+ <%= error_messages_for sessions.authentication %>
306
+ </pre></code>
307
+
308
+
309
+ h3. Additional checks / actions to perform after the user is found
310
+
311
+ Sometimes you may need to perform additional operations on the user object
312
+ before or after you grab it out of the database when authenticating it. The
313
+ Merb::Authentication class implements Extlib::Hook so you can just setup hooks to
314
+ deal with this.
315
+
316
+ Here's an example of checking that a user object is active after it's been
317
+ found:
318
+
319
+ after :authenticate! do |instance, *args|
320
+ raise Merb::Controller::Unauthenticated, "User Not Active" unless instance.user.active?
321
+ end
322
+
323
+ bq. Notice that to fail the check we raised an Unauthenticated exception. The
324
+ session is available in that block as <code>session</code>
325
+
326
+ Really that's all there is to it. By default this plugin doesn't actually
327
+ authenticate anything ;) It's up to you to get your model going, and add an
328
+ authentication strategy. Just remember that to login, you just use
329
+ @session.authenticate(request)@ inside a controller. To logout use
330
+ @session.abandon!@ and to force a login at any time use
331
+ @raise Unauthenticated, "You Aren't Cool Enough"@
332
+ Be aware that strategies may throw :halt for use as a before filter...
333
+
334
+ h3. Contributors
335
+
336
+ # Adam French - "http://adam.speaksoutofturn.com/":http://adam.speaksoutofturn.com/
337
+ # Daniel Neighman - "http://merbunity.com":http://merbunity.com
338
+ # Ben Burket - "http://benburkert.com/":http://benburkert.com/
data/Rakefile ADDED
@@ -0,0 +1,65 @@
1
+ require 'rubygems'
2
+ require 'rake/gempackagetask'
3
+ require 'rubygems/specification'
4
+ require 'date'
5
+ require "spec/rake/spectask"
6
+ require File.join(File.dirname(__FILE__), "../../merb-core/lib/merb-core/version")
7
+ require File.join(File.dirname(__FILE__), "../../merb-core/lib/merb-core/tasks/merb_rake_helper")
8
+ require 'rake/testtask'
9
+
10
+
11
+ GEM_NAME = "merb-auth-core"
12
+ GEM_VERSION = Merb::VERSION
13
+ AUTHOR = "Adam French, Daniel Neighman"
14
+ EMAIL = "has.sox@gmail.com"
15
+ HOMEPAGE = "http://merbivore.com/"
16
+ SUMMARY = "An Authentication framework for Merb"
17
+
18
+ spec = Gem::Specification.new do |s|
19
+ s.rubyforge_project = 'merb'
20
+ s.name = GEM_NAME
21
+ s.version = GEM_VERSION
22
+ s.platform = Gem::Platform::RUBY
23
+ s.has_rdoc = true
24
+ s.extra_rdoc_files = ["README.textile", "LICENSE", "TODO"]
25
+ s.summary = SUMMARY
26
+ s.description = s.summary
27
+ s.author = AUTHOR
28
+ s.email = EMAIL
29
+ s.homepage = HOMEPAGE
30
+ s.add_dependency('merb-core', '~>0.9.9')
31
+ s.add_dependency('extlib')
32
+ s.require_path = 'lib'
33
+ s.files = %w(LICENSE README.textile Rakefile TODO) + Dir.glob("{lib,spec}/**/*")
34
+
35
+ end
36
+
37
+ Rake::GemPackageTask.new(spec) do |pkg|
38
+ pkg.gem_spec = spec
39
+ end
40
+
41
+ desc "install the plugin as a gem"
42
+ task :install do
43
+ Merb::RakeHelper.install(GEM_NAME, :version => GEM_VERSION)
44
+ end
45
+
46
+ desc "Uninstall the gem"
47
+ task :uninstall do
48
+ Merb::RakeHelper.uninstall(GEM_NAME, :version => GEM_VERSION)
49
+ end
50
+
51
+ desc "Create a gemspec file"
52
+ task :gemspec do
53
+ File.open("#{GEM_NAME}.gemspec", "w") do |file|
54
+ file.puts spec.to_ruby
55
+ end
56
+ end
57
+
58
+ desc "Run all specs"
59
+ Spec::Rake::SpecTask.new("specs") do |t|
60
+ t.spec_opts = ["--format", "specdoc", "--colour"]
61
+ t.spec_files = Dir["spec/**/*_spec.rb"].sort
62
+ t.rcov = false
63
+ t.rcov_opts << '--sort' << 'coverage' << '--sort-reverse'
64
+ t.rcov_opts << '--only-uncovered'
65
+ end
data/TODO ADDED
File without changes
@@ -0,0 +1,42 @@
1
+ module Merb
2
+ class Controller::Unauthenticated < ControllerExceptions::Unauthorized; end
3
+
4
+ module AuthenticatedHelper
5
+ protected
6
+ # This is the main method to use as a before filter. You can call it with options
7
+ # and strategies to use. It will check if a user is logged in, and failing that
8
+ # will run through either specified.
9
+ #
10
+ # @params all are optional. A list of strategies, optionally followed by a
11
+ # options hash.
12
+ #
13
+ # If used with no options, or only the hash, the default strategies will be used
14
+ # see Authentictaion.default_strategy_order.
15
+ #
16
+ # If a list of strategies is passed in, the default strategies are ignored, and
17
+ # the passed in strategies are used in order until either one is found, or all fail.
18
+ #
19
+ # A failed login will result in an Unauthenticated exception being raised.
20
+ #
21
+ # Use the :message key in the options hash to pass in a failure message to the
22
+ # exception.
23
+ #
24
+ # === Example
25
+ #
26
+ # class MyController < Application
27
+ # before :ensure_authenticated, :with => [OpenID,FormPassword, :message => "Failz!"]
28
+ # #... <snip>
29
+ # end
30
+ #
31
+ def ensure_authenticated(*strategies)
32
+ session.authenticate!(request, params, *strategies) unless session.user
33
+ auth = session.authentication
34
+ if auth.halted?
35
+ self.headers.merge!(auth.headers)
36
+ self.status = auth.status
37
+ throw :halt, auth.body
38
+ end
39
+ session.user
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,130 @@
1
+ module Merb
2
+ class Authentication
3
+ include Extlib::Hook
4
+ attr_accessor :session
5
+ attr_writer :error_message
6
+
7
+ class DuplicateStrategy < Exception; end
8
+ class MissingStrategy < Exception; end
9
+ class NotImplemented < Exception; end
10
+
11
+ # This method returns the default user class to use throughout the
12
+ # merb-auth authentication framework. Merb::Authentication.user_class can
13
+ # be used by other plugins, and by default by strategies.
14
+ #
15
+ # By Default it is set to User class. If you need a different class
16
+ # The intention is that you overwrite this method
17
+ #
18
+ # @return <User Class Object>
19
+ #
20
+ # @api overwritable
21
+ cattr_accessor :user_class
22
+
23
+ def initialize(session)
24
+ @session = session
25
+ end
26
+
27
+ # Returns true if there is an authenticated user attached to this session
28
+ #
29
+ # @return <TrueClass|FalseClass>
30
+ #
31
+ def authenticated?
32
+ !!user
33
+ end
34
+
35
+ # This method will retrieve the user object stored in the session or nil if there
36
+ # is no user logged in.
37
+ #
38
+ # @return <User class>|NilClass
39
+ def user
40
+ return nil if !session[:user]
41
+ @user ||= fetch_user(session[:user])
42
+ end
43
+
44
+ # This method will store the user provided into the session
45
+ # and set the user as the currently logged in user
46
+ # @return <User Class>|NilClass
47
+ def user=(user)
48
+ session[:user] = nil && return if user.nil?
49
+ session[:user] = store_user(user)
50
+ @user = session[:user] ? user : session[:user]
51
+ end
52
+
53
+ # The workhorse of the framework. The authentiate! method is where
54
+ # the work is done. authenticate! will try each strategy in order
55
+ # either passed in, or in the default_strategy_order.
56
+ #
57
+ # If a strategy returns some kind of user object, this will be stored
58
+ # in the session, otherwise a Merb::Controller::Unauthenticated exception is raised
59
+ #
60
+ # @params Merb::Request, [List,Of,Strategies, optional_options_hash]
61
+ #
62
+ # Pass in a list of strategy objects to have this list take precedence over the normal defaults
63
+ #
64
+ # Use an options hash to provide an error message to be passed into the exception.
65
+ #
66
+ # @return user object of the verified user. An exception is raised if no user is found
67
+ #
68
+ def authenticate!(request, params, *rest)
69
+ opts = rest.last.kind_of?(Hash) ? rest.pop : {}
70
+ rest = rest.flatten
71
+ strategies = rest.empty? ? Merb::Authentication.default_strategy_order : rest
72
+
73
+ msg = opts[:message] || error_message
74
+ user = nil
75
+ # This one should find the first one that matches. It should not run antother
76
+ strategies.detect do |s|
77
+ unless s.abstract?
78
+ strategy = s.new(request, params)
79
+ user = strategy.run!
80
+ if strategy.halted?
81
+ self.headers = strategy.headers
82
+ self.status = strategy.status
83
+ self.body = strategy.body
84
+ halt!
85
+ return
86
+ end
87
+ user
88
+ end
89
+ end
90
+ raise Merb::Controller::Unauthenticated, msg unless user
91
+ self.user = user
92
+ end
93
+
94
+ # "Logs Out" a user from the session. Also clears out all session data
95
+ def abandon!
96
+ @user = nil
97
+ session.clear
98
+ end
99
+
100
+ # A simple error message mechanism to provide general information. For more specific information
101
+ #
102
+ # This message is the default message passed to the Merb::Controller::Unauthenticated exception
103
+ # during authentication.
104
+ #
105
+ # This is a very simple mechanism for error messages. For more detailed control see Authenticaiton#errors
106
+ #
107
+ # @api overwritable
108
+ def error_message
109
+ @error_message || "Could not log in"
110
+ end
111
+
112
+ # Tells the framework how to store your user object into the session so that it can be re-created
113
+ # on the next login.
114
+ # You must overwrite this method for use in your projects. Slices and plugins may set this.
115
+ #
116
+ # @api overwritable
117
+ def store_user(user)
118
+ raise NotImplemented
119
+ end
120
+
121
+ # Tells the framework how to reconstitute a user from the data stored by store_user.
122
+ #
123
+ # You must overwrite this method for user in your projects. Slices and plugins may set this.
124
+ #
125
+ # @api overwritable
126
+ def fetch_user(session_contents = session[:user])
127
+ raise NotImplemented
128
+ end
129
+ end # Merb::Authentication
130
+ end # Merb
@@ -0,0 +1,10 @@
1
+ # This is not intended to be modified. It is for use with
2
+ # Merb::Authentication.default_customizations
3
+ class Merb::BootLoader::MerbAuthBootLoader < Merb::BootLoader
4
+ before Merb::BootLoader::AfterAppLoads
5
+
6
+ def self.run
7
+ Merb::Authentication.default_customizations.each { |c| c.call }
8
+ end
9
+
10
+ end
@@ -0,0 +1,24 @@
1
+ module Merb
2
+ class Authentication
3
+
4
+ class << self
5
+
6
+ # Adds any customizations just before the after_app_loads boot loader
7
+ # so that plugins can offer default customization. This will still allow
8
+ # for a user to overwrite any customizations if required in the after_app_loads
9
+ # block
10
+ # @plugin
11
+ def customize_default(&block)
12
+ default_customizations << block
13
+ default_customizations
14
+ end
15
+
16
+ # Gets the list of declared customizations
17
+ def default_customizations
18
+ @custom_default_blocks ||= []
19
+ end
20
+
21
+ end
22
+
23
+ end # Merb::Authentication
24
+ end # Merb