`config.force_ssl` is different than controller `force_ssl`
Article… or why my cookies don’t have secure flag anymore
Several of you may know that Rails provide force_ssl feature. This is
a handy option that will tell Rails application to load website as
https when someone tries to access it via http.
This baby comes in two forms:
Developer can specify that the entire website is https in
config/enviroments/production.rb or config/initializers/...
# config/enviroments/production.rb
MyApp::Application.configure do
# ...
config.force_ssl = true
# ...
end
… or Developer can tell particular controller to force_ssl
# app/controllers/secret_stuff_controller.rb
class SecretStuffController < ApplicationController
force_ssl
# ...
end
Let’s try the config.force_ssl (config for entire application).
All web-pages will be enforced to use https (nice).
How about cookies ?
Some of you may know that cookies have security options like:
- when to expire the cookie (
Expireoption), - should the cookie be sent only via HTTP or also other protocols like JavaScript (
HttpOnly) - wether the cookie should be sent over by
httpandhttpsconnection or just viahttpsconnection (Secureoption)
If you’re using in your Rails app authentication gem Devise
it will take good care when setting session_id cookie on options
HttpOnly and when to expire. But Secure option won’t be set.
This is where force_ssl (still the global config one) comes handy. It will
not only enforce the http to https redirect, but will enforce
session cookie to be secure => not to be sent via non-secure
conection.


Awesome :)
But problem is that I need to have entire app under https and one
controller http. So let’s try controller force_ssl:


Well I guess we need to check the source :(
# rails/railties/lib/rails/application/default_middleware_stack.rb
def build_stack
# ...
if config.session_store
if config.force_ssl && !config.session_options.key?(:secure)
config.session_options[:secure] = true
end
# ...
end
# ...
end
# rails/actionpack/lib/action_controller/metal/force_ssl.rb
# ...
module ForceSsl
# ...
def force_ssl(options = {})
action_options = options.slice(*ACTION_OPTIONS)
redirect_options = options.except(*ACTION_OPTIONS)
before_action(action_options) do
force_ssl_redirect(redirect_options)
end
end
# ...
end
# ...
My reaction at this point can be described like this => http://youtu.be/TOakzl0k6ik
Solution ?
Well the easiest way would be just tell that:
# config/enviraments/production.rb
config.session_options[:secure] = true
… right ?
Well, this wont work:
-
A/ Rails will ignore this option ( don’t quite know why because I stopped investigationg source code when I realized point B)
-
B/ you are setting
secure=truemeaning: send the session cookie only if user is on secure connection. This is ok whenconfig.force_sslis used globaly on a whole application as everything will be underhttps, but if you forcinghttpsonly on some parts of the application you will not know what is the session of a user visiting site (for example you may want to track if public FAQ was visited by a particular user that is still logged in)
So, the solution: two cookies to save the day!
Basically the idea is that you will leave the “unsecure” session cookie
alone and you create another “secure” cookie. You evaluate both cookies
to check who the “logged in” user is on https sites (protected sites) and you will
still be able to use unsecure cookie to track movement on public pages
(once again the “secure” cookie won’t be sent on non-https conection).
In other words this will prevent session hijacking as you need both cookies to validate the user, and only unsecure cookie to track user activity on public pages.
It may be the case that you will need to have two user variables in you
controller @current_user and @current_user_non_secure but I’ll skip
the implementation details as the article is not about this.
Good example how to implement secure cookie is here:
http://railscasts.com/episodes/356-dangers-of-session-hijacking
# app/controllers/sessions_controller.rb
def create
# ...
cookies.signed[:secure_user_id] = {secure: true, value:
"some_really_random_secure_stuff"}
# ...
end
def destroy
# ...
cookies.delete(:secure_user_id)
end
Devise solution
There is devise_ssl_session_verifiable doing exactly what you need.
# Gemfile
# ...
gem 'devise'
gem 'devise_ssl_session_verifiable'
# ...
souces:
- Discussion on Devise gem issues page https://github.com/plataformatec/devise/issues/3433
Entire blog website and all the articles can be forked from this Github Repo