open_id_authentication (part 2)

In part 1, we looked at some changes to DHH’s OpenID plugin for rails. Now we look at the app’s authenticated sessions controller, which uses that library code. The goals were:

  • Support both OpenID and password authentication
  • Support all the usual login goodies, including before_filter :login_required, remember me functionality (with cookies), HTTP basic authentication, etc.
  • Get this by integrating the restful_authentication plugin/generation and open_id_authentication plugin

First, I installed both plugins and ran the restful_authentication generator

./script/generate authenticated user authenticated_sessions

I chose the name “authenticated_sessions” to distinguish from the built-in (non-authenticated) Rails sessions functionality.

routes.rb adds

  map.resource :authenticated_session, :member => { :complete => :get }
  map.resources :users

Then it’s a matter of some substantial edits to the generated app/controllers/authenticated_session_controller.rb Here’s where it would be nice to have a combined generator like Eastmedia’s. If you’re just trying to get an app and and running, you don’t want to be reading a blog posting like this. :)

The smartest design decisions in the file below (which all came from DHH’s code), are having centralized successful_login(), failed_login(), and destroy() functions, which are common across the otherwise separate openid/password authentication paths.

# This controller handles the login/logout function of the site. 
# File created with restful_authentication generator 
class AuthenticatedSessionController < ApplicationController
 
  # render new.rhtml
  def new
  end
 
  # Added after restful_authentication generator, using code from http://www.loudthinking.com/arc/000604.html
  # Complete arrives to us via a browser redirect from the OpenID provider, which happens after create/begin.
  # Obviously only called in the OpenID path through authentication.
  def complete
    complete_open_id_authentication(params[:openid_url]) do |result, identity_url, sreg|
      case result
      when :canceled
        failed_login "OpenID verification was canceled"
      when :failed
        failed_login "Sorry, the OpenID verification failed"
      when :successful
        if self.current_user = User.find_by_openid_url(identity_url) || User.create(:openid_url => identity_url, :login => sreg['nickname'], :email => sreg['email'])
          successful_login
        else
          failed_login "Sorry, no user by that identity URL exists (#{identity_url})"
        end
      else
        failed_login "Unknown error logging in #{identity_url}"
      end
    end
  end
 
  # Handle the creation of a new authenticated session, OpenID or password authentication path
  def create
    if using_open_id?
      begin_open_id_authentication(params[:openid_url], :required => "nickname, email") do |result, identity_url|
        case result
        when :missing
          failed_login "Sorry, the OpenID server couldn’t be found"
        else
          failed_login "Unknown error in openid begin for #{identity_url}"        
        end
      end
    elsif params[:login]
      password_authentication(params[:login], params[:password])
    else
      failed_login "No valid credentials passed to create authenticated session"
    end
  end
 
  def destroy
    logger.info "logging out #{self.current_user.inspect}"
    self.current_user.forget_me if logged_in?
    cookies.delete :auth_token
    @session[:user] = nil
    flash[:notice] = "You have been logged out."
    redirect_back_or_default(calls_path())
  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
 
private
 
  def successful_login
    logger.info "sucessful login for #{self.current_user.inspect}"
    if params[:remember_me] == "1"
      self.current_user.remember_me
      cookies[:auth_token] = { :value => self.current_user.remember_token , :expires => self.current_user.remember_token_expires_at }
    end
    redirect_back_or_default("/")
    flash[:notice] = "#{self.current_user.login} Logged in successfully"
  end
 
  def failed_login(message)
    logger.info 'Login failed. ' + message
    flash[:notice] = message
    redirect_to new_authenticated_session_path()
  end
 
end

Lastly, views/authenticated_session/new.rhtml is where the initial form is presented when users are redirected from an action that requires authentication. I chose to put the password and openid authentication forms on the same page, but within their own divs with IDs and a common rounding class, so the styles can be tuned in the .css file to make it look nice.

<div id='password-login' class='rounded'>
	<% form_tag authenticated_session_path do -%>
	<p><label for="login">Login</label><br/>
	<%= text_field_tag 'login' %></p>
 
	<p><label for="password">Password</label><br/>
	<%= password_field_tag 'password' %></p>
	<p><label for="remember_me">Remember Me</label><%= check_box_tag 'remember_me' %></p>
	<p><%= submit_tag 'Log in' %></p>
	<% end -%>
</div>
 
