codahale.com٭blog

This is my old blog. My current writing is here: codahale.com

Basic HTTP Authentication with Rails & simple_http_auth

The Problem: You’ve got a controller (or just a few actions) in your Rails app that you’d like to control access to, but don’t feel like dealing with some huge-ass plugin, generator, or engine (whatever the hell those are).

The Solution: simple_http_auth!

Install!

Got your baby wrapped up in a comfy blanket of Subversion?


./script/plugin install -x http://svn.codahale.com/simple_http_auth

Just wanna get yer authentication on?


./script/plugin install http://svn.codahale.com/simple_http_auth

Feeling all DIY-y?


cd my_rails_app/vendor/plugins
svn co http://svn.codahale.com/simple_http_auth

Configure!

Here’s the really choice bit: simple_http_auth is just that–simple. It doesn’t validate username or passwords for you, since that’s your job. It just provdes a nice, clean wrapper for HTTP Basic authentication.

Dig it:


class PenguinsController < ApplicationController

  requires_authentication :using => Proc.new{ |username, password| password == 'ponies!' },
                          :except => [:index],
                          :realm => 'Secret Magic Happy Cloud'

  def index
    # public things...
  end

  def secret_magic_happy_cloud
    # most secret things...
  end

end

Basically, you define an event handler (in this case, a Proc) which, given a username and password, returns true if the pair are valid, and false otherwise. Super cool feature: this event handler is executed within the instance of the controller, which means you get to access all the controller internals you’ve grown to love, like sessions! You can also specify a private or protected method of the controller by passing a Symbol:


  requires_authentication :using => :authenticate

This means that all the details of a user model (if you want one), how passwords are stored, etc., are all up to you. You get to choose the best way to build your app instead of trying to make do with someone else’s infrastructure.

Update: Logout!

You can now log users out:

class HappyMagicController < ApplicationController
  requires_authentication :using => :whatever,
                          :logout_on => :logout

  def logout
    # logout.rhtml will be displayed after the user logs out
  end
end

Woo!

More details can be found in the readme.

Have fun!

