Saturday, June 03, 2006

How I got OpenID working with Ruby on Rails

After spending two hours searching for good documentation for using OpenID as my authentication for my Ruby on Rails app and being disappointed in lack of examples and other documentation, I decided to figure it out myself.  I did, with help from OpenID's author Brian.  Here is what I found. I did all this in a brand new Rails app rather than my pride and joy one that I've been working on so I could get all the kinks out. You may want to do the same your first time.

First I downloaded the Ruby port of OpenID:

$> gem install ruby-openid

At the time of writing, Ruby OpenID 1.0.1 is the current version. The documentation suggests that the next step is to run this command from your rails app:

$> ./script/generate openid_login Account

This step did not work for me.  I got the error: "Couldn't find 'openid_login' generator".  I am still fairly new to Rails so I could not figure out why installing the ruby-openid gem did not automatically register it as a Rails generator.  Other gems seem to automatically hook themselves up during install.  Ah well.  Brian later told me that the gem does not automatically register the generator, but there will be a dedicated gem later on just for the generator.

Upon exploring the install directory for the gem (in my case under /usr/lib/ruby/gems/1.8/gems/ruby-openid-1.0.1), I read the INSTALL file, which suggested that if properly installed, openid should be accessible through

 $> irb
irb(main):001:0> require "openid/consumer"
=> true

That didn't work either.  False came back.  Now I had a place to start.  I found that if I put myself in the lib directory of ruby-openid-1.0.1 that this worked.  Also, I discovered that this worked:

 $> irb
irb(main):001:0> require "rubygems"
=> true
irb(main):002:0> require_gem "ruby-openid"
=> true
irb(main):003:0> OpenID::Consumer
=> OpenID::Consumer

I discovered that downloading the .tar.gz file included several more files (and directories!) than the gem did.  So I downloaded that and tried to run the standalone example consumer.rb file, which is supposed to validate logins.  It kept failing.  It wouldn't work at all on my Linux box.  On my Windows box, I got a "Bad auth key - wrong length" error.

I emailed Brian (an author of the library) and asked for help.  He gave me the next few tips...

First, I needed to read the /usr/lib/ruby/gems/1.8/gems/ruby-openid-1.0.1/examples/README file.  It had additional information on setting up the Rails generator that the top level README file did not.  Specifically, I needed to do this:

$> mkdir -p ~/.rails/generators
$> ln -s /usr/lib/ruby/gems/1.8/gems/ruby-openid-1.0.1/examples/rails_openid_login_generator ~/.rails/generators/openid_login

This made Rails finally recognize the openid_login as a Rails generator. Yay!

$> ./script/generate openid_login account

So now this worked. And now I had a new README_LOGIN file in the root of my Rails app. Reading that for guidance, I needed to create my users table now. The README_LOGIN file had various database syntax listed, but I chose to use ActiveRecord migrations:

$> ./script/generate migration create_users_table
$> vi db/migrate/001_create_users_table.rb

And I filled in this content to the file, which is the minimum ruby-openid requires:

class CreateUsersTable < ActiveRecord::Migration
def self.up
create_table :users do |t|
t.column :openid_url, :string
end
end

def self.down
drop_table :users
end
end

And executed the migration, having already created my database.

$> rake migrate

I also added those lines to app/controllers/application.rb that the README_LOGIN file prescribed:

require_dependency 'openid_login_system'

# Filters added to this controller will be run for all controllers in the application.
# Likewise, all the methods added will be available for all controllers.
class ApplicationController < ActionController::Base
include OpenidLoginSystem
end

I found that the last line the README_LOGIN file suggests to add (model :user) is just that--a suggestion if you run into trouble later on. I left this line out. I'd like to learn it firsthand when this line becomes necessary.

So that was it. I launched my server:

$> ./script/server

And tested my app from across the Internet by visiting http://myhost.mydomain.com:3000/account/login. It worked! I was able to login with my OpenID, get redirected to my OpenID provider to authorize the login, and then the site recognized me as logged in.  I don't know if hosting from localhost in this example would work, with all the redirections between your server and an OpenID provider.  Maybe it would. I didn't test it.

One gotcha in case you run into it in your testing/exploring: the /account/welcome action that the openid_login generator creates does not actually require the client to be logged in. So although /account/welcome comes up by itself after login announcing you are now logged in, if you had not logged in but just navigated directly to /account/welcome yourself, the system would still tell you that you were logged in. This is not a system malfunction as much as incorrect documentation. I fixed it by changing the app/views/account/welcome.rhtml file to this:

<h3>Welcome</h3>

<% if @session[:user_id] %>
	<p>You are now logged into the system...</p>
	<%= link_to "« logout", :action=>"logout" %>
<% else %>
	<p>You are NOT logged into the system...</p>
<% end %>

As a last finishing touch, the login page textbox field lacked that professional-looking I-swirly picture on its left that immediately gives the OpenID recognition.  I downloaded it from https://www.myopenid.com/static/openid-icon-small.gif and added it to my /public/images folder. Then I modified app/views/layouts/scaffold.rhtml by adding the following to the inside of the tag:

<style>
input.openid
{
background: url(/images/openid_login.gif) no-repeat;
background-position: 0 50%;
padding-left: 18px;
border-style: solid;
border-color: lightgray;
border-width: 1px;
}
</style>

And I modified app/views/account/login.rhtml to include a class="openid" attribute for its login input box.

Now I feel I'm ready to integrate this into my actual Rails app. I hope this extra example serves you well. Comments are always welcome. If you ask a question about the process, I'll try my best to answer, but remember I'm not a Rails expert. If you feel you can improve on my methods, please share!

1 comment: