Multi-domain single-signon October 3rd

Single-signon is such a common feature request for many site networks. And while frequent at that, the powers that be impose difficulties on this task that make it a daunting experience for every web developer. In this piece, I’m outlining a practical approach for a hosted Rails application.

Revisiting the definition of “single-signon” we’ll declare that this means:

  1. A user has to be able to login with the same credentials across multiple sites
  2. For convenience, we want a user to be logged in on site B if he had logged in on site A before
  3. If a user logs out of a single site, we want him to be logged out in all other sites as well

So what is it that makes this such a tedious endeavor? Cookies.

Yes, we all knew back in 1990 that cookies were evil. But they’re differently evil at that this time.

Well, the evilness is only half the deal, because obviously, the implementation had security in mind when they decided that a cookie set by site A is only ever going to be transmitted back to site A, to something in the same domain at the very least. At that makes a ton of sense for a single site. In our case, though, it causes pain and grief.

Redirection to the rescue.

In the concept outlined here we’ll make use of a central point of cookie distribution in addition to local cookies being set by the sites themselves.

Since the collection of sites I implemented the following for serves all users from the same hosting cluster and even from the same user database I’ll skip the part of the actual synchronization of user databases or intersecting user databases as an exercise for the reader and focus on the handling of keeping a user logged in even if he bounces between sites.

First of all, we need a hook into all of our controllers since we have to make sure we recover a logged in user session from any potential entry point into our sites. This is best fitted into a before_filter method within ApplicationController.


class ApplicationController < ActionController::Base
  before_filter :sso
  def sso
    return if controller_name == "sso"

    unless session[:sso_verified] and cookies[:sso_token] and
    Token.current_token = Token.find_by_token(cookies[:sso_token])
      Token.current_token = Token.create(
        :token => new_sso_hash,
        :uri => request.request_uri,
        :portal => Portal.current_portal
      )

      redirect_to "#{Portal.find_by_sso(1).domain}" <<
        "/sso/verify/#{Token.current_token.token}" and return false
      end
    end
  end
end

What does this do?

First of all, we’re treating the single-signon process separate from the regular Rails sessions using a separate Token model, the code of which I’ll show in a moment.

A couple of measures ensure that a user is redirected to our central cookie site (for example login.mysite.net) upon his first hit onto our member site. That way we can check for a potentially existing user session on one of the other sites. We check for a flag within a user session session[:sso_verified], check the contents of an explicit sso_token cookie and try to fetch a token record from the database using that cookie’s contents.

If any of those fail, we create a new Token using a hashing method, redirect to the central site, specifically to its SsoController, to verify the existence of other sessions for the current browser and a little more.

In case the single-signon token is actually valid and contained in the database, you should populate the session with the logged in user record. In our application, we use User.current_user. Your mileage may vary.

The aforementioned central point of cookie distribution is setup as just another site in our network. All it’ll do, though, is act upon requests to the SsoController.


class SsoController < ApplicationController
  def verify
    Token.current_token = Token.find_by_token(params[:id])

    if cookies[:sso_token] and
    existing_token = Token.find_by_token(cookies[:sso_token])
      Token.current_token = existing_token.adopt_params(Token.current_token)
    end

    Token.current_token.cleanup

    set_sso_cookie
    redirect_to "#{Token.current_token.portal.domain}" <<
      "/sso/recovery/#{Token.current_token.token}"
  end
end

The verify method (now invoked on the central site) takes the passed hash value, fetches the token created in the filter from the database, checks for the presence of an sso_token cookie (again, on the central site – we would not have seen that cookie previously in the filter since we’ve been on a different domain) and fetches another token from the database in case that cookie is populated, the latter potentially containing a valid user session.

If an existing session is found, it is preferred over the newly created Token for obvious reasons. However, we’re taking the URI from the previous request to properly redirect the user back to whence he came from.

Lastly, the user is being redirected back to the originating site, again to the SsoController, to set a local cookie.


class SsoController < ApplicationController
  def recovery
    session[:sso_verified] = true
    Token.current_token = Token.find_by_token(params[:id])
    set_sso_cookie
    redirect_to Token.current_token.uri
  end  
end

In the recovery method we’re starting out by setting the verification flag in the user session. We then fetch a Token from the database using the hash passed by the central site and set the local sso_token cookie (which is, in total, cookie number 4 transmitted from our sites (a session cookie and an sso_token cookie each for the originating site and the central site)) and redirect the user back to his original entry point to the originating site.

As I mentioned, we’re not going to pollute or interfere with the regular Rails sessions. We’ll create a separate model for the authentication tokens needed for the single signon process.


class Token < ActiveRecord::Base
  belongs_to :user
  belongs_to :portal

  cattr_accessor :current_token

  def adopt_params(adoptee)
    self.uri = adoptee.uri
    self.portal = adoptee.portal
    self
  end

  def cleanup
    return if self.user.nil?
    self.class.destroy_all [ "id != ? AND user_id = ?", self.id, self.user_id ]
  end
end

This code should barely need any explanation as it’s been used by earlier snippets. The cleanup method makes sure only a single Token for a given user exists.

The open spots in this implementation mainly center around associating Token.current_token with a user account as a user logs in and remove that association upon logging out (via a controller method generated via the login generator for example).

Other spots have purposely been left out since they’re too specifically bound to my own domain model but should not be hard to fill in for the experienced reader. If you do have questions though, don’t hesitate to ask in the comments.

Filed Under: Rails

5 comments

Jump to comment form
  1. surapong 10.04.05 / 23PM

    How did you insert the code above into the post?

  2. scoop 10.04.05 / 23PM

    The post uses the typo syntax highlighter. It’s all built-in.

  3. dzsii 10.05.05 / 14PM

    is there any rails specific in the method, or it can be implemented in other languages, for example php? I mean, if you only have to set and read cookies, redirect and store data in db (sessions and user status) it should be.

    Would you be so kind, and write down the steps of the logic whitout the rails thing?

  4. blaine 10.06.05 / 23PM

    This, as implemented, is susceptible to man-in-the-middle attacks, and hijacking of identity etc. If you’re going to do it this way, and there is any important data on any of the sites involved, REQUIRE an SSL connection.

  5. scoop 10.07.05 / 00AM

    Every cookie based session identity actually is susceptible to that. Can you outline how this would be any different in this case?


Archives

  • 133Home
  • 1About
  • 1Git
  • 15Apple
  • 5Conferences
  • 14Development
  • 1limited overload
  • 4Misc
  • 10Personal
  • 3Photography
  • 7poocs.net
  • 40Rails
  • 22Software
  • 16Web

gipoco.com is neither affiliated with the authors of this page nor responsible for its contents. This is a safe-cache copy of the original web site.