Since I wrote my first piece on extending a rails app to accept OpenID quite a few other tutorials and an official plugin have appeared to make that process easier. OpenID is quickly becoming quite mainstream, at least amongst developers, and that is very good news.
It’s becoming so mainstream in fact, that recently I’ve been asked to implement an OpenID server on top of an existing user database so that those users can have an easy single-sign-on option across a range of sites. Writing the server side piece is not quite so straightforward and there’s not much documentation yet. A few sample servers are available but the rails examples don’t run cleanly on the latest gems, so while I took some code from them it made most sense to start from scratch. Over the past couple of days I’ve hacked together something that works for me and even though it could still do with some polish a few notes follow. Please do use the comments to correct anything I may have gotten wrong or skipped over.
Firstly, a word on the process. The OpenID process works along the lines of:
- The client sends a request to the server to establish communication, shared keys, etc.
- Having established the keys, the client then redirects the end-user to the server to log in
- The server logs the end-user in however it wishes
- The server redirects the end-user back to the client with a query string indicating success or failure
- The client then confirms the success of the request with the server
I was starting from a very simple rails app that provided a Users resource and a Session resource. It’s really little more than a skeleton app with restful_authentication installed and configured. Users can log in and anyone can see a user’s page, and that’s all there is to it. I also had the ruby-openid gem installed and used a fair bit of code from the sample apps supplied with the gem.
Preparing The Existing Site
The first thing you’ll want to do is make sure that the HTML pages for your user resources have the correct headers. Chances are you will want users to be able to use their page (eg. http://yourapp.com/users/jystewart) as their OpenID but that won’t actually be where the login takes place, so you’ll want to put something like:
<link rel="openid.server" href="" />
<link rel="openid2.provider" href="" />
in the header block of your user page HTML. From that the OpenID client will know to redirect users to your sessions_url for authentication.
Preparing The Models
To my database that so far held the user details, I added four tables:
create_table :nonces do |t|
t.string :nonce
t.integer :created
t.timestamps
end
create_table :associations do |t|
t.binary :server_url, :secret
t.string :handle, :assoc_type
t.integer :issued, :lifetime
t.timestamps
end
create_table :settings do |t|
t.string :setting
t.binary :value
t.timestamps
end
create_table :trusts do |t|
t.integer :user_id
t.string :trust_root
t.timestamps
end
The first three hold the various details we need for the negotiations with the client, and the fourth stores the details of any sites a given user has said they want to always trust so that we don’t need to confirm those details every time.
We also need an implementation of the OpenID::Store class for the OpenID library we’re using. I simply copied the one from the sample rails app distributed with the gem. With the models and that interface in place we can move on to managing the requests.
Distinguishing OpenID Requests
I decided to use a Mime::Type alias to distinguish between requests made directly in my app and those coming through OpenID. To do that I added:
Mime::Type.register_alias "text/html", :openid
in RAILS_ROOT/config/initializers/mime_types.rb and added the following code in my controllers:
before_filter :check_for_openid_request
private
def openid_server # :nodoc:
@openid_server ||= OpenID::Server::Server.new(ActiveRecordOpenIDStore.new)
end
def openid_request
@openid_request ||= openid_server.decode_request(
openid_params.delete_if{ |key, val|
key == 'openid.session_type' && val == ''})
end
def check_for_openid_request
@openid_params = session[:openid_params] if session[:openid_params]
if openid_request.is_a?(OpenID::Server::CheckIDRequest)
session[:openid_params] = @openid_params
request.format = :openid
end
end
Because there are going to be several steps to our process during which we want to be able to hang on to the OpenID request details, I chose to store them in the session
Handling The Requests
Having defined our Mime::Type alias, we can now use openid as a type in respond_to blocks. The first step then is to change our SessionsController#create action from:
def create
self.current_user = User.authenticate(params[:login], params[:password])
if logged_in?
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
flash[:notice] = "Logged in successfully"
redirect_back_or_default(user_path(current_user))
else
render :action => 'new'
end
end
to:
def create
self.current_user = User.authenticate(params[:login], params[:password])
if logged_in?
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
respond_to do |wants|
wants.html {
flash[:notice] = "Logged in successfully"
redirect_back_or_default(user_path(current_user))
}
wants.openid {
if @trust = current_user.trusts.find_by_trust_root(openid_params['openid.trust_root'])
successful_openid_login
else
redirect_to new_trust_url
end
}
end
else
render :action => 'new'
end
end
This checks for a successful login and if it’s an OpenID request then checks to see if we already trust that site. If we do, we call the ‘successful_openid_login’ method and if not, we redirect the user to ask if they want to trust the client site. The successful_openid_login method looks like:
def successful_openid_login
session[:openid_params] = nil
@resp = openid_request.answer(true)
@resp.add_field(nil, 'is_valid', 'true')
add_sreg
render_openid_response
end
def render_openid_response
response.headers['Content-Type'] = 'charset=utf-8'
@resp = openid_server.encode_response(@resp)
case @resp.code
when OpenID::Server::HTTP_OK
render :text => @resp.body, :status => 200
when OpenID::Server::HTTP_REDIRECT
redirect_to @resp.redirect_url
else
render :text => @resp.body, :status => 400
end
end
And is almost entirely taken from the code supplied with the gem. I ran into some trouble with the ‘is_valid’ field not being added to the responses but being needed by my clients, so added the line
@resp.add_field(nil, 'is_valid', 'true')
The TrustsController implementation is quite straightforward and I’ll not go into details here, but will include it in the zip file you can download at the end of this entry.
This code will handle most of the process, but we have one remaining query to handle, and that is the final check_authentication request that is used to confirm everything fell into place. The actual request will come as a POST to your /session path so you will want to be ready to handle it there. I decided to implement a before_filter to intercept it and handle it separately rather than clutter my create method still further:
before_filter :handle_openid_check_auth_request
def handle_openid_check_auth_request
if openid_params['openid.mode'] == "check_authentication"
signatory = OpenID::Server::Signatory.new(ActiveRecordOpenIDStore.new)
@resp = openid_request.answer(signatory)
@resp.add_field(nil, 'is_valid', 'true')
render_openid_response
end
end
Once I had the code working acceptably I pulled it out into a separate module in the ’lib’ folder. As the code matures it would probably be a good candidate for a rails plugin, but I’m not going to have time to put that together on my own for a while. If anyone wants to collaborate on that, get in touch in the comments (or by email) and maybe we can make it happen.
All the code above along with various other utility methods can be found in this zip file.