Coding is like gardening...

Under the hood: Not-so-basic authentication

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! :)

6 Responses to “Under the hood: Not-so-basic authentication”

  1. Carey says:

    Fantastic! Solved an immediate problem for me.

    I wouldn't mind if the example was more pedantic, and more generic. I may post that on my blog.

  2. Aimee says:

    I'm glad it helped you out. Hopefully it shows that there is much more that can be done with basic authentication than people sometimes realise.

    Let me know if you post a follow-on. I'll be interested to read it.

  3. andrewskegg says:

    There must be some strange behaviour going on with Safari, since I can get Rails to logout but revisiting the protect page and it let's me right in.

    Still – the admin links are hidden, and the auth is destroyed when the browser quits, so it's not all bad.

    Of course, nothing beats proper authentication rather than digest.

  4. Hampers says:

    As a newbie to this designing and coding world, it's an awesome find to mingle with great minds here. May I take this opportunity to ask, is there a way to make things as simple as ABC especially if we are dealing with an error logging in and instead of going back from scratch, there should be a message of where the error lies so users will not go all over again: typing from the very beginning?

  5. I think that the solution works good (so long as you close your eyes when looking at the passwords stored in the PL/SQL). The need for an ORACLE wallet is a bit smelly but that seems to be just how it works. One thing I did miss about that is the wallet needs to be stored in a location the server

  6. 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.

    Thanks and Regards