Authenticate like SSO with ActiveResource

Courtenay : July 18th, 2008

When you have multiple Rails applications that don’t share a common database and you want to share the user authentication information – or rather, use one app to provide authentication for another – there are a few options. Here’s how I solved it recently. This is the simplest way I could think of to get this working. I couldn’t find a plugin to do this, so here’s the result of my pdi.

Effectively what we’re doing is separating the user’s data - their profile info, if you like - from the credentials, and moving the latter to ActiveResource. This is something you should do in your own apps. Too frequently we stuff a bunch of data (like full name, phone number) into the user model, because it’s there. A more advanced version of this code might use the ‘profile’ as the resource name, updating the local profile with data from remote, and keeping User as a pure credential model.

Let’s assume we have App A which will act as the authenticator master. Our other application, App B, will still hold a User record, but we’ll override the authenticate method to use ActiveResource. We’ll also store some other fields like username and email, and will grab those each time the user logs in. That way, they can set an auth token in App A and they can login from cookies in app B (provided the cookie domain is shared).


class User < ActiveRecord::Base

  class Auth < ActiveResource::Base
    self.site = "http://app-a.com"
    self.format = :json
    self.element_name = 'user' # this is the name of the resource in your app
  end

  def self.authenticate(login, password)
    Auth.user = login
    Auth.password = password

    # Authenticating against the app will actually 'prove' the login/pass details.
    # We also want the user's details so we can cache them here.
    authed = Auth.find :first, :params => { :login => login }
    return false unless authed

    # Now, pull the data from remote and store it locally.
    user = User.find_or_initialize_by_login(login)
    user.attributes = authed.attributes
    user.save!
    user.activate!
    user

  rescue ActiveResource::ClientError # 406 error -- bad username/password.
    false
  end

Interestingly enough, find first actually runs the ‘index’ action, and returns the first record. sigh

Now, in your App A: users_controller, you want to set up a filter in the index like so:


  def index
    if params[:login]
      # for single-sign-on.
      @users = User.find(:all, :conditions => { :login => params[:login] })
    else
      @users = User.paginate(:all, :page => params[:page]) #... 
    end

    respond_to do |format|
      format.html
      format.json { render :json => @users }
    end
  end

Do you have a better way of doing this?

9 Responses to “Authenticate like SSO with ActiveResource”

  1. Pratik Says:

    CAS works fine. And Users won't have to login again and again when they change the app. Plus, you also get single click logout.

    What you've done is not really SSO. It's more like "oh hey, same username/pass will work everywhere"

  2. Nicholas Henry Says:

    HTH,

    http://www.infoq.com/news/2008/07/gehtland-security-and-identity

    This is the route I would take.

  3. Jonas Nicklas Says:

    It's been a while since I last used ActiveResource, but I'm pretty sure it still works the same way. If you want your request to route to the show action in the backend, you'll need to do something like this:

    authed = Auth.find(login)
    

    And then you can get it through params[:id] on the other side.

    /Jonas

  4. Chris Flipse Says:

    I've been tinkering with something similar for the last month or so, since seeing Justin Gehtland's talk at RailsConf. I'm not interested in using CAS, because, well, I'd rather not set up a Java server if I don't have to.

    I've done the bit about delegating the user model via ActiveResource, and gone one further.

    If all the applications you're concerned with are hanging off the same domain, you can skip the multiple signon issue by telling your app to stash an authentication token cookie at the top of your domain structure. That cookie will get sent to every subdomain, and you can pull the auth information from that.

    Of course, that breaks if the user has cookies disabled...

  5. Pratik Says:

    If you google, you'll find a CAS server written in camping. It'll be about a day work to convert that into a rails app. I've done it, but cannot OSS it.

  6. Jeff Says:

    You might try rubycas-server. I got it working just fine on a centos vm for testing purposes...never got the change to implement it in production. (http://code.google.com/p/rubycas-server/wiki/HowToConfigure)

  7. Stefan Says:

    This way you still have to provide a login-page and deal with signo-ins from multiple sites, registration, login-errors and so on - so it does not really feel like SSO. I found CAS to be quite an overkill, especially if you already have an existing user-management. There is a poor-mans SSO that uses a simple sequence of redirects to get a logged in user: http://www.vierundsechzig.de/blog/?p=25

    We're currently using this with a fat website in the middle and some satelite applications that delegate to the central website - this is easier than CAS but still get's completely rid of all login-related tasks in your client websites....

  8. Manfred Says:

    We use a request to /users/me to authenticate someone. Something like this:

    User.user = login User.password = password begin User.get(:me) rescue ActiveResource::UnauthorizedAccess nil end

    Unfortunately ActiveResource doesn't instantiate records on non-standard resources, so you will have to hack around that a little bit.

  9. Adam Sanderson Says:

    I'll also vote for RubyCAS, it took a little hacking, but we were able to hook it up to Beast so that our forums could use the same login credentials as the application.

Sorry, comments are closed for this article.