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