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 itand 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
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
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
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 liftingit takes care of all communication with the identity provider.
The required and optional fields used in this examplee-mail, date of birth, and full nameare 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
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
def successful_login redirect_to :controller => 'welcome' end
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.
<% 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 -%>
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>
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
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.