OpenID Single Sign-On

OpenID is an open standard that defines a way that web-based applications can authenticate users via a single identity.


September 23, 2008
URL:http://www.drdobbs.com/web-development/openid-single-sign-on/web-development/openid-single-sign-on/210603354

Jeremy is a software engineer at PatientsLikeMe (www.patientslikeme.com), a social network and health-management tool. He can be contacted at [email protected].


Many software applications—including websites—offer features that are only available to users who first log-in to the system. In a typical application, user identity is confirmed ("authenticated") upon entering the username and password for the account. This approach is straightforward and works, but there are drawbacks. For instance, most apps reinvent the wheel when it comes to authentication, and many do-it-yourself implementations are easily exploited because developers may not have expertise in security. Users are usually required to remember a username/password for every application they use, which can be problematic.

OpenID (www.openid.net) is an open standard that defines a way that web-based applications ("consumers") can authenticate users by delegating the responsibility of authentication to identity providers. With OpenID, users have a single identity that can be used on any OpenID-enabled application, and they only need to remember one password.

In this article, I describe the OpenID authentication system and show how a web application built with Ruby on Rails can use OpenID to authenticate its users.

How OpenID Works

OpenID relies on the HTTP protocol to exchange messages between "consumers" and "identity providers." Consider a user named Bob, whose identity provider is myOpenID (www.myopenid.com). Bob uses the Drupal content-management system that happens to be an OpenID consumer. Here's the general workflow when Bob logs into his Drupal account through OpenID:

  1. Bob visits his Drupal-driven website.
  2. Bob enters his OpenID identity URI, "bobs-id.myopenid.com," in the login form and clicks a Submit button. That's his OpenID identity URI, which looks like a website address, but identifies Bob and his identity provider.
  3. Bob's web browser is redirected to a web page served by myOpenID, where he is prompted for his password.
  4. Bob enters his OpenID password and clicks a Submit button.
  5. myOpenID confirms Bob's password, and his web browser is redirected to Drupal with some information about Bob.

Bob entered his password only on the identity provider website, and never on the consumer website. The user's password is not shared with the consumer and only needs to be submitted once by users to the identity provider, preferably over a secure connection.

Benefits

OpenID provides several benefits to users and developers. Users only need to remember one username (their identity URI) and password to access multiple applications. With a simple cookie and Remember Me checkbox, an OpenID identity provider can act as a convenient Single Sign-On (SSO) solution for someone who uses multiple OpenID consumers.

OpenID identity providers are responsible for the authentication of users, giving web developers one less thing to understand, implement, and maintain, and letting them focus on core business. Supporting OpenID removes a barrier to entry for many potential users who balk at yet another website asking them to sign up.

It's not uncommon for web apps to offer OpenID support as an alternative to traditional authentication methods, but letting experts handle password security reduces the risk of accounts being compromised.

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:

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.

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.