Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

Web Development

OpenID Single Sign-On


Issues

But there is a chicken-and-egg problem: Many website developers choose not to support OpenID because there aren't enough users who understand or use it—and many users don't understand or use OpenID because few websites support it. However, OpenID support and adoption appears to be growing, and there is a growing base of web users who prefer OpenID to traditional authentication.

There are potential risks associated with OpenID as well, such as losing the "keys to the kingdom." If an OpenID password is compromised, a user's online identity and any data on consumer websites may also be compromised. For this reason, I recommend you choose an identity provider you trust, or configure your own identity provider. Also, users who use the same username/password on multiple apps (often including their e-mail account) expose themselves to even greater risks.

One way a user's identity can be compromised is through phishing, such as when a nefarious website claims to support OpenID but only provides a fake authentication form to collect the identity URI and password of unsuspecting users. A web browser with anti-phishing features can help, and I recommend visually confirming that the web address of the page where you enter your password matches that of your identity provider, especially when visiting an unfamiliar site.

The centralized identity provided by OpenID can be a drawback depending on the user's agenda, but users who prefer to remain anonymous can use a different identity URI. Also, since a centralized third party performs authentication for potentially many different websites, identity providers can be aware of some of the websites users visit. Again, choosing an identity provider you trust can alleviate this concern.

An OpenID-Enabled Ruby on Rails Application

The example I present here demonstrates how an OpenID consumer can be implemented with the Ruby on Rails framework. Libraries that make it easy to enable OpenID in web applications are also available for Java, C#, PHP, Python, and other languages and frameworks.

To illustrate, I assume you've installed Ruby 1.8, RubyGems, Rake, Rails 2.0 (or later), a database system (MySQL, PostgreSQL, or SQLite, for instance), and the adapter for your database.

I also assume you are familiar with Ruby and Rails and UNIX-style command-line interface and text editors. I indicate command-line actions with a dollar-sign ($) prefix, which should not be typed as part of the command. The complete source for this project is available online; see "Resource Center," page 5.

First, install the ruby-openid gem (this example was tested with version 2.0.4):


$ sudo gem install ruby-openid

Next, create a Rails application at the command line, and perform a bit of cleanup:


$ rails openid_demo
$ cd openid_demo
$ rm public/index.html

David Heinemeier Hansson, the creator of Rails, wrote a plug-in (github.com/rails/ open_id_authentication/tree/master/README) that uses the ruby-openid gem and makes it easy to add OpenID support to Rails applications, so I install it now:


$ ./script/plugin install open_id_authentication 

Once the plug-in is installed, run this Rake task to create a database migration:


$ rake open_id_authentication:db:create

If you're curious, take a look at the migration that was created. It creates two database tables, "open_id_authentication_associations" and "open_id_authentication_nonces", which store information about the messages received from OpenID identity providers, including authentication keys.

Next, generate a User model with a few basic attributes:


$ ./script/generate model user identity_url:string email:string full_name:string date_of_birth:date

This generates a migration to create a table to store users. It's a good idea to add an index on the identity_url column because it can fetch data. To do so, add this line to the end of the up class method in the create_users migration:


add_index :users, :identity_url, :unique => true

Configure a development database in config/database.yml and run this Rake task to create the development database and run the migrations:


$ rake db:migrate

Next, create a session controller, which handles login (with OpenID, of course) and logout for your users:


$ ./script/generate controller session new

Open app/controllers/session_controller.rb in your text editor or IDE and implement the create and destroy actions (Example 1). The destroy action stands on its own, but the create action delegates to another method, open_id_authentication (Example 2). This method has lots of responsibility, so let's examine it:


if params[:openid_url].blank? && params[:open_id_complete].blank?
  return failed_login("Please enter your OpenID")
end



def create
  open_id_authentication
end
def destroy
  reset_session
  self.current_user = nil
  flash.now[:notice] = 'You have logged out.'
  render :action => 'new'
end

Example 1: Creating and destroying sessions.

