Securing specific routes in Rails 3

By: Todd Fisher, 20 Feb 2011
ssl

SSL is important when dealing with user data.   The rails 3 router makes it easy to mark specific sections of your application secure using the :constraints option within a scope block.

See the following example:

scope :constraints => { :protocol => 'https' } do
    resources :invoices do
      resources :line_items
    end
end

Viewing the corresponding routes this generates the following:

invoice_line_items GET /invoices/:invoice_id/line_items(.:format)  {:protocol=>"https", :action=>"index", :controller=>"line_items"}
                   POST /invoices/:invoice_id/line_items(.:format) {:protocol=>"https", :action=>"create", :controller=>"line_items"}
new_invoice_line_item GET /invoices/:invoice_id/line_items/new(.:format) {:protocol=>"https", :action=>"new", :controller=>"line_items"}
edit_invoice_line_item GET /invoices/:invoice_id/line_items/:id/edit(.:format) {:protocol=>"https", :action=>"edit", :controller=>"line_items"}
invoice_line_item GET /invoices/:invoice_id/line_items/:id(.:format) {:protocol=>"https", :action=>"show", :controller=>"line_items"}
                  PUT    /invoices/:invoice_id/line_items/:id(.:format) {:protocol=>"https", :action=>"update", :controller=>"line_items"}
                  DELETE /invoices/:invoice_id/line_items/:id(.:format) {:protocol=>"https", :action=>"destroy", :controller=>"line_items"}
invoices GET /invoices(.:format) {:protocol=>"https", :action=>"index", :controller=>"invoices"}
         POST /invoices(.:format) {:protocol=>"https", :action=>"create", :controller=>"invoices"}
new_invoice GET /invoices/new(.:format) {:protocol=>"https", :action=>"new", :controller=>"invoices"}
edit_invoice GET /invoices/:id/edit(.:format) {:protocol=>"https", :action=>"edit", :controller=>"invoices"}
invoice GET /invoices/:id(.:format) {:protocol=>"https", :action=>"show", :controller=>"invoices"}
        PUT /invoices/:id(.:format) {:protocol=>"https", :action=>"update", :controller=>"invoices"}
        DELETE /invoices/:id(.:format) {:protocol=>"https", :action=>"destroy", :controller=>"invoices"}

The important part here is:

 :protocol => "https"

This tells rails to only honor the route when the protocol matches HTTPS, but not when it is HTTP.  Perfect, exactly what we need – no plugins required!

Well, almost perfect. The first problem encountered, is likely RAILS_ENV=development no longer works as those routes are only accesible through HTTPS. A simple solution – turn the :protocol value into a variable and change it to HTTP for development, otherwise HTTPS. Add the variable in config/environments/development.rb like so:

SSL_PROTO__ = 'http'

and add the following at the top of config/routes.rb

SSL_PROTO__ = 'https' unless defined?(SSL_PROTO__)

Great, now we can still test SSL in development by simply changing the constant in config/environments/development.rb and our tests can verify SSL is enabled for specific sections of the application.

This is almost everything necessary – the next gotcha will be making sure our application routes HTTP requests to the new HTTPS routes. To do this we can use a special catch all route:

match "invoices(/*path)",
      :to => redirect { |params, request|
                        "https://" + request.host_with_port +
                                     request.fullpath
                      }

Adding this after the invoices resource allows us to catch requests sent to HTTP and redirect them to HTTPS.

The catch all route is nice, but really our application should always link to these resources using SSL. Add the following to your links:

<%= link_to t('menus.new_invoice'),
new_invoice_url(:protocol => 'https') %>

Adding SSL constraints is pretty easy and effective.   What I like about this solution is that it doesn’t require any extra plugins and it is fairly simple to understand.

Tags: , , ,

Comments are closed.