37 Responses to “Basic HTTP Authentication with Rails & simple_http_auth”

  1. Luke Melia Says:

    Nice work on this, Coda! I took a look at it for an opensource rails app I work on, Tracks. It didn’t quite fit our needs, but I borrowed some code from the plugin to integrate into Tracks’ authentication system. I hope you don’t mind!

    Cheers,
    Luke

  2. Coda Says:

    Luke,

    First, awesome!

    Second, I released it under the MIT license for a reason–you can hack it, slash it, remix it, or just plain use it, and all you have to do if you want to redistribute it is say “Some portions (c) Coda Hale 2006.” ;-)

    Finally, in what way didn’t it work for you?

  3. Luke Melia Says:

    Cool. I’ll add the Some portions (c) text.

    Tracks already has fairly typical session-based authentication system with a web form login. It works well for users. But I’m moving to make some enhancements to the Tracks API and I wanted to use our existing controllers together with responds_to to implement it. The controller actions are protected by a login_required before_filter. So I decided it would be most effective update our existing login system to check for HTTP_AUTH credentials when a request doesn’t come in with a session cookie. This provides a way for a script to send credentials with it’s request and interact with those protected controller actions.

    Tracks is GPL and there’s a public Trac (http://dev.rousette.org.uk/) and SVN repository if you feel like checking out what I came up with.

    Thanks again.

  4. Mars Says:

    This plug-in is quite a gem. It works beautifully!

    I wanted HTTP Auth for my app (a Photo Album/Organizer), because web services & syndication clients don’t necessarily work with session/cookie-based logins.

    Cheers!
    *Mars

  5. Scott Says:

    How do I make a logout button?
    It’s not clear from the code what’s keeping the user logged in.

  6. Coda Says:

    Scott, HTTP authentication is a bit different than session-based authentication, because HTTP doesn’t have any state. Once a request is over, the slate is wiped clean.

    I’ll walk you through a browsing session, step-by-step, behind the scenes:

    1. The user clicks a link labeled ‘Secret.’ (Ooo!)
    2. Her browser requests a URI, /secret, from the server.
    3. The server denies this request with a 401 Unauthorized error.
    4. The user’s browser pops up a password dialog, and the user enters her password.
    5. Her browser then re-requests the URI, passing along the username and password entered by the user.
    6. The server returns a picture of a pony with a puppy on its back, dressed like a cowboy.
    7. The user clicks on another link, ‘Secret 2.’
    8. Her browser sees that the URI belongs to the same server, and requests the URI, sending the cached username and password entered previously.
    9. The server returns a picture of David Hasslehoff in a thong, on a giant puppy’s back. The puppy is still dressed like a cowboy.

    Does this make any sense? (You may need to print it out and redact the bits about puppies and Hasselhoves with a Sharpie for it to sink in. ;-)

    As with anything HTTP, the state of your application (in this case, the “logged-in-ness” of the user) is either maintained on the client side (the browser) or the server side (your app). In this case, the browser is caching the username and password, and won’t re-display the login dialog until it gets another 401 Unauthorized error from the server in that realm.

    Which is precisely what you should do. Luckily, I am bored and in a programming mood, so I’ve cooked the whole thing up for you. Get the latest source from the repository and do this:

    class BlahController < ApplicationController
      requires_authentication :using => :authenticate,
        :realm => 'HappyAppy',
        :logout_on => :logout
    
      def logout
        # put yer logout message in logout.rhtml and it'll be displayed
      end
    
    end
    

    Lemme know how that works, Scott.

  7. Scott Says:

    I’m trying it now….

  8. Scott Says:

    It’s not doing what I would expect.

    My controller is:
    requires_authentication(:using => :authenticate,
    :realm =>’Journal’,
    :logout_on =>:logout)

    def authenticate(username, password)
    if session[:user].nil?
    session[:user] = User.login(username, password)
    end
    session[:user]
    end

    def logout
    session[:user] = nil
    render :inline=>”logged out from journal controller”
    end

    I also have:
    /view/journal/logout.rhtml >> logged out from rhtml

    ApplicationController
    def logout
    render inline=>’logged out from application controller’
    end

    Here’s what’s happening (though I don’t expect you to troubleshoot for me)
    1. goto /journal/index
    prompt comes up
    login as invalid user
    * prompt comes up again as expected
    login as valid user
    * journal entries appear as expected

    2. goto /journal/logout
    prompt comes up
    if I login as valid or invalid
    * prompt continues to appear
    * this isn’t expected, though it’s not so bad
    sometimes I get the logged out message from application controller
    sometimes I get logged out message from rhtml
    I never get logged out message from journal controller

    3. goto /journal/index
    no prompt - journal entries appear
    * this is not expected
    unless I received logged out message from application controller,
    then it forces me to login again, as expected.

    I changed the authenticate method to be the following:

    def authenticate(username, password)
    # removed nil check
    session[:user] = User.login(username, password)
    end

    But that didn’t seem to help.

    I’ll probably play with it more tomorrow.

    Thanks for making the change.

    btw: I agree with your rant on how rails needs an authentication scheme.

  9. Scott Says:

    yuk, I didn’t keep my formatting, oh well.

  10. Scott Says:

    too bad I can’t edit my previous comments, oh well.
    I’ll try to create a new test case for you in your simple_http_auth_test sometime tomorrow, too.

  11. Coda Says:

    Well, I think this is as sophisticated as it’ll get, Scott. There’s no real way to log out using Basic HTTP Authentication–it’s official–and the functionality I just added is super hacky. I wish I had better news for you.

    Also, you are weirding things up by making logout render in three different places. The limitation of the code is that it will render the rhtml file associated with the logout action, and I guess bug out when the logout action tries to render some text. This is due to the way render_to_string is implemented, and it’s not something I really want to get into.

    Basically, Scott, I can’t help you any more. The protocol doesn’t support what you want, the hack I came up with (which really is the only one on record) doesn’t seem to meet your needs… I think the solution to your problem is outside the boundaries of this plugin.

    Bottom line: you may just need to tell your users to close their browsers to log out, or use an authentication scheme based on a protocol which maintains state (i.e., over HTTP, instead of through it).

    Sorry.

  12. 虚拟主机 Says:

    Thanks for the information. This is very useful

  13. Derek Says:

    the Model Security generator uses HTTP authentication too, just FYI. You may want to look it over if you haven’t already.

    He handles the logout by passing a new login window. He says it’s the only way to get the browser to stop sending auth data with each request. I haven’t tried your plugin yet so maybe yours does the same..

  14. Gacha Says:

    This plugin is doing his job well, but I found one imperfection (to me). I created:

    def logout
    redirect_to '/'
    end

    but it doesn’t work for me, it says that I need a logout.rhtml template. Maybe it would be greate if we could redirect to some page on logout or simply render :text => 'Goodbye!'.

    Thanks.

  15. Immad Says:

    Firstly, I love it, worked straight away and really simple.

    Secondly, I needed to just protect everything indiscriminantly so I put requires_authentication in the application controller which works fine. Just in case thats not obvious :-).

    Also I had to restart the webrick server after pulling in the pugin although I guess there might be a better way then restarting.

  16. Adam Block Says:

    I did the same thing that #15 did: put requires_authentication in application.rb. However, I want to exempt one page (a login/info page). I tried saying:


    requires_authentication :using => lambda{ |username,password| ... },
    :except => ["main/beta"]

    …but that didn’t work. Is there a way to exempt an action that’s not in the same controller as the requires_authentication method?

    Thanks!

  17. devjax Says:

    really cool!

  18. Dan Curran Says:

    @Adom Block:

    A quick way to get something similar to what you are looking for is to change line 47 of simple_http_auth.rb to have at the end

    || !@except_actions.include?({:controller=>controller.controller_name,:action=>controller.action_name}

    This will allow you to format you use:

    require_authentication :using=>…
    :except => [{:controller=>'main',:action=>'beta'}]

    This is the format I like to use. You can modify line 47 to fit whatever schema you need.

  19. links for 2007-01-29 - DuaneFields.com - http://www.duanefields.com Says:

    [...] Basic HTTP Authentication with Rails & simple_http_auth | Archives | codablog | Coda Hale ./script/plugin install http://svn.codahale.com/simple_http_auth (tags: rails authentication) [...]

  20. Niko Says:

    Nice PlugIn.

    I have i problem with Safari, though. When using rails 1.2 and a restful controller it stops processing when calling edit:

    http://pastie.caboo.se/41894

    The only thing that helps is excluding the “edit” action from auth. Users are then able to see the edit-view, but not to save (as the “update” action isn’t excluded).

    Firefox & IE6 & IE7 don’t have this problem.

  21. Michael Says:

    Hi,

    I really like your plugin and want to use it in a web service I am developing with rails (its perfect for this). I am having a problem getting this code to work with lighttpd + mod_proxy_core sending request to mongrel / webrick. I want to use lighttpd so that username and password is encrypted over SSL. I just started working with this stack and don’t know where to begin debugging. It seems like mod_proxy_core is discarding the ‘WWW-Authenticate’ header before sending the request to mongrel. You might be saying to yourself why not let lighttpd handle this. The reason is of course is that I would like this information to be authenticated against /etc/passwd using PAM– something which lighttpd does not offer to my knowledge. Thanks in advance for reading this.

  22. Silvestre Says:

    I justed wanted to say thanks! Awesome plugin, small and sweet.

  23. Dagy Says:

    Hi,
    I just want to ask for integration with apache basic auth. U will probably have more experience to tell me if it’s possible to somehow use same authentication (file) as apache uses.

    Thanks, Dagy

  24. flightsnormal Says:

    I have found and fix some error in plugin (”only & except” dont working on my localhost and was impossible to login from logout page) if you interested mail me.
    And thanks for plugin!

  25. Andrew Janssen Says:

    Hey Coda. Thanks always for your work. We owe you one.

    A small suggestion– I’m writitng code to test that I’m calling your plugin correctly. It would be great to expose the functionality of the private login method of simple_auth_test.rb (line 268) to the outside Rails app– the app needs to test its security just like the simple_http_auth’s mock controller does in the test cases. The demi gods of DRY would smile upon us if we didn’t duplicate the inside of login in our Rails apps.

    Thanks again.

  26. George S Says:

    Hi,

    I’m getting an error on line 68 of simple_http_auth.rb unless I override the render method as public in my controller. It says ApplicationController::render is protected. Any thoughts on this?

  27. 虚拟主机 Says:

    This is very useful

  28. Toby Hede Says:

    I just installed the plugin on a CPanel VPS. I can confirm that you need to use the .htaccess snippet in the readme to get authentication working:

    RewriteRule ^(.*)$ dispatch.fcgi [E=X-HTTP_AUTHORIZATION:%{HTTP:Authorization},QSA,L]

  29. Toby Hede Says:

    Oh, and awesome work, btw.

    :)

  30. David Lowenfels Says:

    Great plugin! I spent an hour looking for how to log out with some hack, until I realized it was built in!
    Then I ran into a problem, since I am calling requires_authentication in an admin namespaced application_controller.rb that I inherit from for all my admin pages.
    Logout didn’t seem to work, as this plugin uses a specific :logout_on action instead of a controller name and action.
    What I did to make it work was copy and paste (boo, not DRY) the requires_authentication statement into every admin controller. And then on one of them I put the logout action and the :logout_on parameter. Not a great solution.

    I tried to hack on the plugin code myself to make it work but to no avail.

  31. David Lowenfels Says:

    ahh okay, I understand it now. The logout action actually does work with application_controller inheritance. The confusing part is that the authentication window pops up again… you are supposed to click cancel to make it actually log out. Another funny thing is that it’s not following my directives to render with no layout.


    def admin_logout
    render :action => 'admin_logout', :layout => false
    end

  32. Max Lapshin Says:

    Hi, You should change controller.render to controller.send(:render,…
    for your plugin to work with Edge Rails

  33. Coda Says:

    Max– Edge already has basic HTTP authentication baked in. This plugin won’t be compatible with anything beyond 1.2.

  34. Sridhar Ratnakumar Says:

    I am yet to look at Rails-2.0’s HTTP auth API. Meanwhile here is the patch to make it work with the 2.0 pre-release.

    Index: vendor/plugins/simple_http_auth/lib/simple_http_auth.rb
    ===================================================================
    — vendor/plugins/simple_http_auth/lib/simple_http_auth.rb (revision 214)
    +++ vendor/plugins/simple_http_auth/lib/simple_http_auth.rb (revision 215)
    @@ -42,7 +42,7 @@
    if controller.action_name.intern == @logout_action
    controller.response.headers["Status"] = “Unauthorized”
    controller.response.headers["WWW-Authenticate"] = “Basic realm=\”#{@realm}\”"
    - controller.render :action => @logout_action.to_s, :status => 401
    + controller.send :render, :action => @logout_action.to_s, :status => 401
    return false
    elsif (@only_actions.include?(controller.action_name.intern) || @only_actions.empty?) && !@except_actions.include?(controller.action_name.intern)
    username, password = get_auth_data(controller)
    @@ -65,7 +65,7 @@
    unless authenticated
    controller.response.headers["Status"] = “Unauthorized”
    controller.response.headers["WWW-Authenticate"] = “Basic realm=\”#{@realm}\”"
    - controller.render :text => @error_msg, :status => 401
    + controller.send :render, :text => @error_msg, :status => 401
    end
    return authenticated
    end

  35. Chris Says:

    Hi Coda,

    Your plugin works great. I included it into my project and switched off the Apache authentication-stylee. I use it for HTTP-authentication against LDAP with TLS. so far so fine.
    Now i need to implement a webservice-client. both sides (webservice-server and webservice-client) use HTTP-authentication.
    for the webservice is use WSS4R-plugin (for secure connection).
    But of course the webservice now expects an authenticated user… I suppose that’s my problem right now. I have to ensure that the webservice-client gets an authenticated user (who btw. is authenticated through webservice-client yet).
    Do you have an idea how to solve it?

    Every hint would be helpful.

    Best regards
    Chris

  36. Andrew Janssen Says:

    @Sridhar Ratnakumar: Thanks for your patch! You rock

  37. Blog of BigSmoke » Native PostgreSQL authentication in Rails with rails-psql-auth Says:

    [...] using a HTTP Basic authentication challenge. (The code for this is adapted from Coda Hale’s Basic HTTP authentication plugin.) It’s possible to specify a guest_username in the database.yml which will be used as a [...]