<div id='openid-login' class='rounded'>
	<% form_tag authenticated_session_path do %>
	  <p><label for="openid_url">OpenID</label><%= text_field_tag 'openid_url' %></p>
	  <p><label for="remember_me">Remember Me</label><%= check_box_tag 'remember_me' %></p>
	  <%= submit_tag 'Login' %>
	<% end %>
</div>

You’ll notice that most of my postings and a lot of the plugins have headed in the direction of making authenticated sessions a REST resource, using the newer RESTful support in Rails. Reflecting back on it all, it isn’t the best conceptual or practical match. As long as the end-user functionality is the same (support for remember_me, HTTP basic authentication, etc.) — it seems more strightforward to implement authenticated sessions as a traditional controller, not a REST resource.

open_id_authentication (Part 1)

open_id_authentication is the OpenID consumer plugin for Rails, being written by DHH and some on the Rails core team.

This plugin was getting some quick updates at first, but things have slowed down as the 37signals crew has focused on launching the app they developed it for — highrise.

So, in the absence of other updates over the last few weeks, here are some changes I made for one of my own Rails apps. In part one of two posts, we look here at what changes were made on the library side. In part two, we’ll look at the the library client (rails controller) side.

On the left is http://svn.rubyonrails.org/rails/plugins/open_id_authentication lib/open_id_authentication.rb revision 6452.

On the right (or down below, if the browser isn’t wide enough) is the modified version used for my rails app.

60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
  protected
    def normalize_url(url)
      OpenIdAuthentication.normalize_url(url)
    end
 
    # The parameter name of "openid_url" is used rather than the Rails convention "open_id_url"
    # because that's what the specification dictates in order to get browser auto-complete working across sites
    def using_open_id?(identity_url = params[:openid_url]) #:doc:
      !identity_url.blank? || params[:open_id_complete]
    end
 
    def authenticate_with_open_id(identity_url = params[:openid_url], fields = {}, &block) #:doc:
      if params[:open_id_complete].nil?
        begin_open_id_authentication(normalize_url(identity_url), fields, &block)
      else
        complete_open_id_authentication(&block)
      end
    end
60
61
62
63
64
65
66
67
68
69
  protected
    def normalize_url(url)
      OpenIdAuthentication.normalize_url(url)
    end
 
    # The parameter name of "openid_url" is used rather than the Rails convention "open_id_url"
    # because that's what the specification dictates in order to get browser auto-complete working across sites
    def using_open_id?(identity_url = params[:openid_url]) #:doc:
      !identity_url.blank? || params[:open_id_complete]
    end
In these diffs, you see authenticate_with_open_id has been removed. The original plugin code on the left routes OpenID’s begin and complete operations through this single function and keys off an HTTP parameter to know which operation we’re really doing. In the implementation on the right, the client calls directly into the appropriate (and newly un-private) begin & complete methods, and we’ll now have to make sure to normalize the url in those functions, as necessary.
80
81
82
83
84
85
86
87
88
89
90
91
  private
    def begin_open_id_authentication(identity_url, fields = {})
      open_id_response = timeout_protection_from_identity_server { open_id_consumer.begin(identity_url) }
 
      case open_id_response.status
      when OpenID::FAILURE
        yield Result[:missing], identity_url, nil
      when OpenID::SUCCESS
        add_simple_registration_fields(open_id_response, fields)
        redirect_to(open_id_redirect_url(open_id_response))
      end
    end
70
71
72
73
74
75
76
77
78
79
80
81
82
83
 
    def begin_open_id_authentication(identity_url, fields = {})
      identity_url = normalize_url(identity_url)
      logger.info "beginning openid authentication for #{identity_url}"
      open_id_response = timeout_protection_from_identity_server { open_id_consumer.begin(identity_url) }
 
      case open_id_response.status
      when OpenID::FAILURE
        yield Result[:missing], identity_url, nil
      when OpenID::SUCCESS
        add_simple_registration_fields(open_id_response, fields)
        redirect_to(open_id_redirect_url(open_id_response, identity_url))
      end
    end