def open_id_authentication
  if params[:openid_url].blank? && params[:open_id_complete].blank?
    return failed_login("Please enter your OpenID")
  end
  @openid_url = params[:openid_url]
  # Pass optional :required and :optional keys to specify what sreg fields 
  # you want. Be sure to yield registration, a third argument in the block.
  authenticate_with_open_id(@openid_url, 
      :required => [:email],
      :optional => [:dob, :fullname]
      ) do |result, identity_url, registration|

    if result.successful?
      user = User.find_by_identity_url(identity_url)
      if user.nil?
        user = User.new
        user.identity_url = identity_url
      
        unless assign_registration_attributes(user, registration)
         return failed_login("Your OpenID registration failed: " +
            user.errors.full_messages.to_sentence)
        end
      end
      self.current_user = user
      successful_login
    else
      failed_login(result.message || "Sorry, could not authenticate 
        #{identity_url}")
    end
  end
end

Example 2: OpenID authentication.

The aforementioned condition checks that users entered an identity URI, which is accessed through params[:openid_url], where openid_url is the name recommended for the HTML form element where users enter their identity URI. (In Rails, the blank? method returns True for either a nil value or empty string.)

The other check, for a blank params[:open_id_complete] value, is needed because identity providers redirect back to this action after authentication is done with this complete flag set, but without the openid_url parameter.

The failed_login method (Example 3) adds the error message to the response and renders the login form:


authenticate_with_open_id(@openid_url, 
    :required => [:email],
    :optional => [:dob, :fullname]
    ) do |result, identity_url, registration|



(app/controllers/session_controller.rb)

def failed_login(message)
  flash.now[:error] = message
  render :action => 'new'
end

Example 3: Handling a login failure.

This method, authenticate_with_open_id, is provided by the open_id_authentication plug-in you installed earlier. Simply pass in the identity URI entered by users and specify any required or optional fields, as well as a Ruby block that handles the result. The plug-in does the heavy lifting—it takes care of all communication with the identity provider.

The required and optional fields used in this example—e-mail, date of birth, and full name—are a subset of those defined by the Simple Registration ("sreg") extension, which gets some basic, commonly used information about users from their identity providers. Other sreg attributes include nickname, language, timezone, postcode, gender, and country.

The method passes a result object, a clean version of the identity URI, and a hash of sreg attributes into the block:


if result.successful?
  user = User.find_by_identity_url(identity_url)

A successful result indicates that users have been authenticated. Find the user in your database by his identity URI (taking advantage of that index you added earlier):


if user.nil?
  user = User.new
  user.identity_url = identity_url
  unless assign_registration_attributes(user,       registration)
    return failed_login "Your        OpenID registration failed: " +
  user.errors.full_messages.to_sentence
  end
end

If you didn't find a user with the authenticated identity URI, create a new User object, set the identity URI, and assign any sreg attributes that were returned. Example 4 shows the method assign_registration_attributes; note that it saves the new User object to the database, performing any validation defined in the model:


   self.current_user = user
successful_login



# Maps OpenID sreg keys to fields of your user model.
# registration is a hash containing valid sreg keys
def assign_registration_attributes(user, registration)
  { 
    :email => 'email', 
    :date_of_birth => 'dob', 
    :full_name => 'fullname' 
  }.each do |model_attr, registration_attr|
    unless registration[registration_attr].blank?
      user.send "#{model_attr}=", registration[registration_attr]
    end
  end
  user.save
end

Example 4: Assigning sreg attributes to new users.

At this point, you have a valid user (either found or created). The user should have a session so that the application knows who he is when he submits another HTTP request. A common approach is to manage a current_user object in ApplicationController (Example 5). Reward the user's successful login by calling successful_login (Example 6), which simply redirects to WelcomeController#index.

class ApplicationController < ActionController::Base
  helper_method :current_user
  protected
  # Accesses the current user from the session.
  def current_user
    @current_user ||= 
      (session[:user_id] && User.find_by_id(session[:user_id]))
  end
  # Store the given user in the session.
  def current_user=(new_user)
    session[:user_id] = new_user && new_user.id
    @current_user = new_user
  end     
end

Example 5: Managing the current user's session.

def successful_login
  redirect_to :controller => 'welcome'
end

Example 6: Handling a successful login.

Authentication can be unsuccessful for a variety of reasons, including an invalid identity URI, if users canceled, or if they failed to enter the correct password. Handle an unsuccessful result by passing the result's message to failed_login:


else
  failed_login result.message || "Sorry, could not authenticate 
    #{identity_url}"
end

The next step is to build a login form like Figure 1. At a minimum, you need a form with a text field named openid_url and a Submit button that posts to the session/create action (Example 7) in app/views/session/new.html.erb.

[Click image to view at full size]

Figure 1: Login form.

<% form_tag session_path do -%>
<dl class="form">
     <dt><label for="openid_url">OpenID</label></dt>
     <dd><%= text_field_tag "openid_url", @openid_url, :size => 30 %></dd>
     <dd><%= submit_tag 'Log in', :disable_with => "Logging in…" %></dd>
</dl>
<% end -%>

Example 7: Login form.

Your users will probably expect to land somewhere after they log in, welcoming them to the app. The SessionController#successful_login method redirects users to "welcome/index", so create the controller:


$ ./script/generate controller welcome index

Next, update the welcome template (Example 8) to give users a slightly personalized page.


<p>
     Your OpenID identity URL is 
     <strong><%= current_user.identity_url %></strong>.
</p>

<p>
     <%= link_to 'Log out', logout_path %>
</p>

Example 8: Welcome!

You just need to map some routes, including one named open_id_complete to catch the responses from identity providers. Open config/routes.rb in your text editor and add the routes in Example 9, then start your Rails app:


$ ./script/server



ActionController::Routing::Routes.draw do |map|
  map.login  'login',  :controller => 'session', :action => 'new'
  map.logout 'logout', :controller => 'session', :action => 'destroy'
  # You can have the root of your site routed with map.root -- 
  # just remember to delete public/index.html.
  map.root :controller => 'welcome', :action => 'index'
  map.open_id_complete 'session', :controller => 'session', 
    :action => 'create', :requirements => { :method => :get }
  map.resource :session
  # Install the default route as the lowest priority.
  map.connect ':controller/:action/:id.:format'
  map.connect ':controller/:action/:id'
end

Example 9: Routes.

Go to http://localhost:3000/login in your web browser and you should see a simple login form. If you have an OpenID, you should be able to log in!

This isn't the prettiest app ever. I didn't build a custom layout with any colors or images, and there's little value in the User model since it's only a copy of some data returned by identity providers. Again, the complete (and better looking) project is available online.

Registration is transparent in this example. Users are automatically "registered" the first time their identity URI is encountered by the app. That may not be right for your application. A few other common strategies for registration include:

  • Prompt new users for additional information, required or not, to complete registration.
  • Require up-front registration to ask for additional information, where OpenID authentication is the final step.
  • Require up-front registration where OpenID is optional, supplementing another type of authentication, such as traditional username/password login. (If you do this, try to leverage an established authentication library for your language or framework. At a minimum, store salted hashes instead of plaintext passwords!)

Conclusion

OpenID is a straightforward way to add authentication to your web application, whether it's built with Ruby on Rails or another framework. OpenID is still evolving. New features are being standardized, new extensions are being developed, identity providers are continuously improving their support, and more applications support it everyday, either as the primary means of authentication or to supplement another method.


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.