merb-auth-core

MerbAuth is an authentication framework for use with the Merb web framework.

MerbAuth does not try to dictate what you should use as a user model, or how it should authenticate. Instead it focuses on the logic required to check that an object passes authentication, and store the keys of authenticated objects in the session. This is in fact the guiding principle of MerbAuth. The Session is used as the place for authentication, with a sprinkling of controller helpers. You can choose to protect a controller action, or a route / group of routes. This makes sense to talk about an authenticated session. For example, inside your controller:

  • session.authenticated? returns true if the session has been authenticated. False otherwise # session.authenticate(controller) authenticates the session based on customizable user defined rules
  • session.user returns the currently authenticated user object
  • session.user= manually sets the currently authenticated user object
  • session.abandon! sets the session to unauthenticated, and clears all session data
  • session.authenticate! authenticates the session against the active strategies

MerbAuth makes use of Merb’s exception handling facilities which return correct HTTP status codes when a 200 OK would be inappropriate. To fail a login, or to force a login at any point in your controller code, simply raise an Unauthenticated exception, with an optional message and the user will be presented with login page. The login page is in fact the html view for Extensions#unauthenticated

To protect your controllers, add a simple before filter to your controller.

before :ensure_authenticated

It is possible to use MerbAuth with any object as a user object, provided that object does not evaluate to false and it can be serialized in an out of the session. For this reason, merb-auth-core does not try to implement even a simple login form for you, since it may not meet your requirements.

How Does It Authenticate my arbitrary user?

This is very similar to the BootLoader process in Merbs initialization. You declare a class that inherits from Merb::Authentication::Strategy and define an instance method run!


  class PasswordStrategy &lt Merb::Authentication::Strategy
  
    def run!
      if params[:login] && params[:password]
        user_class.authenticate(params[:login], params[:password])
      end
    end
  end
  

This login strategy uses the authenticate method on the User class to retrieve a user by login and password. Remember, you can put as much logic here as you require. The strategy uses an instance variable so the full power of classes are available to your strategy.

The strategy provides access to the current request giving you access to the params hash, session etc.

To pass authentication, simply return a non-nil non-false value from the run! method. Any false or nil value will cause that strategy to fail. Then the next strategy will be tried.

You can add as many strategies as you like and they will be tried one after another until either one is found that works (login), or none of them have passed (failed attempt).


    class PasswordLoginBasicAuth &lt Merb::Authentication::Strategy
      def run!
        if params[:api_key] && params[:api_token]
          Machine.api_authenticate(params[:api_key], params[:api_token])
        end
      end
    end

Now that we have two, they will be executed in the order that they were declared when we call session.authenticate!(self). The first one that returns a value that doesn’t evaluate to false, will be considered the winner.

Customizing the user_class

Notice the user_class method in the above strategy examples. This is a convenience method on a strategy to provide you with the user_class to use for this strategy. You can overwrite this method on a per strategy basis to use different user model types. You do not have to use this method and it’s only there to keep track of the “default” user class. (if any)

By default the strategy#user_class method will defer to Merb::Authentication#user_class. You can set which is the “default class” that Merb::Authentication will use in the provided strategies by setting it in Merb.root/merb/merb-auth/setup.rb


    
    Merb::Authentication.user_class = Person

This will cascade throughout the default strategies, and your own strategies using the user class that you defined. In this case Person.

There is no default class set for Merb::Authentication.user_class by default

Strategies and Inheritance

Strategies may be inherited multiple times to make the job of combining similar aspects easier. You can inherit as many levels as you like and at any point you may mark a strategy as abstract

An abstract strategy just means that it will not be run when it comes time to authenticate. Instead it’s good to put common logic in and then inherit from it to keep your strategies DRY.

To mark a class as abstract, use the abstract! class method.


  class AbstractStrategy < Merb::Authentication::Strategy
    abstract!
  end

At any point you can activate a registered strategy. You don’t need to register your strategies, you just declare them, but plugin developers make life easier when they do.

To activate a registered strategy:


  Merb::Authentication.activate!(:defualt_password_form) 

You can easily mix this in with your own strategies. In you Merb.root/merb-auth/strategies.rb


  class MyStrategy < Merb::Authentication::Strategy
    def run!
      #...
    end
  end</p>
Merb::Authentication.activate!(:default_openid)

class AnotherStrategy < Merb::Authentication::Strategy
def run!
#…
end
end
<p>

This will collect them in order of declaration. i.e.: MyStrategy, Merb::Authentication::Strategies::Basic::OpenID, AnotherStrategy

Customizing the order of the strategies

By default, strategies are run in the order they are declared. It’s possible to customize the order that the strategies are called.

Merb::Authentication.default_strategy_order will return an array of the strategy classes in the order that they will be run. You can customize this by setting the default_strategy_order array manually.

Authenticateion.default_strategy_order.order = [Second, First, Fourth]

It’s possible to leave some out, and re-order existing ones. It will error out if you specify one that doesn’t exist though.

Authentication based on Routes

