Subdomains and sessions to the rescue!

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.

After some research we decided to go with the subdomain-fu gem, which is great to give your application the ability of handling subdomains. Another great resource we have used is Ryan Bates’ screencast 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.

Setup a development environment

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 /etc/hosts, and configuring the subdomains you are goint to need:

127.0.0.1     localhost local.host test.local.host subdomain.local.host xxx.local.host

We are making explicit all domains and subdomains that will point to our local machine, under IP 127.0.0.1. By default localhost 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, and point subdomains to it like sub.my_app.local. There are some other ways to create this setup, but we would rather go with this due to readability and easy configuration.

To make sure everything is fine we need to clear our dns cache:

Under linux:

/etc/init.d/dns-clean start

Under OS X:

dscacheutil -flushcache

And that is it. Restart your application and try it out. Access it under the new domain or subdomain you have created, for instance local.host:3000, to see if everything is working fine.

You may be asking why we could not be using just subdomain.localhost instead of subdomain.local.host 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!

Installing subdomain-fu

Subdomain-fu is easy to install as any other gem:

gem install subdomain-fu

Configure it in your environment:

config.gem 'subdomain-fu', :version => '0.5.3'

Then we are going to need a config file to setup subdomain-fu, with basically the following code:

SubdomainFu.tld_sizes = {
  :development => 1, # local.host
  :test => 1,
  :production => 1 # my_app.com
}

This config file can be placed at config/initializers folder. Basically it tells subdomain-fu the sizes of the domain for each environment, i.e. 0 for localhost, 1 for foo.com, 2 for foo.bar.com.

The gem gives you some special powers through the current_subdomain method in your controllers, and also some adds to your routes like using the :subdomain option: root_url(:subdomain => 'foo') #=> foo.local.host.

Routing

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 admin.foo.com. And subdomain-fu will help you here: you just need to setup your routes using the :subdomain condition, like the example below:

ActionController::Routing::Routes.draw do |map|
  map.with_options :conditions => { :subdomain => /^[A-Za-z0-9-]+$/ } do |app|
    app.resources :posts
    app.root :controller => 'posts'
  end
  # ... other routes
end

This will ensure your routes require a subdomain to be recognized.

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.

Sharing sessions between subdomains

Due to security issues, browsers do not allow sharing cookies using only .com, just under the complete domain like foo.com, and of course it is totally right. Could you imagine sharing sessions between gmail.com and hotmail.com, just because they are both .com? I could not. The same rule applies while trying to share sessions using localhost only. Browsers will not allow you sharing sessions between subdomains under localhost, and some weird issues may appear. We have had AuthenticityToken errors while trying to use localhost.

Anyway, we were smart enough to create our own subdomain configuration using local.host instead of localhost, remember? Our configuration actually simulates a full domain like the foo.com example instead of .com only. Think this way: localhost => com, local.host => foo.com.

We are going to use this setup now. To enable sharing sessions in your application you need to configure the :domain option in your session config hash, for each environment. Here is how to do it in development config:

config.action_controller.session = { :domain => '.local.host' }

Please note the dot prepended to the domain. It will enable sharing sessions between all subdomains in your application. Think about it as *.local.host, including local.host 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.

Do not forget to setup the production environment with the same config, pointing to the real domain of your application.

Testing

We are using cucumber in this application together with celerity/culerity, 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 example.com. And that is okay for default Rails integration tests, except when we use “real browsers” tests like celerity or selenium. You have to set it up by yourself. Just create two steps like this:

Given /^I am visiting the root application$/ do
  host! 'local.host'
end

Given /^I am visiting the subdomain "([^\"]*)"$/ do |subdomain|
  host! "#{subdomain}.local.host"
end

And use then inside your features:

Given I am visiting the subdomain "my_sub"

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 =).

Mailers

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 Thread.current. Just create a filter in your application controller:

before_filter :set_current_subdomain
protected
  def set_current_subdomain
    Thread.current[:current_subdomain] = current_subdomain
  end

Then create a new helper called MailHelper inside your app/helpers folder, adding a method to obtain the subdomain:

