Thursday, September 30, 2010

Ruby, Rails, OpenID, and Google Integration for the Busy Developer

We're busy developers, you and I, and we want to get back to building our awesome HTML5 app and not muck around too much with user sign in and registration.  There are enough accounts out there, there's no reason why your users need to create a new account identity just for your system.  Let them sign in with an existing account, your users will thank you!!

We're going to learn the bare minimum required to allow your users to use their Google Account to register and sign in to your Ruby on Rails 3 web app.  I think you'll find that it's very easy to add OpenID support for Google Accounts, especially for Rails 3 web apps.

Assumptions:
  • You are building a Ruby on Rails 3 web app.
  • You do not yet have user authentication or registration for your app.  A future article will show you how to add Google OpenID to your authlogic app, but this article is all about starting from scratch.
  • You want your users to use their Google Accounts for their identity.
  • You have a model named User which represents a registered user in your system.
(Once you have Google Account authentication via OpenID, you are more than ready to publish your web app in the Chrome Web Store.  I've written a quick start guide for the Chrome Web Store which will help you get your app into the store in under 30 minutes.)

Step Zero: Add attributes to User
Although we are assuming you already have a User model, we need to add a identifier_url attribute.  This attribute, which should be unique, stores the OpenID URL identifying the user.

You can use a migration like:



class AddIdentifierUrlToUsers < ActiveRecord::Migration
  def self.up
    add_column :users, :identifier_url, :string
    add_index :users, :identifier_url, :unique => true
  end

  def self.down
    remove_index :users, :identifier_url
    remove_column :users, :identifier_url
  end
end


This code also assumes you have email, first_name, and last_name as attributes for your user, in addition to identifier_url.

Step One: Add the gems
You'll need both ruby-openid and rack-openid added to your Gemfile.  It's as simple as:



gem "ruby-openid"
gem "rack-openid"


And then, of course, run bundle install which will pull down the gems and lock them into your project.

Step Two: Tell Rails about Rack::OpenID

Open up config/application.rb and add


require 'rack/openid'


to the file of the file.  Then, inside the Application you'll need to add


config.middleware.use 'Rack::OpenID'

Step Three: Create AuthenticationHelper
Create an authentication_helper.rb in the app/helpers directory.  This file should include:



module AuthenticationHelper
  def signed_in?
    !session[:user_id].nil?
  end
  
  def current_user
    @current_user ||= User.find(session[:user_id])
  end
  
  def ensure_signed_in
    unless signed_in?
      session[:redirect_to] = request.request_uri
      redirect_to(new_session_path)
    end
  end
end


Step Four: Tell your app about AuthenticationHelper
Open up app/controllers/application_controller.rb and add the line include AuthenticationHelper



class ApplicationController < ActionController::Base
  protect_from_forgery
  include AuthenticationHelper
end


Step Five: Add the routes
Open up config/routes.rb and add the line:



resource :session


Step Six: Create the SessionsController
Create app/controller/sessions_controller.rb which should look like:



class SessionsController < ApplicationController
  skip_before_filter :verify_authenticity_token
  
  def new
    response.headers['WWW-Authenticate'] = Rack::OpenID.build_header(
        :identifier => "https://www.google.com/accounts/o8/id",
        :required => ["http://axschema.org/contact/email",
                      "http://axschema.org/namePerson/first",
                      "http://axschema.org/namePerson/last"],
        :return_to => session_url,
        :method => 'POST')
    head 401
  end
  
  def create
    if openid = request.env[Rack::OpenID::RESPONSE]
      case openid.status
      when :success
        ax = OpenID::AX::FetchResponse.from_success_response(openid)
        user = User.where(:identifier_url => openid.display_identifier).first
        user ||= User.create!(:identifier_url => openid.display_identifier,
                              :email => ax.get_single('http://axschema.org/contact/email'),
                              :first_name => ax.get_single('http://axschema.org/namePerson/first'),
                              :last_name => ax.get_single('http://axschema.org/namePerson/last'))
        session[:user_id] = user.id
        if user.first_name.blank?
          redirect_to(user_additional_info_path(user))
        else
          redirect_to(session[:redirect_to] || root_path)
        end
      when :failure
        render :action => 'problem'
      end
    else
      redirect_to new_session_path
    end
  end
  
  def destroy
    session[:user_id] = nil
    redirect_to root_path
  end
end


Note that the create method handles both sign in and registration.

It's also very important to point out that even though this code asks for attributes like firstName and lastName, the OpenID provider might not return them to your app.  So, you'll probably want to add a second step for new users to collect more information.  This is accomplished with the user_additional_info_path(user).

Step Seven: Create an error page
Just in case it didn't work, you can display a helpful message for your user.  Create a app/views/sessions/problem.html.erb



<h1>DOH!</h1>

<p>Looks like your Google login didn't quite work.  <%= link_to 'Try again?', new_session_path %></p>


Step Eight: Protect your pages
To ensure that only signed in users can access your resources, simply place before_filter :ensure_signed_in in any controller you wish to protect.

For example:



class AdditionalInfosController < ApplicationController
  before_filter :ensure_signed_in
  
  def show
    @user = User.find(params[:user_id])
  end
  
  def update
    @user = User.find(params[:user_id])
    @user.update_attributes(params[:user])
    redirect_to(session[:redirect_to] || root_path)
  end
  
end


Summary
Congrats, you've gone from zero user registration and authentication to a full OpenID based Google Account based system.  All of this code can be found in the Bracket Baby app on Github.
Post a Comment

Disclaimer

I'm probably required to say that the views expressed in this blog are my own, and do not necessarily reflect those of my employer. Also, except as otherwise noted, the content of this page is licensed under the Creative Commons Attribution 3.0 License, and code samples are licensed under the BSD License.