With MerbAuth you can protect routes rather than individual actions on a controller. The benefit with doing this is that the request is stopped earlier in the request process. This is a more efficient method of protection than controller based protection.

The downside is that a controllers action is not guaranteed to be protected. For example


  
  authenticate do 
    match("/one").to(:controller => "one", :action => "index")
  end
  
  match("/two").to(:controller => "one", :action => "index")
  

The /one route is protected, but you can see that both of these routes point to the same controller action. If the One#index method is accessed through the /two route, there is no protection. You can specify which strategies to use as arguments to the authenticate method. The default strategies are used if there is no argument given.

Specifying selected strategies per action

If you need to protect an action regardless of which route leads to it, you can use controller level protection. Use a before :ensure_authenticated filter to protect actions in your controller whenever the are accessed.

It’s possible to configure each call to ensure_authenticated with a custom list of strategies to run. These will be run in order and should have an instance method of #run!


  class ApiMethods < Application
    before :ensure_authenticated, :with => [
                                            Merb::Authenticated::Strategies::Basic::Form,
                                            Merb::Authenticated::Strategies::Basic::BasicAuth, 
                                            Merb::Authenticated::Strategies::Basic::OpenID, 
                                           ],
                                  :only => [:index]
    before :machine_only, :only => [:create]
    
    def index
      display @stuff
    end

    def create
      stuff = Stuff.create(params[:stuff])
      display stuff
    end
    
    private
    def machine_only
      ensure_authentiated Merb::Authenticated::Strategies::Basic::OAuth, Merb::Authenticated::Strategies::Basic::BasicAuth
    end
  end

You can see in this example that you can specify a list of strategies to use. These will be executed in the order of the array passed in, with the default order ignored completely.

Where should Strategies be defined?

You should store your strategies in


  merb
  `-- merb-auth
      |-- setup.rb
      `-- strategies.rb

This is a good place to put everything together so you can see what you’re doing at a glance. It is also auto included by merb-auth-core when you’re using it.

What Strategies are there?

See merb-auth-more

Storing you user object into the session

You need to tell MerbAuth how to serialize your object into and out of the session. If possible try not to store large or complex data items in the session but just store the objects key.

To configure your user object to go in and out of the session, here’s how you could do it.


    class Merb::Authentication

      # return the value you want stored in the session 
      def store_user(user)
        return nil unless user 
        user.id
      end

      # session info is the data you stored in the session previously 
      def fetch_user(session_info)
        User.get(session_info)
      end
    end

Registering Strategies

Intended for plugin developers as a way to make it easy to use strategies there is the possibility to register a strategy without loading it.


  Authentication.register(:my_strategy, "/absolute/path/to/strategy.rb")

This then allows developers to use


  Authentication.activate!(:my_strategy)

Providing feedback to users (Error Messages)

There’s at least 4 ways to provide feedback to users for failed logins.

  • Overwrite Merb::Authentication#error_message The return of this method is the default message that is passed to the Unauthenticated exception. Overwrite this to provide a very basic catch all message.
  • Provide a default message when you declare your before filter.
    
        before :ensure_authenticated, :with => [Openid, :message => "Could not log you in with open ID"]</li>
    	<li>OR
        before :ensure_authentication, :with => {:message => “Sorry Buddy… You Lose”}
      
    When you pass a message, it will replace the Merb::Authentication#error_message default for this action
  • Use an after filter for your login action. This can be used to set your messaging system. For example:
    
        after :set_login_message, :only => [:create]</li>
    </ul>
    private
    def set_login_message
    if session.authenticated?
    flash[:message] = “Welcome”
    else
    flash[:error] = “Bad.. You Fail”
    end
    end
    
    • Use the authentications error messaging inside your strategies to set error messages there. You can add to these errors just like adding to DataMappers validation errors.
    
    session.authentication.errors.add(“Label”, “You Fail”)
    
    Add as many as you like, ask session.authentication.errors.on(:label) to get specific errors etc Really… They’re just like the DataMapper validation errors. The bonus of using this system is that you can add messages inside your Strategies, and then in your views you can do this:
    
    <%= error_messages_for sessions.authentication %>
    

    Additional checks / actions to perform after the user is found

    Sometimes you may need to perform additional operations on the user object after you have found a valid user in the strategy. There is a hook method Merb::Authentication.after_authentication which is designed for this.

    Here’s an example of checking that a user object is active after it’s been found:

    Merb::Authentication.after_authentication do |user, request, params| user.active? ? user : nil end

    Pass the user model on if everything is still ok. Return nil if you decide in the after_authentication hook that the user should in fact not be allowed to be authenticated.

    By default this plugin doesn’t actually authenticate anything ;) It’s up to you to get your model going, and add an authentication strategy.

    To logout use session.abandon! and to force a login at any time use raise Unauthenticated, "You Aren't Cool Enough"

    Contributors

    1. Adam French – http://adam.speaksoutofturn.com/
    2. Daniel Neighman – http://merbunity.com
    3. Ben Burket – http://benburkert.com/