{"id":481,"date":"2009-12-16T16:48:31","date_gmt":"2009-12-16T18:48:31","guid":{"rendered":"http:\/\/blog.plataformatec.com.br\/?p=481"},"modified":"2009-12-16T20:06:21","modified_gmt":"2009-12-16T22:06:21","slug":"subdomains-and-sessions-to-the-rescue","status":"publish","type":"post","link":"https:\/\/blog.plataformatec.com.br\/2009\/12\/subdomains-and-sessions-to-the-rescue\/","title":{"rendered":"Subdomains and sessions to the rescue!"},"content":{"rendered":"
We have been working on an application that allows administrators to create accounts for their users. Each account will be accessible under a subdomain, so we needed to setup a subdomain environment inside our application, and also in our development machine. In addition, we must be able to let the users signed in among several subdomains, as a user can access other accounts when allowed.<\/p>\n
After some research we decided to go with the subdomain-fu<\/a> gem, which is great to give your application the ability of handling subdomains. Another great resource we have used is Ryan Bates’ screencast<\/a> about the subject. But they did not solve our problem completely, so here we are going to document a few steps to help you get up and running easily with subdomains and sessions.<\/p>\n As we need to test all the subdomain stuff in our application while developing it, we are going to need some extra setup for our development environment. However, our local machine knows nothing about subdomains, so how do we do that? We need to tell our machine which subdomains we will be using, manually. You can do this by editing your hosts file, located at We are making explicit all domains and subdomains that will point to our local machine, under IP 127.0.0.1. By default To make sure everything is fine we need to clear our dns cache:<\/p>\n Under linux:<\/p>\n Under OS X:<\/p>\n And that is it. Restart your application and try it out. Access it under the new domain or subdomain you have created, for instance You may be asking why we could not be using just Subdomain-fu<\/a> is easy to install as any other gem:<\/p>\n Configure it in your environment:<\/p>\n Then we are going to need a config file to setup subdomain-fu<\/a>, with basically the following code:<\/p>\n This config file can be placed at The gem gives you some special powers through the Chances are you are going to need specific actions to be handled only under subdomains, but not in the root domain or a specific subdomain, like This will ensure your routes require a subdomain to be recognized.<\/p>\n Finally, everything is perfect, you are now able to test subdomains in your environment, create some filters to ensure the subdomain exists and your user has access to it, sessions work as expected, and so on, right? Nope. We are not ready yet. Sessions are our pain.<\/p>\n Due to security issues, browsers do not allow sharing cookies using only Anyway, we were smart enough to create our own subdomain configuration using We are going to use this setup now. To enable sharing sessions in your application you need to configure the Please note the dot prepended to the domain. It will enable sharing sessions between all subdomains in your application. Think about it as Do not forget to setup the production environment with the same config, pointing to the real domain of your application.<\/p>\n We are using cucumber<\/a> in this application together with celerity<\/a>\/culerity<\/a>, and at the beginning it was kind of pain to get it up and running. The first thing you must bear in mind is that you always have to setup the host you are testing. By default, cucumber uses host And use then inside your features:<\/p>\n That should do the trick. Make sure you use only subdomains you have configured in your hosts file, or you will get some weird errors =).<\/p>\n We had some issues while creating mailers with links pointing to the subdomain. As our users has many subdomains, we don’t know where we should point the user inside the mailer, due to default lack of request context as we have in the controller. To solve this we could not use any class accessor, because they are not thread safe. So we decided to go with Then create a new helper called This Devise has been doing a great work while authenticating a user under a specific subdomain. There are two cases to be handled: the first one is when your This will tell devise to find the user based on both The second case happens when your subdomain data is inside another model associatied with the user, let’s say a user has many accesses. In addition to what we have done in the first case, we must override a class method from Devise to add our own condition for finding the user:<\/p>\n Devise is now totally capable of handling authentication based on subdomains. Remember<\/strong>: Devise is managing authentication, so it will not be able to do anything after the user signs in. Be sure to also add filters to your controllers to ensure a user will never access a subdomain it has no access.<\/p>\n A few steps are needed to get our development machine up and running to create an application using subdomains, but they are key steps to ensure you are not going to have problems while starting. <\/p>\n What about you? Have you ever developed an application using subdomains? Have you run through any of these issues, or maybe another you want to share? Do you have any tip?<\/p>\n We have been working on an application that allows administrators to create accounts for their users. Each account will be accessible under a subdomain, so we needed to setup a subdomain environment inside our application, and also in our development machine. In addition, we must be able to let the users signed in among several … \u00bb<\/a><\/p>\n","protected":false},"author":7,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"ngg_post_thumbnail":0,"footnotes":""},"categories":[1],"tags":[7,56,55],"aioseo_notices":[],"jetpack_sharing_enabled":true,"jetpack_featured_media_url":"","_links":{"self":[{"href":"https:\/\/blog.plataformatec.com.br\/wp-json\/wp\/v2\/posts\/481"}],"collection":[{"href":"https:\/\/blog.plataformatec.com.br\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/blog.plataformatec.com.br\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/blog.plataformatec.com.br\/wp-json\/wp\/v2\/users\/7"}],"replies":[{"embeddable":true,"href":"https:\/\/blog.plataformatec.com.br\/wp-json\/wp\/v2\/comments?post=481"}],"version-history":[{"count":43,"href":"https:\/\/blog.plataformatec.com.br\/wp-json\/wp\/v2\/posts\/481\/revisions"}],"predecessor-version":[{"id":524,"href":"https:\/\/blog.plataformatec.com.br\/wp-json\/wp\/v2\/posts\/481\/revisions\/524"}],"wp:attachment":[{"href":"https:\/\/blog.plataformatec.com.br\/wp-json\/wp\/v2\/media?parent=481"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blog.plataformatec.com.br\/wp-json\/wp\/v2\/categories?post=481"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blog.plataformatec.com.br\/wp-json\/wp\/v2\/tags?post=481"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}Setup a development environment<\/h3>\n
\/etc\/hosts<\/code>, and configuring the subdomains you are goint to need:<\/p>\n
127.0.0.1 \u00a0 \u00a0 localhost local.host test.local.host subdomain.local.host xxx.local.host<\/pre>\n
localhost<\/code> was already there, so we have just added the needed subdomains. When working with this subdomains I personally like to create a domain like
my_app.local<\/code>, and point subdomains to it like
sub.my_app.local<\/code>. There are some other ways to create this setup, but we would rather go with this due to readability and easy configuration.<\/p>\n
\/etc\/init.d\/dns-clean start<\/pre>\n
dscacheutil -flushcache<\/pre>\n
local.host:3000<\/code>, to see if everything is working fine.<\/p>\n
subdomain.localhost<\/code> instead of
subdomain.local.host<\/code> as subdomain configuration. And you are right, we could use it, and it should work fine to access subdomains. However, there are some “bumps in the road” while sharing sessions, so keep reading!<\/p>\n
Installing subdomain-fu<\/h3>\n
gem install subdomain-fu<\/pre>\n
config.gem 'subdomain-fu', :version => '0.5.3'<\/pre>\n
SubdomainFu.tld_sizes = {\r\n :development => 1, # local.host\r\n :test => 1,\r\n :production => 1 # my_app.com\r\n}<\/pre>\n
config\/initializers<\/code> folder. Basically it tells subdomain-fu<\/a> the sizes of the domain for each environment, i.e. 0 for
localhost<\/code>, 1 for
foo.com<\/code>, 2 for
foo.bar.com<\/code>.<\/p>\n
current_subdomain<\/code> method in your controllers, and also some adds to your routes like using the
:subdomain<\/code> option:
root_url(:subdomain => 'foo') #=> foo.local.host.<\/code><\/p>\n
Routing<\/h3>\n
admin.foo.com<\/code>. And subdomain-fu<\/a> will help you here: you just need to setup your routes using the :subdomain condition, like the example below:<\/p>\n
ActionController::Routing::Routes.draw do |map|\r\n map.with_options :conditions => { :subdomain => \/^[A-Za-z0-9-]+$\/ } do |app|\r\n app.resources :posts\r\n app.root :controller => 'posts'\r\n end\r\n # ... other routes\r\nend<\/pre>\n
Sharing sessions between subdomains<\/h3>\n
.com<\/code>, just under the complete domain like
foo.com<\/code>, and of course it is totally right. Could you imagine sharing sessions between
gmail.com<\/a><\/code> and
hotmail.com<\/a><\/code>, just because they are both
.com<\/code>? I could not. The same rule applies while trying to share sessions using
localhost<\/code> only. Browsers will not allow you sharing sessions between subdomains under
localhost<\/code>, and some weird issues may appear. We have had
AuthenticityToken<\/code> errors while trying to use
localhost<\/code>.<\/p>\n
local.host<\/code> instead of
localhost<\/code>, remember? Our configuration actually simulates a full domain like the
foo.com<\/code> example instead of
.com<\/code> only. Think this way:
localhost => com<\/code>,
local.host => foo.com<\/code>.<\/p>\n
:domain<\/code> option in your session config hash, for each environment. Here is how to do it in development config:<\/p>\n
config.action_controller.session = { :domain => '.local.host' }<\/pre>\n
*.local.host<\/code>, including
local.host<\/code> itself. By doing this, you are now able to sign a user in one subdomain, or even the root domain, and redirect it to any other subdomain, for instance. The session will be kept and the user will stay signed in as expected.<\/p>\n
Testing<\/h3>\n
example.com<\/code>. And that is okay for default Rails integration tests, except when we use “real browsers” tests like celerity<\/a> or selenium<\/a>. You have to set it up by yourself. Just create two steps like this:<\/p>\n
Given \/^I am visiting the root application$\/ do\r\n host! 'local.host'\r\nend\r\n\r\nGiven \/^I am visiting the subdomain \"([^\\\"]*)\"$\/ do |subdomain|\r\n host! \"#{subdomain}.local.host\"\r\nend<\/pre>\n
Given I am visiting the subdomain \"my_sub\"<\/pre>\n
Mailers<\/h3>\n
Thread.current<\/code>. Just create a filter in your application controller:<\/p>\n
before_filter :set_current_subdomain\r\nprotected\r\n def set_current_subdomain\r\n Thread.current[:current_subdomain] = current_subdomain\r\n end<\/pre>\n
MailHelper<\/code> inside your
app\/helpers<\/code> folder, adding a method to obtain the subdomain:<\/p>\n
def current_subdomain\r\n Thread.current[:current_subdomain]\r\nend<\/pre>\n
MailHelper<\/code> is a module provided by Rails which is included in all mailers. Now you are able to create links inside your mailers using the
current_subdomain<\/code> method, just like you do in your controllers:<\/p>\n
link_to \"Go to application\", root_url(:subdomain => current_subdomain)<\/pre>\n
What about Devise?<\/h3>\n
User<\/code> model has a
subdomain<\/code> attribute and you want the authentication process take into account this
subdomain<\/code> with the
current_subdomain<\/code>. First of all you need to update devise call inside your user model to add the
:authentication_keys<\/code> option:<\/p>\n
devise :all, :authentication_keys => [:email, :subdomain]<\/pre>\n
subdomain<\/code> and
email<\/code>. Then you have to add the subdomain to your sign in form as a
hidden field<\/code>, so Devise will be able to get this information easily from params while authenticating the user:<\/p>\n
f.hidden_field :subdomain, :value => current_subdomain<\/pre>\n
def self.find_for_authentication(conditions={})\r\n conditions[:accesses] = { :subdomain => conditions.delete(:subdomain) }\r\n find(:first, :conditions => conditions, :joins => :accesses)\r\nend<\/pre>\n
Here we go!<\/h3>\n