Test HTTP Basic Authentication in Rails 3 With Capybara

I’m using some HTTP basic authentication in Chirk HR as a simple way of preventing unauthorized access. It’s simple, fast, and easy to change to a more robust authentication later on.

Ideal Authentication Test

As part of my testing habits, I try to really exercise important methods. Authentication is definitely one of them. Authentication is also tricky because many of the authentication libraries give you helper methods that bypass some of the actual logging in process (e.g. open login page, enter username, enter password, submit). For this reason I try to test my authentication at the integration level:

  1. Load login form
  2. Enter authentication data and submit
  3. Verify login was successful

My integration tool of choice is capybara. Its DSL is high level enough for me to model how a visitor behaves without being too complex.

Testing HTTP Basic Authentication

One feature of capybara is that it supports different drivers. A driver is a way to actually request and parse the HTTP request/response. It’s best to think of them as separate browsers. There are text-only ones, ones that embed an entire WebKit browser with JavaScript support, and ones that drive an actual FireFox process inside the GUI.

The problem with supporting different drivers is that the capybara can only abstract so much, as each browser supports different features such as HTTP basic. Fortunately, with Ruby it is easy to inspect each driver’s methods and call its version of HTTP basic as needed. By abstracting these into a method, our tests now have a clean way to use HTTP basic with capybara.

require 'test_helper'
 
class Admin::CohortTest < ActionDispatch::IntegrationTest
  def basic_auth(name, password)
    if page.driver.respond_to?(:basic_auth)
      page.driver.basic_auth(name, password)
    elsif page.driver.respond_to?(:basic_authorize)
      page.driver.basic_authorize(name, password)
    elsif page.driver.respond_to?(:browser) && page.driver.browser.respond_to?(:basic_authorize)
      page.driver.browser.basic_authorize(name, password)
    else
      raise "I don't know how to log in!"
    end
  end
 
  test "should block access without invalid HTTP auth" do
    visit '/admin'
 
    assert_equal 401, page.status_code
  end
 
  test "should show the page" do
    basic_auth('edavis', 'password')
 
    visit '/admin'
 
    assert_equal 200, page.status_code
    assert has_content?("Cohorts")
  end
 
end

Looking back at the ideal authentication test we can see that this is very close to fulfilling the behavior we want.

  1. The basic_auth method enters the authentication data and submits.
  2. visit tries to open a HTTP basic protected page, which opens the login form.
  3. The final assert has_content?("Cohorts") verifies that the login was successful.

Because of how HTTP basic works the flow is a bit different in that we are setting the authentication first and then loading the page. It’s not ideal but for something quick I think it’s understandable enough.

Ideally, you’d extract the basic_auth method to the test helper so you can reuse it in other tests.

Gotchas

There are a few gotchas to be aware of when using and testing HTTP basic this way:

  • With HTTP Basic, authentication is passed in the clear to the server. Depending on your application this might be a problem. On Chirk HR I’m using SSL for everything so the HTTP Basic headers are encrypted there. For something stronger than HTTP Basic, try using HTTP Digest or a full authentication system.
  • The test is calling methods on the underlying driver which could break if you switch capybara drivers. You’ll want to standardize on one or two drivers for you application to keep things simple.
  • Also if the drivers change their HTTP basic APIs in the future then your tests might start failing.

Using and testing HTTP Basic authentication in Rails doesn’t need to be difficult. While it isn’t as fully featured as full authentication systems like devise or sorcery, HTTP Basic could be a good enough system for you to use in your application.

Thanks to the Stackoverflow post where I learned this.

2 comments

  1. Alex Dean says:

    I added this conditional to work with the capybara poltergeist (phantom.js) driver:

    elsif page.driver.respond_to?(:headers=) # poltergeist
    page.driver.headers = { “Authorization” => ActionController::HttpAuthentication::Basic.encode_credentials(name, password) }

Comments are closed.