Thursday, July 28, 2011

Getting SSL, Capybara, Rails 3, and Devise to work together

I've noticed a lot of Rails blog posts recommending that you only turn SSL on in production and don't try to use it in the test or development environments. This is a monumental mistake, a lesson I learned hard in the early days of OtherInbox: SSL enforcement has a lot of subtleties and corner cases that need to be exercised and tested on a routine basis. As a quick slightly embarrasing example, we had a bug at OIB where flash messages would get swallowed during redirects. It turned out we were accidentally redirecting people to an http:// page, where the flash would be available, but our SSL enforcement code redirected them quickly to https://, and that second redirect ate the flash message. Since we couldn't reproduce the bug on our own machines it was much harder to track down than you would think.

But I know why the bloggers recommend the naive approach though: getting SSL to work offline is a major pain! Right now I'm building a Rails app that enforces SSL on every page. It's all private data (the public-facing site is managed by a separate CMS) so I don't want any page to be accessible over http://. Below is a summary of the code changes I had to make to get this to work. I was mainly guided by this great Collective Idea blog post about Rails 3 SSL that I'd recommend you start with.

It's not hard to get SSL working in development mode if you use an app container like Phusion Passenger. I explained how to do that a few years ago with an Apache-Mongrel setup, but the basic ideas apply.

Update 1/31/2012: I wrote up an easy way to get SSL working with Unicorn and Pound instead of Passenger which is my new preferred way of working. Also check out the Devise wiki for more useful SSL info.
# config/application.rb
Bundler.require(:default, Rails.env) if defined?(Bundler)
require 'rack/ssl' # add this before the app definition
module YourApp
class Application < Rails::Application
# <snip>
config.middleware.insert_before ActionDispatch::Cookies, Rack::SSL
# <snip>
end
end
# when Rails 3.1 ships you can get rid of the rack/ssl require, and just
# add config.force_ssl = true to the above
view raw application.rb hosted with ❤ by GitHub
# app/controllers/application_controller.rb
def default_url_options(options = {})
options.merge(protocol: "https")
end
# config/initializers/devise.rb
# httponly: true is not needed for SSL enforcement but I think it's a good default
config.cookie_options = { secure: true, httponly: true }
view raw devise.rb hosted with ❤ by GitHub
# lib/failure_app.rb
# found this on the devise wiki but can't find the page anymore
# make sure this code gets loaded; if it's in lib you need to require it
# explicitly or make lib/ an autoload path
class CustomFailure < Devise::FailureApp
def redirect_url
new_user_session_url(protocol: "https")
end
# You need to override respond to eliminate recall
def respond
if http_auth?
http_auth
else
redirect
end
end
end
view raw failure_app.rb hosted with ❤ by GitHub
gem "rack-ssl", "1.3.2"
# when Capybara issue 409 or 422 get resolved, you can switch back to the official
# capybara gem
# https://github.com/jnicklas/capybara/pull/409
# https://github.com/jnicklas/capybara/pull/422
gem "capybara", git: "https://github.com/mcolyer/capybara.git", branch: "fix-ssl-redirects"
view raw Gemfile hosted with ❤ by GitHub
# config/initializers/session_store.rb
# this probably isn't needed since Rack::SSL handles it, but just for good measure
YourApp::Application.config.session_store :cookie_store,
:key => '_yourapp_secure_session',
:secure => true,
:httponly => true

No comments: