Rails 3: Forcing SSL

Once again, I found myself beating my way through a website todo, and again I painfully managed to complete the task. Perhaps I can spare you some of that pain with this discussion of SSL.

A nice convenience with the price of two late nights spent forcing my way through the seemingly most ridiculous bugs. What objective snatched away those precious hours of sleep?

Forcing SSL. That’s it. I implemented an administrator interface to my blog so I can easily post, comment, etc. (or else I’d never get time to write) however I was bugged every time I saw the basic http auth dialog with its warning: Your password will be sent unencrypted.

Naturally, I’m a conspiracy theorist and anticipate some foreign nation overtaking my blog and using it to bring about the end of the world (ok not really). However, having to type in https:// every time I want to securely do my magic jumbo gets really irritating, and too many times have I authenticated without SSL. Way too many times.

Initial Solution

It was time to forcefully redirect to SSL sessions…sounds simple enough. I started out (well, actually this was after a few revisions) with the following skeleton-code for the application controller.

class ApplicationController < ActionController::Base
  ...

  private

  def authenticate
    unless require_ssl
      authenticate_or_request_with_http_basic do |user, password|
        session[:admin] = user == 'user' && password == 'secret'
      end
    else
      return false
    end
  end

  def require_ssl
    # SSL needs to be forced if the server is in production and the request is not already SSL
    ssl_required = Rails.env.production? and not request.ssl?

    flash.keep if ssl_required
    redirect_to :protocol => "https://" if ssl_required
    ssl_required
  end
end

I was already calling before_filter :authenticate in my controllers, so it seemed sensible to extend that before filter with an SSL redirect. However, by the first night I could not get past the dreaded “Too many redirects” error. I checked the code logic, and all seemed well — in fact, all was! But after probing the return value of request.ssl?, I found it never returned true. Why? After some searching, I found the code definition for the ssl? qualifier:

# File actionpack/lib/action_dispatch/http/url.rb, line 20
def ssl?
  @env['HTTPS'] == 'on' || @env['HTTP_X_FORWARDED_PROTO'] == 'https'
end

Passenger and SSL

So that was useless…until I did some Googling. I deploy all my apps with Passenger (which up until now has been awesome) however there is an active bug that seems to spring up every other version in which SSL headers are not transferred from Apache to the Rails app via Passenger — which means my app keeps trying to redirect to SSL, but never gets feedback that it is in SSL. So with a little more research, I found temporary fix that modified the ssl? method:

# lib/url.rb
module ActionDispatch
  module Http
    module URL
      def ssl?
        @env['SERVER_PORT'].to_s == '443' || @env['HTTPS'] == 'on' || @env['HTTP_X_FORWARDED_PROTO'] == 'https'
      end
    end
  end
end

I was pretty sure the Rails app was getting the port number, and SSL is typically handled over port 443, so I rewrote the method to take that into account. Admittedly it’s a less than ideal solution, but it was the best I could do without messing up all my clean Application.rb code. So I uploaded, did a touch tmp/restart.txt, and…

Final Adjustment

…I still got the dreaded too many redirects error! That was too much for one night, so I picked it back up the next day, this time probing the output of the ssl_required evaluation. To my astonishment, Rails.env.production? and not request.ssl? always evaluates to true! Obviously, the first operand is working properly on the production server, but after probing just the request.ssl? part, it appears to be working correctly as well.

Of all things, it appears Ruby 1.9.2 has some boolean bug, because I rewrote the ssl_required evaluation:

ssl_required = Rails.env.production? and not request.ssl?

# ...to...

ssl_required = !request.ssl? and Rails.env.production?

Essentially, I dropped the more readable “and not” syntax and went back to the more terse “!” operator. After a restart, bingo! I am still befuddled as to why my original syntax doesn’t work properly, but in the meantime I am quite satisfied to have working SSL redirection.

Concluding Remarks

The initial SSL markup was pretty easy to implement, but if you’re looking for a gemified way to setup SSL requirements, I recommend the ssl_requirement gem. The gem’s approach is almost identical to the mine, however it has some “prettier” methods you can call. As I already had some basic authentication in place and have not migrated to full user accounts yet, I needed a custom solution that would automatically be called alongside the authenticate filter.

Also, the request.ssl? override I wrote will not load automatically in Rails 3 since it is a core class — to autoload lib files, take a look at the auto_require post to auto-override the core ssl? method.

Finally, if anyone has any insight on the syntax quirk or quick/easy solutions to get Passenger 3.0.7 to pass along the SSL headers, you are welcome to enlighten me in the comments.