Jonathan Martin
Rails 3: Forcing SSL
~ by Jonathan Martin
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | 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:
1 2 3 4 | # 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:
1 2 3 4 5 6 7 8 9 10 | # 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:
1 2 3 4 5 | 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.