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 |
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 |
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 |
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.