def current_subdomain
  Thread.current[:current_subdomain]
end

This MailHelper 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 method, just like you do in your controllers:

link_to "Go to application", root_url(:subdomain => current_subdomain)

What about Devise?

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 User model has a subdomain attribute and you want the authentication process take into account this subdomain with the current_subdomain. First of all you need to update devise call inside your user model to add the :authentication_keys option:

devise :all, :authentication_keys => [:email, :subdomain]

This will tell devise to find the user based on both subdomain and email. Then you have to add the subdomain to your sign in form as a hidden field, so Devise will be able to get this information easily from params while authenticating the user:

f.hidden_field :subdomain, :value => current_subdomain

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:

def self.find_for_authentication(conditions={})
  conditions[:accesses] = { :subdomain => conditions.delete(:subdomain) }
  find(:first, :conditions => conditions, :joins => :accesses)
end

Devise is now totally capable of handling authentication based on subdomains. Remember: 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.

Here we go!

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.

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?

33 responses to “Subdomains and sessions to the rescue!”

  1. rodrigo3n says:

    Very good article, I definitely should try Culerity and Devise.
    Ahh, we don’t need *gem sources -a http://gemcutter.org* anymore, remember gems.rubyforge.org now points to gemcutter.org 😀

  2. rodrigo3n says:

    Very good article, I definitely should try Culerity and Devise.
    Ahh, we don’t need *gem sources -a http://gemcutter.org* anymore, remember gems.rubyforge.org now points to gemcutter.org 😀

  3. Sure, I always forget it.. bad habit =)..
    Thanks for the tip, I’ll update the post.

  4. Sure, I always forget it.. bad habit =)..
    Thanks for the tip, I’ll update the post.

  5. Try to fix that whitespace under “Configure it in your environment” it made me think there is no more content (a friend said this too…)

  6. Try to fix that whitespace under “Configure it in your environment” it made me think there is no more content (a friend said this too…)

  7. Tomislav Car says:

    In one of our applications, we actually had the need to have different domains alltogether, so the way we work around that is to append a “development sufix” to the domain name, and just strip it off.

    So, /etc/hosts could look like
    127.0.0.1 http://www.infinum.hr-local
    127.0.0.1 http://www.google.com-local
    etc.

    you access it locally with
    http://www.infinum.hr-local:3000

  8. Tomislav Car says:

    In one of our applications, we actually had the need to have different domains alltogether, so the way we work around that is to append a “development sufix” to the domain name, and just strip it off.

    So, /etc/hosts could look like
    127.0.0.1 http://www.infinum.hr-local
    127.0.0.1 http://www.google.com-local
    etc.

    you access it locally with
    http://www.infinum.hr-local:3000

  9. Tomislav Car says:

    Also, Carlos, did you find a way to dynamically generate the contents of the /etc/hosts file?

    I don’t like that I have to enter manually each subdomain/domain I’d like to test into /etc/hosts.

    Something like this in /etc/hosts would be perfect:
    include /home/myapp/config/hosts

    And then I could just generate the file with subdomains/domains from the database and include it in /etc/hosts.

    I could probably append to the /etc/hosts file, but that would require sudo, and wouldn’t be alltogether automatic…

    It’s not a big thing I know, but it makes you happy 🙂

  10. Tomislav Car says:

    Also, Carlos, did you find a way to dynamically generate the contents of the /etc/hosts file?

    I don’t like that I have to enter manually each subdomain/domain I’d like to test into /etc/hosts.

    Something like this in /etc/hosts would be perfect:
    include /home/myapp/config/hosts

    And then I could just generate the file with subdomains/domains from the database and include it in /etc/hosts.

    I could probably append to the /etc/hosts file, but that would require sudo, and wouldn’t be alltogether automatic…

    It’s not a big thing I know, but it makes you happy 🙂

  11. @tomislav I think applications won’t require that much subdomains to test while in development. Actually I’m using just 3, and one of them is also configured in our test environment so tests always run in a “real” configured subdomain. Due to it we decided to go manually edit /etc/hosts.

    In addition, instead of editing your /etc/hosts file each time you need a new subdomain, you could also create a PAC file and setup your browser to use it as a proxy. There is an example of a file like this in the railscast I’ve linked in the text. That should do the trick.
    I hope that helps =).
    Thanks, Carlos.

  12. @tomislav I think applications won’t require that much subdomains to test while in development. Actually I’m using just 3, and one of them is also configured in our test environment so tests always run in a “real” configured subdomain. Due to it we decided to go manually edit /etc/hosts.

    In addition, instead of editing your /etc/hosts file each time you need a new subdomain, you could also create a PAC file and setup your browser to use it as a proxy. There is an example of a file like this in the railscast I’ve linked in the text. That should do the trick.
    I hope that helps =).
    Thanks, Carlos.

  13. George Guimarães says:

    @tomislav

    You can use DNS to do that. Instead of local.host that Carlos used, you can use a real DNS domain, like local.infinum.hr.

    It’s possible to use a wildcard to map all *.local.infinum.hr to 127.0.0.1. So, there would be no reason to dinamically mess with /etc/hosts. Sure… this may be a bit overwhelming but it’s a simple DNS-hack that works.

    Here at Plataforma we have never used such config, but I have personally used this when building/testing some personal projects.

    Another aproach would be configuring an local DNS resolver, but that’s even more cumbersome.. =D

  14. George Guimarães says:

    @tomislav

    You can use DNS to do that. Instead of local.host that Carlos used, you can use a real DNS domain, like local.infinum.hr.

    It’s possible to use a wildcard to map all *.local.infinum.hr to 127.0.0.1. So, there would be no reason to dinamically mess with /etc/hosts. Sure… this may be a bit overwhelming but it’s a simple DNS-hack that works.

    Here at Plataforma we have never used such config, but I have personally used this when building/testing some personal projects.

    Another aproach would be configuring an local DNS resolver, but that’s even more cumbersome.. =D

  15. Tomislav Car says:

    Ok, thanks Carlos and George, I’m sure I’ll implement one of these solutions.

  16. Tomislav Car says:

    Ok, thanks Carlos and George, I’m sure I’ll implement one of these solutions.

  17. José Valim says:

    @Daniel Yeah, fixed. I already tried to fix that several times, but for some reason today I could handle that in 5 minutes. Thanks for the motivation 😉

  18. José Valim says:

    @Daniel Yeah, fixed. I already tried to fix that several times, but for some reason today I could handle that in 5 minutes. Thanks for the motivation 😉

  19. Dan Pickett says:

    Ghost is an awesome gem for managing hostnames when testing:

    http://github.com/bjeanes/ghost/

  20. Dan Pickett says:

    Ghost is an awesome gem for managing hostnames when testing:

    http://github.com/bjeanes/ghost/

  21. Daniel Kehoe says:

    I wonder if you looked at Matthew Hollingworth’s subdomain_routes gem as an alternative to the subdomain-fu gem. He says it’s useful in URL generation, route recognition, and better for route definition. The subdomain_routes code for the routes.rb file looks simpler than what’s needed for subdomain-fu.

    I’m about to start a project which requires subdomains (and uses Devise), so I’m curious if you made a choice (and why) between subdomain_routes and subdomain-fu.

  22. Daniel Kehoe says:

    I wonder if you looked at Matthew Hollingworth’s subdomain_routes gem as an alternative to the subdomain-fu gem. He says it’s useful in URL generation, route recognition, and better for route definition. The subdomain_routes code for the routes.rb file looks simpler than what’s needed for subdomain-fu.

    I’m about to start a project which requires subdomains (and uses Devise), so I’m curious if you made a choice (and why) between subdomain_routes and subdomain-fu.

  23. @Daniel I haven’t looked at subdomain_routes, but it seems to be a nice library for handling subdomains. I’ll definitely take a closer look at this library.

    Anyway, subdomain_routes should do the trick as well as subdomain-fu did. You shouldn’t have issues with both. The main difference I can see for your code is the way you’ll be using routes, as subdomain full requires you to use :subdomain key instead of adding it to the named route. And you just need to test whether the model-based subdomains will work as expected for you specific case.
    I’d encourage you to give it a try with subdomain_routes. Also, you could provide some feedback here again if you decide so.

    Thanks, Carlos

  24. @Daniel I haven’t looked at subdomain_routes, but it seems to be a nice library for handling subdomains. I’ll definitely take a closer look at this library.

    Anyway, subdomain_routes should do the trick as well as subdomain-fu did. You shouldn’t have issues with both. The main difference I can see for your code is the way you’ll be using routes, as subdomain full requires you to use :subdomain key instead of adding it to the named route. And you just need to test whether the model-based subdomains will work as expected for you specific case.
    I’d encourage you to give it a try with subdomain_routes. Also, you could provide some feedback here again if you decide so.

    Thanks, Carlos

  25. Daniel Lopes says:

    Really good post, as always. I also use Ghost for my local machine too.

    In case of mailer I normally store the subdomain with another account data in database table, so when a rake taks or cron call a mailer I allways query for account and pass that account as body param to mailer. But your aproach with Threads are great too, thanks.

    For wildcard subdomains is really easy to setup like George said, but you should remember if you release it on web you will need a wildcard ssl certiface and it will cost to you arround $200 year.

  26. Daniel Lopes says:

    Really good post, as always. I also use Ghost for my local machine too.

    In case of mailer I normally store the subdomain with another account data in database table, so when a rake taks or cron call a mailer I allways query for account and pass that account as body param to mailer. But your aproach with Threads are great too, thanks.

    For wildcard subdomains is really easy to setup like George said, but you should remember if you release it on web you will need a wildcard ssl certiface and it will cost to you arround $200 year.

  27. Eric Wagoner says:

    One thing about subdomains that I never see addressed:

    You’ll want to make http://www.subdomain.domain act the same as subdomain.domain, especially if you’re writing an application that will be used by the general public. A very large percent of the population think everything on the web has to start with “www.” even if you tell them otherwise. The press will do the same thing, no matter how much effort you put into giving them the correct URL.

    So, save yourself from a lot of headache, and your users from a lot of confusion, by just making the two equivalent in your application.

  28. Eric Wagoner says:

    One thing about subdomains that I never see addressed:

    You’ll want to make http://www.subdomain.domain act the same as subdomain.domain, especially if you’re writing an application that will be used by the general public. A very large percent of the population think everything on the web has to start with “www.” even if you tell them otherwise. The press will do the same thing, no matter how much effort you put into giving them the correct URL.

    So, save yourself from a lot of headache, and your users from a lot of confusion, by just making the two equivalent in your application.

  29. Andrew Coleman says:

    I wrote a slightly similar plugin for dealing with segregated data sets. I had similar needs but nobody should be able to share any data with any other subdomain. Check it out: http://github.com/penguincoder/acts_as_restricted_subdomain

    There is a fork that has support for recognizing the entire domain, as well so you aren’t limited to just subdomains.

    I use the plugin in a medical environment and it works great for keeping everything separate while allowing for a multi-homed application.

  30. Andrew Coleman says:

    I wrote a slightly similar plugin for dealing with segregated data sets. I had similar needs but nobody should be able to share any data with any other subdomain. Check it out: http://github.com/penguincoder/acts_as_restricted_subdomain

    There is a fork that has support for recognizing the entire domain, as well so you aren’t limited to just subdomains.

    I use the plugin in a medical environment and it works great for keeping everything separate while allowing for a multi-homed application.

  31. @daniel Thanks, Ghost is really a nice tool. Storing the mail in the database seems great when you use rake tasks or cron jobs, or even a background job to send emails.

    @eric Thanks for the tip.

    @andrew Nice plugin, I’ll take a look at it. Thanks.

  32. @daniel Thanks, Ghost is really a nice tool. Storing the mail in the database seems great when you use rake tasks or cron jobs, or even a background job to send emails.

    @eric Thanks for the tip.

    @andrew Nice plugin, I’ll take a look at it. Thanks.

  33. Anonymous says:

    Interesting! Gives me lots to think about! I learn something (usually more than one) new every day here!http://questionaries.org