Table of Contents
Handling sessions in multi-domain environment is not the simplest things to do, because of the fact that cookies are scoped to a domain they were set by.
Recently we were developing an application with such an idea in mind:
-
Application will work as a base for other mini-applications (which we call sites)
-
Each site can be accessed via different url types: site.example.org and example.org/site
-
We want the users to remain logged in when switching from one url type to another
I won't be covering application structure, routing, etc. here, I will only write about maintaing the sessions is such an environment.
So this is pretty simple here - all that we needed to do was to set cookie domain to .example.org (note the dot
at the beginning). This could be done via:
ActionController::Base.session = {
:domain => ".example.org"
}
However there was an additional requirement that we need to deal with:
-
Each site can be accessed via custom domain - site.com
-
Of course there's no way here to keep the user logged in when he's switching from site.com to example.org/site or site.example.org, at least it cannot be done with setting cookie domain to whatever value
Technically, to access the site via site.com, that domain must point to our IP address. Then we need to detect that the site is being accessed via custom domain and set cookie domain respectively.
This could be done via some funky before_filters in an Application Controller, however we found much better and cleaner way.
Rack's middleware to the rescue
Rack itself is a minimal interface between web server and your ruby framework. It's used by Ruby on Rails (since 2.3) and Merb. The request comes from web server, goes through middleware layers and enters the application.
So we wrote a middleware layer that detects the host with which our application is accessed and sets cookie domain for the request. Here it is:
app/middlewares/set_cookie_domain.rb
class SetCookieDomain
def initialize(app, default_domain)
@app = app
@default_domain = default_domain
end
def call(env)
host = env["HTTP_HOST"].split(':').first
env["rack.session.options"][:domain] = custom_domain?(host) ? ".#{host}" : "#{@default_domain}"
@app.call(env)
end
def custom_domain?(host)
domain = @default_domain.sub(/^\./, '')
host !~ Regexp.new("#{domain}$", Regexp::IGNORECASE)
end
end
Now we need to turn it on:
environment.rb
config.load_paths += %W( #{RAILS_ROOT}/app/middlewares )
production.rb
config.middleware.use "SetCookieDomain", ".example.org"
.example.org is the default domain that will be used unless the application is accessed via custom domain (like site.com), we give it different values depending on environment (production/staging/development etc).
And since we're fans of test driven development, here's the test that ensures us that everything works as expected:
tests/integration/set_cookie_domain_test.rb
require 'test_helper'
class SetCookieDomainTest < ActionController::IntegrationTest
context "when accessing site at example.org" do
setup do
host! 'example.org'
visit '/'
end
should "set cookie_domain to .example.org" do
assert_equal '.example.org', @integration_session.controller.request.session_options[:domain]
end
end
context "when accessing site at site.com" do
setup do
host! 'site.com'
visit '/'
end
should "set cookie_domain to .site.com" do
assert_equal '.site.com', @integration_session.controller.request.session_options[:domain]
end
end
context "when accessing site at site.example.org" do
setup do
host! 'site.example.org'
visit '/'
end
should "set cookie_domain to .example.org" do
assert_equal '.example.org', @integration_session.controller.request.session_options[:domain]
end
end
end
Test is sponsored by great Shoulda and Webrat gems.
Feel free to comment and share.