Recently I worked on a project that required a single login to access administration options. There was no need for a full-blown RESTful authentication solution – I was advised to “Just use basic auth!”
Rails makes it easy. You probably know the standard example. You put a before_filter :authenticate in the controllers that require it, and set it up in the Application Controller.
def authenticate
authenticate_or_request_with_http_basic do |user, password|
user == 'admin' && password == 'pass'
end
end
It’s all well and good … until you want to add a log out button. The browser stores the successful login credentials in a sort of cookie, and applies them to every page which requests basic authentication. Once you’ve logged in, it’s actually quite hard to make the browser forget you until you quit and restart the browser. It’s hard, but not impossible. If you can force basic authentication to fail, the browser will throw away the credentials.
So the solution is to add a session variable that says “NO SRSLY, LOG ME OUT PLS!” This is the logout action (a destroy method in a Sessions Controller)
def destroy
session[:logout_requested] = true
flash[:notice] = "You have logged out successfully"
redirect_to(root_path)
end
Now for the tricky bit. The way this works is subtle and takes a moment to figure out each time I think about it. We change the authenticate method in the Application Controller so that as well as checking the username and password, it also ensures that this flag has not been set. Meaning we can cause basic authentication to fail when we want it to.
def authenticate
authenticate_or_request_with_http_basic do |user, password|
user == 'admin' && password == 'pass' && session[:logout_requested] != true
end
session[:logout_requested] = nil
end
Next time our user goes to a page which requires authentication, the browser still provides the correct username and password, but the flag causes the basic authentication to fail. Obviously we then have to clear the flag straight away, otherwise the user would not be able to get back in again even with the correct credentials. The user must type in the correct login name and password again to be able to get back in.
Perhaps we want to know whether the user is logged in or not, so that we know whether to display an edit button. We can set another session variable. Conveniently, authenticate_or_request_with_http_basic returns a boolean value.
def authenticate
session[:logged_in] = authenticate_or_request_with_http_basic do |user, password|
user == 'admin' && password == 'pass' && session[:logout_requested] != true
end
session[:logout_requested] = nil
end
Remember to set the flag to false when you log out. Also remember that this flag could be true, false or nil so a check in the Application Controller looks like this:
def logged_in?
session[:logged_in] == true
end
Finally it’s worth noting that the username and password do not have to be hard-coded like this. It’s simple for an example, but don’t think that’s all there is to basic authentication. There’s nothing to stop you comparing against values in a settings table or even doing a user lookup à la RESTful authentication.
def authenticate
session[:logged_in] = authenticate_or_request_with_http_basic do |email, password|
user = User.authenticate(email, password)
if user && session[:logout_requested] != true
self.current_user = user
true
else
self.current_user = nil
false
end
end
session[:logout_requested] = nil
end
Thanks to Richard and Tris for their help in figuring out the not-so-basic aspects of basic authentication! :)
