Posts tagged "subdomain"

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?