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.