A BLISSfully simple guide to OpenId on Merb
OpenId can seem like a daunting task to implement when you first look at it -- the specification is so long (ok, well it needs to be, it covers a lot of "specifics"), it can be hard to test, there isn't a nice little "playground" to try out each step, etc. If you have one thing wrong, it can be difficult to find out what is wrong, but if everything's right, it just works. Hopefully this how-to will help you painlessly get it done right, in Merb. I followed this tutorial, for Rails, but converted several things to apply to Merb instead. With merb_datamapper-0.4.4 and datamapper-0.2.5 I had to fix a couple things, but merb_datamapper-0.4.5 is in working order.
You will need the following to get your app openid-enabled:
1) The ruby-openid gem
2) 2 Routes
3) Sessions
4) 1 Controller with 2 Actions
5) 1 View
1) ruby-openid
This step is pretty simple. In the root of your application, just run the command:
gem install ruby-openid -i gems
Run that, then include it as a dependency in config/dependencies.rb:
dependency 'ruby-openid'
2) 2 Routes
One route will receive a POST request (or get, if you prefer) from your login form, requiring only one field that (according to the openid specs) SHOULD be named 'openid_url'.
The other route will receive a redirect from the user's openid provider, with a bunch of query parameters.
Here are my routes -- I added in there a /login and /logout route: login will show a login form, and logout will remove the login information from the session.
r.to(:controller => 'openid') do |openid|
openid.match("/login").to(:action => 'login').name(:login)
openid.match('/start_login').to(:action => 'start_login').name(:start_login)
openid.match('/complete_login').to(:action => 'complete_login').name(:complete_login)
openid.match("/logout").to(:action => 'logout').name(:logout)
end
3) Sessions
Sessions must be turned on in Merb, if you want to remember that someone's logged in (who they're logged in as). Here's the easiest way to do it with DataMapper: Open up config/merb.yml, and find the "session_store:" line. Uncomment it, and make it say "session_store: datamapper". merb_datamapper makes it so simple, the next time you start up your application, if there is no 'sessions' table, merb_datamapper will create one, and you'll be ready to go.
4) 1 Controller and 2 Actions
You need 2 functional actions -- I recommend creating a new controller, but you can just as well put those actions into an already-existing controller if you wish.
Controller Action #1: This action will receive one parameter, 'openid_url', and from that, it is to 1) determine the user's openid endpoint server, 2) request a request token (shared secret) from that server, and 3) redirect the user to verify/authorize that request token. The ruby-openid gem takes care of all of the magic, all you have to do is tell it to start. Here is my (very minimal, not quite fault-tolerant) openid start_login action:
def start_login(openid_url)
trust_root = 'http://localhost:4000'
checkid_request = openid_consumer.begin(openid_url)
redirect checkid_request.redirect_url(
trust_root,
trust_root + url(:complete_login) # using the named route from the above routes
)
# Pick up various failures via rescue here
end
As you can see, there are only two lines of action there: 1) openid_consumer.begin(openid_url), which does the discovery of the user's openid server and gets a request token, and 2) redirect. The checkid_request.redirect_url method generates the url with the proper parameters -- the root url of your website you wish to ask authorization for, and then the url you wish the user to be returned to.
Controller Action #2: This action will receive the user back from his/her openid server, with an answer to your authorization request. Its purpose is to check whether or not your login succeeded. I may not be doing this the best or most correct way, but it works:
def complete_login
response = openid_consumer.complete(request.send(:query_params), 'http://'+request.host+request.path)
if response.status.to_s == 'success'
# The user is now logged in: Do what you will.
session[:openid_url] = response.identity_url
redirect(session.data.delete(:after_login) || '/')
else
# Failed: The openid_server returned unauthorized, or other error.
# Be nice to the user and tell them something went wrong. (not in those words)
end
end
Now of course you will want to fill in the failure code, and probably change what it does after the user is successfully logged in. As you can see I put in there to redirect to the url stored in session[:after_login]. I had set that value just before forcing the user to log in, saving the url the user was trying to access before they logged in.
One more thing in the controller: You've noticed I'm using a method "openid_consumer". That doesn't just come from nowhere, I have a protected method in the controller that sets up the OpenID::Consumer object for both methods to use:
protected
def openid_consumer
@openid_consumer ||= OpenID::Consumer.new(session, OpenID::Store::Filesystem.new("#{Merb.root}/tmp/openid"))
end
(use MERB_ROOT instead for merb < 0.5.3)
And one more thing: you need to "require 'openid/store/filesystem'" at the top of the controller these actions are in, and you may need to create the tmp/openid directory.
5) 1 View
This one view is just the login form. You could include this anywhere on your page, or on every page of your app, if you like. Craft that session[:after_login] (saving the url) smartly so that the user can be returned to whatever page they started at before login.
Did I miss anything?
- - - update January 17 - - -
in merb_datamapper 1.4.5, use
:session_store: datamapperinstead of data_mapper with the underscore.
3 comments:
Thanks for the tutorial. I had to change MERB_ROOT to Merb.root in the openid_consumer method. merb 0.9.2 on OS X 10.5.2 and Ruby 1.8.6.
dependency 'ruby-openid' seems to fail in Merb 0.9.2 now....but require 'openid' works pretty well.
not super sure what's up with that, but thought it might help your readers.
Thanks Adam, your hint was very useful for me...
Post a Comment