Notice the “private” is gone on the right — it’s moved down below the begin and complete methods. Otherwise nothing too exciting here. Just added the url normalization and a log message to help illuminate looking back on a failed or successful login.
93
94
95
96
97
98
99
100
101
102
103
104
105
106
    def complete_open_id_authentication
      open_id_response = timeout_protection_from_identity_server { open_id_consumer.complete(params) }
      identity_url     = normalize_url(open_id_response.identity_url) if open_id_response.identity_url
 
      case open_id_response.status
      when OpenID::CANCEL
        yield Result[:canceled], identity_url, nil
      when OpenID::FAILURE
        logger.info "OpenID authentication failed: #{open_id_response.msg}"
        yield Result[:failed], identity_url, nil
      when OpenID::SUCCESS
        yield Result[:successful], identity_url, open_id_response.extension_response('sreg')
      end      
    end
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
    def complete_open_id_authentication(identity_url)
      identity_url = normalize_url(identity_url)
      logger.info "completing openid authentication for #{identity_url}"
      open_id_response = timeout_protection_from_identity_server { open_id_consumer.complete(params) }
 
      case open_id_response.status
      when OpenID::CANCEL
        yield :canceled, identity_url, nil
      when OpenID::FAILURE
        logger.info "OpenID authentication failed: #{open_id_response.msg}"
        yield :failed, identity_url, nil
      when OpenID::SUCCESS
        yield :successful, identity_url, open_id_response.extension_response('sreg')
      end
    end
The Result[] class on the left was added in one of the last few changes, and broke the plugin (at least for me and Ben). So the code on the right just reverts back to the previous working code, which returns the result code directly as a ruby symbol. As the plugin gets updates from the core team, I’m sure this’ll get a fix.
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
 
    def open_id_consumer
      OpenID::Consumer.new(session, OpenID::FilesystemStore.new(OPEN_ID_AUTHENTICATION_DIR))
    end
 
 
    def add_simple_registration_fields(open_id_response, fields)
      open_id_response.add_extension_arg('sreg', 'required', [ fields[:required] ].flatten * ',') if fields[:required]
      open_id_response.add_extension_arg('sreg', 'optional', [ fields[:optional] ].flatten * ',') if fields[:optional]
    end
 
    def open_id_redirect_url(open_id_response)
      open_id_response.redirect_url(
        request.protocol + request.host_with_port + "/",
        open_id_response.return_to("#{request.url}?open_id_complete=1")
      )     
    end
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
private
 
    def open_id_consumer
      OpenID::Consumer.new(session, OpenID::FilesystemStore.new(OPEN_ID_AUTHENTICATION_DIR))
    end
 
    def add_simple_registration_fields(open_id_response, fields)
      open_id_response.add_extension_arg('sreg', 'required', [ fields[:required] ].flatten * ',') if fields[:required]
      open_id_response.add_extension_arg('sreg', 'optional', [ fields[:optional] ].flatten * ',') if fields[:optional]
    end
 
    def open_id_redirect_url(open_id_response, identity_url)
      open_id_response.redirect_url(
        request.protocol + request.host_with_port + "/",
        open_id_response.return_to("#{request.url};complete?openid_url=#{identity_url}")
      )     
    end

The openid_redirect_url function is where the interesting change is. Both left and right code contain a convention (as Rails is wont to do) on how the HTTP request is formatted to distinguish the OpenID complete operation from begin.

The code on the right uses an explicit, separate action instead of one action and a param switch to encode the complete. This is cleaner IMHO, and allows begin and complete to have distinct Rails routing signatures. For the code on the left, the Rails routing is

map.open_id_complete 'session', :controller => "session", :action => "create", :requirements => { :method => :get }

And for the code on the right, it’s

  map.resource :authenticated_session, :member => { :complete => :get }

Note, the code on the right is bad in that it doesn’t use the recommended path helper, so now, as of a recent edge rails change, the resource ‘;complete’ action should be ‘/complete’ (so I should use that helper!)

Why pass the identity_url from the begin operation to complete? Because in the case of link rel redirection, we want to make sure to have our identity url reflect what the user gave us (e.g. his blog URI), not what it later resolves to (the provider URI) — or else, we’ll break user’s ability to switch providers transparently with redirection.

There we go. Let me know which of my decisions were good, bad, or broken. And next, we’ll see what this looks like from the client perspective.

An OpenID screencast for developers

Simon Willison’s “How to Use OpenID” screencast did a lot of good — raising the awareness of OpenID and how it works for users. Today, the major bottleneck is getting more web applications to support OpenID logins.

Here’s a screencast and a set of resources targeted at that, with a focus on Rails but some general applicability.

http://leancode.com/openid-for-rails/

It’s not quite as polished as Simon’s and I tripped on a few things — e.g. OpenID Identifier — but check it out and feel free to forward it on to anyone who might want a deeper picture of how OpenID does what it does.

8 Steps to your first OpenID enabled app

