Simple Subdomain Authentication In Ruby on Rails

Derek
Derek
14
Aug

Using a subdomain as an account key (ie – highgroove.heartbeathq.com where “highgroove” is the account key) is a great way to personalize a web application. Rails has a nifty plugin written just for this, but the implementation information is a bit scattered. Here’s a step-by-step guide for implementing, testing, and simulating this powerful feature.

1. Take a look at the Account Location Plugin

Coming in at a concise 30 lines of code, it’s an easy read.

2. Customize the plugin for your needs

The plugin assumes we’re working with an Account model and that an instance variable named @account is the Account associated with the current subdomain. In my case, I wasn’t working with an Account class and I also didn’t want to assign the associated object to an instance variable of the same name (For example, in some cases @account could be assigned to a different account than the one associated with the subdomain).

I’m working with a Heartbeat Dashboard. Here’s my modified code. Instead of installing the plugin, I created a DashboardLocation module:


module DashboardLocation
  def self.included(controller)
    controller.helper_method(:dashboard_domain, :dashboard_subdomain, 
                             :dashboard_host, :dashboard_url, 
                             :current_dashboard, :current_subscription)
  end

  protected
    def default_dashboard_subdomain
     current_dashboard.subdomain if current_dashboard
    end

    def dashboard_url(dashboard_subdomain = default_dashboard_subdomain, use_ssl = request.ssl?)
      (use_ssl ? "https://" : "http://") + dashboard_host(dashboard_subdomain)
    end

    def dashboard_host(dashboard_subdomain = default_dashboard_subdomain)
      dashboard_host = "" 
      dashboard_host << dashboard_subdomain + "." 
      dashboard_host << dashboard_domain
    end

    def dashboard_domain
      dashboard_domain = "" 
      dashboard_domain << request.subdomains[1..-1].join(".") + "." if request.subdomains.size > 1
      dashboard_domain << request.domain + request.port_string
    end

    def dashboard_subdomain
      request.subdomains.first
    end

    def current_dashboard
      Dashboard.find(:first, 
                     :conditions => ["subdomain = ? and subdomain IS NOT NULL",dashboard_subdomain])
    end

    def ensure_current_dashboard
      return true if current_dashboard
      flash[:warning] = "Please select a dashboard to login." 
      redirect_to(:controller => '/home') and return false
    end
end

3. Install the plugin (or your own module)

If you don’t need to make any modifications, install the plugin straight-up:

ruby script/plugin install http://dev.rubyonrails.org/svn/rails/plugins/account_location/
If you need a customized module like me, put it in your lib folder (i.e. lib/dashboard_locaton.rb) and require it in your environment.rb file (require ‘dashboard_location’).

4. Add some Test Helpers

I added the methods below to TestHelper to make it easier to test our subdomain functionality. Remember I’m using current_dashboard instead of @account to represent the subdomain record.


class Test::Unit::TestCase
  ...
   # Puts a dashboard into the subdomain
  def dashboard_setup(dashboard = dashboards(:derek_dashboard) )
     @request.host = "#{dashboard.subdomain}.local.host" 
     assert_equal dashboard, current_dashboard
  end

  def clear_dashboard
    @request.host = "local.host" 
    assert_nil current_dashboard, 
               "There is a current dashboard when there shouldn't be: #{current_dashboard}" 
  end

  def current_dashboard
     Dashboard.find(:first, :conditions => ["subdomain = ? and subdomain IS NOT NULL",dashboard_subdomain])
  end

  def dashboard_subdomain
    @request.subdomains.first
  end
5. Add some Functional Tests

# failure - invalid subdomain
@request.host = "invalid.local.host" 
get :index
assert_redirected_to :controller => 'home'
assert_not_nil flash[:warning]

# success
dashboard_setup
get :index
assert_response :success
assert_template "dashboard/index" 

6. Implement the functionality in our controllers


class ApplicationController < ActionController::Base
  include DashboardLocation
  ...

7. Verify the tests pass

8. Simulate the environment on your development machine.

Alter your “hosts” file to re-route domain lookups to your local machine (127.0.0.1).

On Mac OSX and Linux:


$ sudo vi /etc/hosts

On Windows:


C:\WINDOWS\SYSTEM32\DRIVERS\etc\hosts 

Add an entry for your application domain (i.e. heartbeathq.com):


127.0.0.1 heartbeathq.com
Add entries for any subdomain you want to test:

127.0.0.1 highgroove.heartbeathq.com
127.0.0.1 rubyonrails.heartbeathq.com

Refresh and clear your cache to make sure lookups are re-routed:

On Mac OSX:

Issue the command in the terminal:


$ sudo lookupd -flushcache

On Linux:

Linux is pretty good about reading your /etc/hosts file, but to be on the safe side, restart ncsd if it’s running:


$ sudo /etc/init.d/ncsd restart

On Windows:

Issue the command at the command prompt:


C:\>ipconfig /flushdns

You’ll now be able to access your application through a pretty URL like: http://highgroove.heartbeat.com.

9. Don’t forget…
  • If you’re like me, you may be dealing with 2 records – a record authenticated via the subdomain and a user authenticated and placed in the session. Don’t forget to ensure that the user in the session has access to the subdomain record.
  • Remove the entries we added to /etc/hosts before your site launches.
  • Setup your real DNS server with a CNAME or other wildcard entry and your webserver of choice with the same wildcard mapping for your application.

That’s it! It’s another “why I love Rails” moment – subdomain-as-account-key functionality in 30 lines of code tested and ready for production.

P.S. Heartbeat, our Ruby on Rails control panel built during RailsDay 2006, is getting ready to emerge with some very powerful new features. Videos to come late this week.

Comments

  1. Benjamin said 1 day later:

    Using the subdomains as a key is definetely clever. It may not have a huge variety of applications because it is not needed all too often, but I can see the value in doing it as an alternate in some specialty cases. I’d be interested in hearing what applications you’d use it in. – ben @ http://rubyonrailsblog.com/

blog comments powered by Disqus