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.

Comments (3) to “open_id_authentication (Part 1)”

  1. [...] small batches of software goodness « open_id_authentication (Part 1) [...]

  2. +1

  3. Thanks for the effort on this!

    I am working on a rewrite using the latest ruby-openid (that open_id_authentication plugin is incompatible), but my plugin will probably be for Merb since that is my framework of choice now.

    There were lots of loose ends in the plugin. It surprises me a more definitive and actively maintained plugin for Rails hasn’t been written to replace this yet with all the interest in OpenID.

Post a Comment
(Never published)