The No Shit Guide To Supporting OpenID In Your Applications
27 FEB
OpenID, with the superhuman effort of Mr Willison is taking the world by storm and I am amoungst the masses leaping onto the bandwagon. Simon has done an excellent post and screencast detailing the whys and what-fors of OpenID so I thought I’d have a bash at applying the same no-bullshit approach to the other side of the coin, supporting (or as the official terminology puts it consuming) OpenID in your applications. With this post I’m going to blast through the absolute essentials you need to get started so if you need more general background on OpenID check out Simon’s stuff first. The examples, as you might except will be using Ruby on Rails but all of the concepts are applicable across platforms. So, without further ado, grab yourself a beer and we’ll begin…
Overview of the authentication process
When consuming OpenID what you are trying to do is ask the user for their OpenID (which is a URL) then ascertain from their OpenID server that they actually own this OpenID. Once you know that they own the OpenID you can then wack it in the session (and use it as a really lightweight means of identifying users between visits) or key it in with your own application specific account data if you need more power. This article is going to take you up to and including verifying the user’s OpenID. What you do with it is left to your imagination.
On a more granular level the verification process breaks down into these steps:
- Get the user to give you their OpenID URL.
- ‘Begin’ the verification process whereby your OpenID library of choice will work out the users OpenID server and, if successful, provide you with a redirect URL.
- Redirect the user to the given redirect URL. You specify a return URL within this URL.
- The user goes to their OpenID server, logs in and authorises your site’s verification request and is then redirected back to your return URL.
- Your server ‘completes’ the verification request and, if successful, confirms that this user owns this OpenID. The end.
So that’s essentially it. Some of the details of the transactions between your server, the user’s delegates and the OpenID are pretty complex but fortunately for us there are lots of good libraries for most platforms that mean you don’t need to bugger about with the crypotography and stuff. Woo hoo. For these examples we are going to use the ruby-openid gem but you can choose your own. Also note that East Media have a OpenID Consumer plugin for Rails that wraps even more detail with some generators but it’s good to understand the concepts before you let something write your code for you.
Get your library sorted
That’s easy. For us Rubyists its:
$ sudo gem install ruby-openid -y
Create your OpenID consuming controller
We are going to try to be as RESTy as possible here so we’ll create a singleton resource called openid. In routes.rb:
map.resource :openid, :member => { :complete => :get }
Then we’ll set up a simple controller. Firstly, we’ll need to require the ruby-openid gem here. We are also going to need a method that gives us an OpenID consumer object which is the single most complex part of this whole thing (and it isn’t complex). First, here’s the skeleton:
require_gem 'ruby-openid'
class OpenidController < ApplicationController
def new
# TODO: show a form requesting the user's OpenID
end
def create
# TODO: begin the OpenID verification process
end
def complete
# TODO: omplete the OpenID verification process
end
protected
def openid_consumer
@openid_consumer ||= OpenID::Consumer.new(session,
OpenID::FilesystemStore.new("#{RAILS_ROOT}/tmp/openid"))
end
end
The OpenID::Consumer constructor takes two arguments, the first one should be a hash like object that holds session data. That’s always going to be session for Rails. The second one takes a file store object which is used to store state information for the verification process. There’s lots (including an ActiveRecord store) but for many apps the filesystem store is fine.
Getting the user’s OpenID
The new action just needs to show a simple form posting the OpenID to the create action:
<% form_tag openid_path do %>
<%= text_field_tag 'openid_url' %> <%= submit_tag 'Login' %>
<% end %>
Note that it’s convention to call the field openid_url so browsers will autocomplete nicely. They also recommend that you embed the OpenID logo in the form field. Get the logo then try some CSS like this:
#openid_url {
background: url(/images/login-bg.gif) no-repeat #FFF 5px;
padding-left: 25px;
}
Beginning the verification
The create action is going to be responsible for kicking off the process:
def create
openid_url = params[:openid_url]
response = openid_consumer.begin openid_url
if response.status == OpenID::SUCCESS
redirect_url = response.redirect_url(home_url, complete_openid_url)
redirect_to redirect_url
return
end
flash[:error] = "Couldn't find an OpenID for that URL"
render :action => :new
end
We simply get the OpenID and pass it to the begin method of our consumer object to get a response. We then handle the status of the response which can have a number of states. For this super simple example we are just going to look for success but in a production app you’ll need to handle error states more usefully.
If the response was successful we call redirect_url passing the trust root and the return URL. The return URL is simply our complete action. The trust root is normally the homepage URL of your site. We then redirect the user to the resulting URL where the user logs in to their OpenID server, authorises your verification request and is (normally) redirected to return URL you provided.
Completing the verification
When the user is redirected back to your application the server will append information about the response in the query string which the OpenID library will unpack:
def complete
response = openid_consumer.complete params
if response.status == OpenID::SUCCESS
session[:openid] = response.identity_url
# the user is now logged in with OpenID!
redirect_to home_url
return
end
flash[:error] = 'Could not log on with your OpenID'
redirect_to new_openid_url
end
After passing the params hash containing all the info that the OpenID server sent us to the complete method we are given a response status to handle. Again for production apps more states should be handled but here, if the complete was successful we have completed the process. Here we just store the identity_url given in the session but at this point we could also do something like:
session[:user] = User.find_by_openid_url(response.identity_url)
Which would grab the users local account data based on the OpenID. Easy as pasty. However, there’s a few more bits and bobs you might want to know about.
Simple Registration Extension (SReg)
SReg is a basic means by which you can request additional information about the user from their OpenID server which you might normally use to prefill account details or other form fields. The information you can request access to is in the spec but there’s not much there at the moment. It’s still kind of useful. To request this information you need to add parameters to the redirect URL which is of course handled for you by your library. Revisiting the create action, we just add a call to add_extension_arg:
def create
openid_url = params[:openid_url]
response = openid_consumer.begin openid_url
if response.status == OpenID::SUCCESS
response.add_extension_arg('sreg','required','email') # <== here...
response.add_extension_arg('sreg','optional','nickname,gender') # <== ...and here
redirect_url = response.redirect_url(home_url, complete_openid_url)
redirect_to redirect_url
return
end
flash[:error] = "Couldn't find an OpenID for that URL"
render :action => :new
end
Then in the complete action, extract the returned information:
def complete
response = openid_consumer.complete params
if response.status == OpenID::SUCCESS
session[:openid] = response.identity_url
# the user is now logged in with OpenID!
@registration_info = response.extension_response('sreg') # <= { 'name' => 'Dan Webb', etc... }
redirect_to home_url
return
end
flash[:error] = 'Could not log on with your OpenID'
redirect_to new_openid_url
end
Immediate mode
Immediate mode allows you to attempt to verify the user without them leaving your site at all. This is normally possible if, during the first time you attempt to verify a user, they choose to always allow you to verify them and offers a slightly more streamlined login experience.
To implement this we first pass an extra argument to redirect_url:
def create
openid_url = params[:openid_url]
response = openid_consumer.begin openid_url
if response.status == OpenID::SUCCESS
redirect_url = response.redirect_url(home_url, complete_openid_url, true) # <== here
redirect_to redirect_url
return
end
flash[:error] = "Couldn't find an OpenID for that URL"
render :action => :new
end
Then ensure that our complete action handles the OpenID::SETUP_NEEDED status by redirecting them to the OpenID server’s setup page:
def complete
response = openid_consumer.complete params
case response.status
when OpenID::SUCCESS
session[:openid] = response.identity_url
redirect_to home_url
return
when OpenID::SETUP_NEEDED
redirect_to response.setup_url # <== here!
return
end
flash[:error] = 'Could not log on with your OpenID'
redirect_to new_openid_url
end
Fin
So that’s it. All you need to know to start getting OpenID going in your web applications. You’ve got no excuse now. In fact, neither have I…
Any corrections, questions or further wisdom are very welcome. I’m no expert on OpenID, I’ve literally just picked all this up from reading API docs, specs and source code and thought it would be good to share. let me know what you think.
30 Comments (Closed)
your post hit my feed reader the exact same moment as this svn log message: http://dev.rubyonrails.org/changeset/6245 You all shopping at the same grocer?
Sebastian at 27.02.07 / 01AM
Ha yeah, me too. It’s all moving!
Dan at 27.02.07 / 01AM
There is a bit of a problem with this:
redirect_to response.setup_url
I noticed in the wild some servers don’t append sreg requests to the setup_url on setup_needed – so when
check_immediate
fails the setup_url won’t cause the server to prompt the user for sreg data. The spec really is kind-of fuzzy about this – it doesn’t seem to be required for honestly claiming openid and sreg support. The server component of ruby-openid definitely doesn’ add sreg back as far as I can tell. Boo-urns.To get around this you should simply re-send your request in not-so-immediate mode. (Literally – using a new
openid_consumer.begin
and everything)If you think about it – this really isn’t all that different from using the
setup_url
anyway, and begs the question of why it is even needed to begin with. (I hear from the IRC grapevine that, sure enough, it isn’t – and will be going away in OpenID 2.0 )Again, to illustrate the problem:
- Consumer to auth server: Plz identify
some_url
, plz also give me back these heresreg_fields
if you could, and do it @immediate@ly if possible.- Server reply to consumer: Dear consumer, I can’t identify
some_url
just now, plz send your request again, likse so: <<plz>some_url
, non-immediately… >> 3. Consumer to server: Okdokee here I go! Plz identifysome_url
, non-immediately…It also shows how silly it is to use setup_url vs just resending the damn request.
rubyruy at 27.02.07 / 03AM
Go knowledge, rubyruy. It certainly does seem like setup_url is redundant.
Dan at 27.02.07 / 09AM
Nice tutorial. I spotted a few typos, use your spell checker.
Great work though. I’m not that well set in Ruby or RoR yet but this tutorial was explained very well. Easy to understand.
Sam Figueroa at 27.02.07 / 10AM
I did use a spell checker…could the typos you refer to be in fact English (rather than American) english? I’m from the UK, that’s how we spell stuff round here.
Glad you liked it though!
Dan at 27.02.07 / 10AM
Hi Dan, nice article.
The typo’s I noticed were ones that spell-checkers will never find, but good on you for defending the language of our beautiful isle!
Under ‘Getting the users OpenID’ you say: ‘Not that it’s convention to call the field…’—Is this meant to be note?
Also, some of your code has improperly nested quotes, but I wouldn’t panic it’ll just force people to understand what they’re doing instead of just copy/pasting.
Andrew at 27.02.07 / 12PM
Hi,
Great write up and code snippets, at this point there are now lots of people yapping about openid but not too many laying out code.
I am wondering what package, if any, you are using for your code colorizing.
Ann E. Mouse at 27.02.07 / 13PM
Andrew: Cheers, for letting me know…I’ve fixed those things.
Ann: I use Code Highlighter which is a JavaScript highlighter that I wrote myself. Get it from my SVN.
Feel free to use it on your own projects.
Dan at 27.02.07 / 15PM
So let’s assume that we have a site with an existing userbase and user accounts.
I assume that we could just let them enter their OpenID URL on their account page (so existing accounts and history could be linked to OpenID accounts). When logging in, one would follow your instructions for sending them to OpenID, or use the existing authentication system if they don’t have an OpenID.
Or, would you recommend going cold turkey and forcing everyone to use OpenID even if they already have an account?
topfunky at 27.02.07 / 17PM
topfunky: I think it would pan out pretty much like you said. You just provide exisiting users a way of linking there existing account to an OpenID. New users would also be given an option to use an OpenID when they sign up (which would then go off and prefill some of the registration information with SReg) then you could run the two login types alongside each other.
There’s still a few things I’m trying to suss out before adding support to Fridaycities (which already has normal user accounts) but it’s going fairly smoothly.
If you’ve not noticed yet check DHH’s start of an OpenID wrapper which is very basic at the moment but looks like its going to be cool given a few weeks.
Dan at 27.02.07 / 17PM
Talk about everyone being on the same page, but perhaps not looking at each other. I’ve almost finished my own OpenID plugin, then found out DHH has done his own, then think about writing a tutorial on how to use it, and I get this.
It’s all marvelous though, the buzz just keeps getting louder.
James at 27.02.07 / 20PM
I was aware that there are and will be a fair few plugins that abstract OpenID into a more usable form which is what we will all end up using and that’s cool. My intention with this article was to give a lower level bare bones view of what being an OpenID consumer involves because, with all new concepts like this, it’s good to understand what you are dealing with before you start using the library/plugin etc.
James, I’d love to see what you got so far.
Dan at 27.02.07 / 20PM
Though ‘OpenID’ sounds like a great idea, I don’t see it taking off in a big way. And yes, the theory that I would only ever have to remember one id & password makes total sense. But from a business perspective, it makes no sense. You have no way of gathering demographical data, and no way of safeguarding content from minors, just to name a few common scenarios.
gabriel at 28.02.07 / 16PM
gabriel: The problems you are talking about don’t actually exist with OpenID because OpenID doesn’t provide a universal account it only provides a universal way of signing in. Each application still retains its own user accounts with all the same information as before. The only difference is that each application doesn’t require its only username/password.
Dan at 28.02.07 / 18PM
I may have spoken too early, I will probably need to download the source, and see how things work before I bash it.
At first look, it appears one would go to an OpenID Server Provider, sign-up, which looks like you provide only a username, password, and an email. Then go to a site that supports OpenID and login with the chosen password and ‘username’ with provider.
This is where I get lost…. At what point do you add demographics? Age, sex, location, etc, etc . . .
gabriel at 28.02.07 / 20PM
gabriel: rather than digging into code at this stage it sounds like you might find Simon’s screencast useful in seeing how OpenID fits in to the whole process or possibly registering for an OpenID enabled site like Magnolia.
Dan at 28.02.07 / 20PM
Nice info. I am wondering how you would (efficiently) handle autologin.
George Moschovitis at 01.03.07 / 12PM
Hey Dan
Just a tiny little one, I think you might have missed off a param from your sreg example…
@registration_info = response.extension_response
Should be
@registration_info = response.extension_response(‘sreg’)
Dave Verwer at 01.03.07 / 14PM
@Dan
I see… yes, with some tweaking, this should start to pick up serious momentum, and hopefully it does, as it has been a long time coming.
Usability needs some improvements, but this could go places.
I will code this in to one of my sites, and see how users respond.
gabriel at 01.03.07 / 20PM
I think one of the keys to making OpenID successful is making sure that user accounts on sites that support OpenID may have more than one OpenID. My fear is that Consumer sites will rely on only one Provider which defeats the purpose of the service being decentralized.
See this post by Martin Atkins for further elucidation.
Scott McMillin at 03.03.07 / 02AM
I’ve just got openID for my comments on abscond.org, using my plugin, called ‘blatant’, which you can find here
I’ve yet to write docs or anything, but will be writing a blog soon describing how I’ve implemented it and my thoughts.
James at 04.03.07 / 19PM
Great and timely write-up Dan. Thanks! I’m starting to get excited about OpenID possibilities. Can anyone comment on whether the following workflow is supported by the protocol?
Can I use Open ID in a Rails app to affect this workflow scenario:
a) initially
-authenticate and register user using only their OpenID (without requesting any user profile information)(I know the answer to this is YES—the step below is the real question)
b) subsequently
-at some other point in time (perhaps weeks later), use the existence of an arbitraty set of conditions within the Rails App to trigger an email to the (now registered) user. The email has an embedded URL link that will-trigger another authentication request to the OpenID server (and this time require provision of the user’s Full Name). After the user logs-in to the provider and approves the request-the user is redirected back to the Rails application/site for further processingThe reason for doing this is to create a “just in time” or “need to know” model for requesting user profile information. This would allow a site to say “you can register without us even knowing your name” and “we’ll only ask for your name under the following conditions …, or when we need your name to provide you an additional service that you request”.
Does OpenID support this?
Donovan Dillon at 16.03.07 / 10AM
Great writeup. I have one quick question:
The ‘immediate’ mode is very nice, especially in the demos you see where you only have to fill in your OpenID and click a button…and BANG! you’re in. Very nice.
Another thing that is nice is the ‘preapproval’ of members that you can do, as Simon Willison mentions on a recent presentation/’slidecast’ The Future of OpenID ...which is where you can grant permission to say, a ‘group’ on a site based on someone’s OpenID URL…if you know it.
NOTE: For those not familiar with this, basically…he outlines how a site could work like this: I know you…and I know your OpenID…I can grant you permission to an area of a site before you’ve ever logged in, because I know your OpenID).
Again…very cool.
Here’s the question. If we put these two things together, don’t we get a system where if I know your OpenID and a site has implemented the ‘immediate mode’ functionality in their site (which…would be great for usability, etc), how does it prevent any user that knows your OpenID from visiting a site you are a member of and signing in automatically (assuming you have checked the ‘allow this site to get my information for ‘xx’ number of days…which most users would)?
Tom Kersten at 01.04.07 / 09AM
The site looks great ! Thanks for all your help ( past, present and future !)
morganusvitus at 05.04.07 / 10AM
Thanks for a great post. I am experimenting with openid and ran into one problem. The trust_root that is being passed to the openid verifier is the of the one of the underlying mongrel instances that I have running behind Apache. So instead of sending www.mydomain.com as trust_root it sends one of the localhost:800? as the trusted root and it gets rejected because it doesn’t match the return_url. Has someone else experienced this? I am reading up on the documentation but haven’t figured it yet.
Baldur Gudbjornsson at 07.05.07 / 20PM
http://anticworld.com/kamin.html
Pam at 07.05.07 / 22PM
Hi, I must be doing something wrong. How do I specify my home_url properly?
Cristiano Betta at 08.05.07 / 19PM
Hi, I must be doing something wrong. How do I specify my home_url properly?
Cristiano Betta at 08.05.07 / 19PM
Hello Dan,
This is Priyanka, an Acquisition Editor at a publishing firm. I read your online guide on supporting the usage of OpenID. I am considering a book on OpenID and I thought you might as well give a thought to authoring a book on the same. If interested, kindly get in touch with me and we can discuss the details further. Thanks Priyanka P.S: Nice blog by the way :-)
Priyanka at 21.05.07 / 05AM