openid_login_generator isn’t the only game in town for creating an OpenID-enabled Rails application, and it’s certainly not the best for real-world apps. But it is the simplest. The generator doesn’t even generate migrations for you, yet it’s only 8 steps to your first OpenID enabled app (ok, you had to install ruby, rails and sqlite3 first). Here’s a cheat sheet length tutorial.

$ rails your_app -d sqlite3; cd your_app
$ sudo gem install openid_login_generator -y
$ script/generate openid_login openid_account
$ ${EDITOR} app/controllers/application.rb
  require_dependency "openid_login_system"

  class ApplicationController < ActionController::Base
    include OpenidLoginSystem
    model :user
$ script/generate migration create_user_model
$ ${EDITOR} db/migrate/001_create_user_model.rb
  def self.up
    create_table :users do |t|
         t.column :o penid_url, :string
    end
  end

  def self.down
    drop_table :users
  end
$ rake db:migrate
$ script/server

Now browse to http://localhost:3000/openid_account/login and test your new openid login-enabled app. You’re good and ready to add “before_filter :login_required” to controllers to limit access. See the readme for more.

OpenIDAuthentication tutorial

DHH’s OpenIDAuthentication plugin for Rails is evolving and progressing quickly. Over the weekend, David updated the plugin to no longer assume an overloaded login form.

Removed open_id? as the idea of using the same input box for both OpenID and username has died — use using_open_id? instead (which checks for the presence of params[:openid_url] by default) [DHH - Revision 6631]

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

It’s wonderful having the creator of Rails working on OpenID. :) The design of DHH’s plugin is small, simple and clean — it doesn’t rely on a lot of generated code like openid_login_generator and has a much cleaner abstraction on top of ruby-openid, as compared to Eastmedia’s OpenID+Consumer plugin/generator.

The barriers to implementing OpenID for a Rails site are falling quickly.

Overloading username and openid_url

DHH, who is promisingly writing a Rails plugin for OpenID with Rick and others, has made a case for applications to store a user’s openid_url in place of their username, and present a unified login form which hides or reveals the different processing of openid & password logins, based on the presence of the http prefix.

When users start typing an OpenID identifier URL into the username field, the web app transparently turns it into an OpenID login action.

The common implementation today is to add an openid_url field alongside the existing user data, provide a login screen that offers regular and OpenID login, and often internally maps OpenID’s nickname (from the Simple Registration Extention) to the username/login.

What are some pros/cons to the proposed overloading approach?

Pros

  • For users, the the login or signup screens aren’t complicated further with another visible alternative
  • As a developer, you don’t have to use an additional field in the database
  • As a developer, you don’t have to deal with potential uniqueness conflicts between one person’s OpenID nickname, and another person’s username (which may require an additional username/email form to be presented during account creation in the case of a conflict)

Cons

  • According to Spec (s 3.2.1), the user shouldn’t be required to prefix their identifier with http (basically, a usability issue)
  • The gentleman’s standard (also in the spec) of naming the form field “openid_url” to allow it to be auto-populated across sites is lost.
  • Both OpenID users and traditional users may be confused by the overloaded form. If the OpenID logo is displayed, it doesn’t have a natural home which won’t confuse one party or the other
  • The application no longer has a place to store a friendly name for the user. So views display a URL, which will typically be much longer than a username, and require some additional view logic to turn into a link
  • OpenID accounts are at a privacy disadvantage vs. username/password accounts, because their visible username tracks directly back to them
  • For the developer, the code for the two authentication methods (password, openid) becomes even more coupled

Wrong on anything? And other major pro/cons missed?

On the whole, what’s common with current implementations (separate openid form on the login page for the user, separate openid field in the database for the application) is both in line with the spec, and still the better compromise. As the core team’s Rails Plugin quickly evolves, expect details and directions to change.

OpenID protocol diagram

OpenID Protocol Diagram

There are a number of diagrams illustrating OpenID, including the flow diagram in the spec, this one from Brain and JanRain, and this nice one from Paul and Technometria. But none yet matched what I needed.

The diagram is part of a future OpenID cheatsheet, which itself is part of the future screencast on OpenID development for Rails. And I’m happy if others use it for other things. As usual with a diagram, there are tradeoffs and opportunities for errors or misunderstandings. I’d like your feedback.

I split out the HTTP requests from responses, so the order of this could be more clear, even though this adds steps. I also skipped certain details for brevity. I considered coloring actions that involve the user, but just used the text (e.g. “user logins in …”).

I’m sure I let some errors creep in. Any comments for making this diagram more accurate & effective?