Sandro Turriate

Coder, cook, explorer

How to generate a signed Rails session cookie

Bypass the sign-in process by sending a signed session cookie in the HTTP headers.

In my last article I wrote about testing the Twitter sign-in process by visiting Twitter, signing in to a real account, authorizing the application and returning to the callback url. We followed the exact path that the user follows to ensure that sign-in integrates flawlessly with Twitter. As we begin testing the rest of our application we'll need users to sign in but they don't need to follow the end-to-end sign-in path.

Most authentication systems provide a way of bypassing the sign-in process under the test environment but I had trouble coming up with a good solution for OmniAuth (pre 0.2.0). Passing an acceptable hash of attributes to the OmniAuth callback url was the initial idea but before leaving the middleware, OmniAuth makes a GET request to Twitter verifying account credentials. Sure, I could use EphemeralResponse to cache this request but with sign-in thoroughly tested elsewhere, the rest of my suite should sign in as simply as possible.

I can't easily go through OmniAuth without faking the Twitter interaction or monkeypatching the library so I decided to bypass OmniAuth altogether. When OmniAuth is successful, the user is signed in via their session. To mimic this behavior, all we have to do is to send a legitimately signed session cookie to the server. Unfortunately, generating a signed session cookie is not a straightforward process but after reading through a fair amount of source I wrote a class to generate them:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
class SessionCookieGenerator
  extend Forwardable
  def_delegators :session_hash, :[], :[]=

  attr_reader :session_hash, :session_key

  def initialize
    @session_key = Rails.configuration.session_options[:key]
    @session_hash  = {"session_id" => SecureRandom.hex(32)}
    @cookie_jar = ActionDispatch::Cookies::CookieJar.new(Rails.configuration.secret_token, Capybara.default_host)
  end

  def to_s
    "#{session_key}=#{signed_session_value};"
  end

  def signed_session_value
    @cookie_jar.signed[session_key] = {:value => session_hash.to_hash}
    Rack::Utils.escape(@cookie_jar[session_key])
  end
end

Initialize a new object, set a session key and value, then call #to_s to retrieve the signed session cookie string.

Here's how you use it:

1
2
3
4
5
6
7
Given /^I am signed in as (.+)$/ do |nickname|
  @current_user = User.find_by_nickname(nickname)
  session_generator = SessionCookieGenerator.new
  session_generator['user_id'] = @current_user.id
  page.driver.set_cookie session_generator.to_s
  page.driver.get "/"
end
The above snippet assumes that a user is signed in via session[:user_id].

Now you can sign in without going through the sign-in form; just make sure at least one test actually goes through the full sign-in process.