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!
June 4th, 2006 at 12:12am
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
June 4th, 2006 at 10:17am
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?
June 6th, 2006 at 9:19am
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.
June 7th, 2006 at 8:59pm
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
June 16th, 2006 at 5:43pm
How do I make a logout button?
It’s not clear from the code what’s keeping the user logged in.
June 16th, 2006 at 7:38pm
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:
/secret, from the server.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 endLemme know how that works, Scott.
June 16th, 2006 at 9:26pm
I’m trying it now….
June 16th, 2006 at 11:28pm
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.
June 16th, 2006 at 11:30pm
yuk, I didn’t keep my formatting, oh well.
June 16th, 2006 at 11:40pm
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.
June 16th, 2006 at 11:56pm
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.
June 29th, 2006 at 12:56am
Thanks for the information. This is very useful
August 10th, 2006 at 12:55pm
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..
October 28th, 2006 at 3:18am
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.
November 30th, 2006 at 11:47am
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.
December 26th, 2006 at 12:44am
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!
January 17th, 2007 at 1:37am
really cool!
January 24th, 2007 at 2:43pm
@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.
February 3rd, 2007 at 8:04am
[...] 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) [...]
February 21st, 2007 at 4:11am
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.
April 5th, 2007 at 3:15pm
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.
April 14th, 2007 at 8:38am
I justed wanted to say thanks! Awesome plugin, small and sweet.
May 17th, 2007 at 1:57am
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
May 30th, 2007 at 11:33pm
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!
June 29th, 2007 at 12:46pm
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
loginmethod ofsimple_auth_test.rb(line 268) to the outside Rails app– the app needs to test its security just like thesimple_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 ofloginin our Rails apps.Thanks again.
July 18th, 2007 at 4:47pm
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?
July 18th, 2007 at 11:47pm
This is very useful
July 26th, 2007 at 7:54pm
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]
July 26th, 2007 at 7:54pm
Oh, and awesome work, btw.
:)
August 2nd, 2007 at 5:21pm
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.
August 2nd, 2007 at 5:35pm
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
September 12th, 2007 at 1:22am
Hi, You should change controller.render to controller.send(:render,…
for your plugin to work with Edge Rails
September 12th, 2007 at 1:31am
Max– Edge already has basic HTTP authentication baked in. This plugin won’t be compatible with anything beyond 1.2.
October 4th, 2007 at 4:46am
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
February 7th, 2008 at 12:03am
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
March 20th, 2008 at 2:56pm
@Sridhar Ratnakumar: Thanks for your patch! You rock
May 2nd, 2008 at 12:42pm
[...] 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 [...]