Rails, OpenID, and Acts as Authenticated

5 Mar 2007

This weekend I added OpenID to a Rails application for the first time, and this blog post describes the steps I took to integrate OpenID with Acts as Authenticated for account creation and access.

First I installed David’s OpenID Rails plugin (as discussed at David’s blog) into my application which was already using AAA to handle account creations and logins. I then created the following migration to add the OpenID identity URL to my user model:

class AddOpenId < ActiveRecord::Migration
  def self.up
    add_column :users, :identity_url, :string
  end

  def self.down
    remove_column :users, :identity_url
  end
end

And I changed the User model to allow accounts to be created either with login/email/password or with only an identity url (only changed lines are listed):

class User < ActiveRecord::Base
  validates_presence_of :login,
    :email, :if => :not_openid?
  validates_length_of :login,
    :within => 3..40, :if => :not_openid?
  validates_length_of :email,
    :within => 3..100, :if => :not_openid?
  validates_uniqueness_of :login, :email, :salt, :allow_nil => true

  def password_required?
    not_openid? && (crypted_password.blank? or not password.blank?)
  end
 
  def not_openid?
    identity_url.blank?
  end
end

This allows me to create User records without the usual required fields as long as the user created the account via an OpenID login.

And finally, the controller changes:

class AccountController < ApplicationController
  def login
    if using_open_id?
      open_id_authentication
    elsif params[:login]
      password_authentication(params[:login], params[:password])
    end
  end

  protected
 
    def password_authentication(login, password)
      if self.current_user = User.authenticate(params[:login], params[:password])
        successful_login
      else
        failed_login("Invalid login or password")
      end
    end
 
    def open_id_authentication
      authenticate_with_open_id do |result, identity_url|
        if result.successful?
          if self.current_user = User.find_or_create_by_identity_url(identity_url)
            successful_login
          else
            failed_login "Sorry, no user by that identity URL exists (#{identity_url})"
          end
        else
          failed_login result.message
        end
      end
    end

  private
 
    def successful_login
      redirect_back_or_default(index_url)
      flash[:notice] = "Logged in successfully"
    end

    def failed_login(message)
      redirect_to(:action => ‘login’)
      flash[:warning] = message
    end
end

That’s it! You can see it in action at the Rails plugin directory.

Update
I updated this code to match the plugin changes that were made between the time I installed the plugin and the time I posted this entry. :)

Update 2
I made another change to the code based on Geoff’s comment. Thanks, Geoff!


Actions

Informations

14 responses to “Rails, OpenID, and Acts as Authenticated”

Chris (12:15:16) :

Nice post. We’ve been looking into the OpenID thing and it definitely looks like it’s a simple enough thing to add.

Leancode » OpenIDAuthentication tutorial (13:46:10) :

[...] This morning, Ben Curtis put up an excellent, short tutorial on using this plugin on an existing site. [...]

Bernie Thompson (21:56:23) :

Ben,

With the latest rev of the DHH plugin (6334), did you hit this error when processing openid complete?

NoMethodError (You have a nil object when you didn’t expect it!
The error occurred while evaluating nil.downcase):
.//vendor/plugins/open_id_authentication/lib/open_id_authentication.rb:43:in `normalize_url’
.//vendor/plugins/open_id_authentication/lib/open_id_authentication.rb:62:in `normalize_url’
.//vendor/plugins/open_id_authentication/lib/open_id_authentication.rb:95:in `complete_open_id_authentication’

I may have messed something up in my config…

Bernie

Ben (22:17:46) :

Yes, Bernie, I ran into a problem with this changeset which changed how the results were returned in complete_open_id_authentication. In my copy of the plugin I changed those yields back to just returning the symbols to match what my controller code was expecting.

Bernie Thompson (23:42:27) :

Thanks, Ben! I reverted the same. Took me a while to figure out what was going on, made worse missing else clause in the README recommended code

def open_id_authentication
authenticate_with_open_id do |result, identity_url|
case result
when :missing
....

Which caused the mismatch result codes to fall through. Thanks again.

noin (14:36:08) :

I just added “attr_reader :code” to the beginning of OpenIdAuthentication::Result and used result.code in the case statement.

7 OpenID Resources for Rails Developers (08:46:59) :

[...] 3) Ben Curtis demonstrates how to tie in OpenID with the popular Acts As Authenticated plugin. [...]

Bob (15:01:24) :

In the AccountController, where is using_open_id? defined? I’ve tried to follow along with your example but Rails dies with unknown method.

FWIW, I’ve been trying for weeks to combine OpenID authentication with a straightforward role-based authorization scheme (e.g. the Authorized plugin.) The OpenID guestbook at http://rorek.org/blog/Simple_Rails_OpenID_Guestbook got very close as did http://identity.eastmedia.com/identity/show/Bookmarks+Demo+Application but neither consider roles. Backfitting even simple RBAC into the demo apps has been prohibitively painful.

Ben (15:04:17) :

@Bob:

It’s defined in the plugin.

sol (04:08:38) :

does anyone here know how one would do the registration for this?
DHH in his example with registration, checks for the identity_url in the user table, and if found, adds the user info.
So the users have to register their url before? I don’t really understand this.
Wouldn’t it be better to just add the identity if it is not existing, and update the details, on login if it is found?

Geoff (15:05:24) :

Instead of messing with the plugin code you can boil that whole case statement down if you use the OpenIdAuthentication::Result#successful? or OpenIdAuthentication::Result#unsuccessful? methods. Here is how I my open_id_authentication method looks.

I don’t know if this will layout right in the comment. Ben feel free to fix it for me ;)

def open_id_authentication

  authenticate_with_open_id do |result, identity_url|

    if result.successful?

      if self.current_user = User.find_or_create_by_identity_url(identity_url)

        successful_login

      else

        failed_login "Sorry, no user by that identity URL exists (#{identity_url})"

      end

    else

      failed_login result.message

    end

  end

end

Thanks for the guide. Helped me integrate it with the RestfulAuthentication plugin.

Ben (07:57:51) :

@Geoff

Thanks for the tip. I have updated the code in the post.

Nicolás Orellana, Entre viajes y Orelworks! » Blog Archive » Todo lo que tienes que saber sobre OpenID (15:47:48) :

[...] Si trabajas en Rails existen muchos recursos e incluso una gema para lograrlo y también de como ajustarlo al famoso acts_as_authenticated. [...]

Bob (20:11:59) :

Ah – I missed all the hidden steps of installing the open_id_authentication plugin, running migrations, setting index_url, etc. It’s still not clear in the code how one signs up with an OpenID URL – it takes more than simply adding an openid_url field in signup.rhtml

Thanks for clarifying this a bit; it’s just demoralizing trying to tie together OpenID, one of a half-dozen authentication plugins/engines/schemes, and a straightforward authorization system. As usual, what ought to be a simple matter of installing a turnkey auth scheme is taking far more effort than the entire rest of the application. This has been true each of the three times I’ve attempted a Rails application and I don’t see things getting better overall…

If I can get some cooperation from the Austin on Rails group, I’ll solve this problem for the next 6 months (until core Rails changes enough to deprecate key components without providing replacements – I’m thinking of UserEngine…) and write it up so I’m not accused of pointless bitching. :)