open_id_authentication (part 2)

In part 1, we looked at some changes to DHH’s OpenID plugin for rails. Now we look at the app’s authenticated sessions controller, which uses that library code. The goals were:

  • Support both OpenID and password authentication
  • Support all the usual login goodies, including before_filter :login_required, remember me functionality (with cookies), HTTP basic authentication, etc.
  • Get this by integrating the restful_authentication plugin/generation and open_id_authentication plugin

First, I installed both plugins and ran the restful_authentication generator

./script/generate authenticated user authenticated_sessions

I chose the name “authenticated_sessions” to distinguish from the built-in (non-authenticated) Rails sessions functionality.

routes.rb adds

  map.resource :authenticated_session, :member => { :complete => :get }
  map.resources :users

Then it’s a matter of some substantial edits to the generated app/controllers/authenticated_session_controller.rb Here’s where it would be nice to have a combined generator like Eastmedia’s. If you’re just trying to get an app and and running, you don’t want to be reading a blog posting like this. :)

The smartest design decisions in the file below (which all came from DHH’s code), are having centralized successful_login(), failed_login(), and destroy() functions, which are common across the otherwise separate openid/password authentication paths.

# This controller handles the login/logout function of the site. 
# File created with restful_authentication generator 
class AuthenticatedSessionController < ApplicationController
 
  # render new.rhtml
  def new
  end
 
  # Added after restful_authentication generator, using code from http://www.loudthinking.com/arc/000604.html
  # Complete arrives to us via a browser redirect from the OpenID provider, which happens after create/begin.
  # Obviously only called in the OpenID path through authentication.
  def complete
    complete_open_id_authentication(params[:openid_url]) do |result, identity_url, sreg|
      case result
      when :canceled
        failed_login "OpenID verification was canceled"
      when :failed
        failed_login "Sorry, the OpenID verification failed"
      when :successful
        if self.current_user = User.find_by_openid_url(identity_url) || User.create(:openid_url => identity_url, :login => sreg['nickname'], :email => sreg['email'])
          successful_login
        else
          failed_login "Sorry, no user by that identity URL exists (#{identity_url})"
        end
      else
        failed_login "Unknown error logging in #{identity_url}"
      end
    end
  end
 
  # Handle the creation of a new authenticated session, OpenID or password authentication path
  def create
    if using_open_id?
      begin_open_id_authentication(params[:openid_url], :required => "nickname, email") do |result, identity_url|
        case result
        when :missing
          failed_login "Sorry, the OpenID server couldn’t be found"
        else
          failed_login "Unknown error in openid begin for #{identity_url}"        
        end
      end
    elsif params[:login]
      password_authentication(params[:login], params[:password])
    else
      failed_login "No valid credentials passed to create authenticated session"
    end
  end
 
  def destroy
    logger.info "logging out #{self.current_user.inspect}"
    self.current_user.forget_me if logged_in?
    cookies.delete :auth_token
    @session[:user] = nil
    flash[:notice] = "You have been logged out."
    redirect_back_or_default(calls_path())
  end
 
protected
 
  def password_authentication(login, password)
    if self.current_user = User.authenticate(params[:login], params[:password])
      successful_login
    else
      failed_login("Invalid login or password")
    end
  end
 
private
 
  def successful_login
    logger.info "sucessful login for #{self.current_user.inspect}"
    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
    redirect_back_or_default("/")
    flash[:notice] = "#{self.current_user.login} Logged in successfully"
  end
 
  def failed_login(message)
    logger.info 'Login failed. ' + message
    flash[:notice] = message
    redirect_to new_authenticated_session_path()
  end
 
end

Lastly, views/authenticated_session/new.rhtml is where the initial form is presented when users are redirected from an action that requires authentication. I chose to put the password and openid authentication forms on the same page, but within their own divs with IDs and a common rounding class, so the styles can be tuned in the .css file to make it look nice.

<div id='password-login' class='rounded'>
	<% form_tag authenticated_session_path do -%>
	<p><label for="login">Login</label><br/>
	<%= text_field_tag 'login' %></p>
 
	<p><label for="password">Password</label><br/>
	<%= password_field_tag 'password' %></p>
	<p><label for="remember_me">Remember Me</label><%= check_box_tag 'remember_me' %></p>
	<p><%= submit_tag 'Log in' %></p>
	<% end -%>
</div>
 
<div id='openid-login' class='rounded'>
	<% form_tag authenticated_session_path do %>
	  <p><label for="openid_url">OpenID</label><%= text_field_tag 'openid_url' %></p>
	  <p><label for="remember_me">Remember Me</label><%= check_box_tag 'remember_me' %></p>
	  <%= submit_tag 'Login' %>
	<% end %>
</div>

You’ll notice that most of my postings and a lot of the plugins have headed in the direction of making authenticated sessions a REST resource, using the newer RESTful support in Rails. Reflecting back on it all, it isn’t the best conceptual or practical match. As long as the end-user functionality is the same (support for remember_me, HTTP basic authentication, etc.) — it seems more strightforward to implement authenticated sessions as a traditional controller, not a REST resource.

Comments (3) to “open_id_authentication (part 2)”

  1. I’m trying to get the :remember_me functionality to work with open_id_auth. Since it leaves to verify at the identity server, the remember_me param is wiped out when it gets back. What are your thoughts?

  2. Hi Ryan,

    You’re right, there appears to be an error in persisting the remember_me param from begin() through the redirects to complete(). In part one of the article (changes to DHH’s lib), there should be additional changes to add the remember_me param to the redirect URL (line 115, as I have the line numbers in the diffs in part 1 of the article). To make that happen, you’ll also have to add paramters to the routines there to pass down whether remember_me is set.

    Thanks for catching this error — let us know if you code up a solution that works!

  3. Thanks. this informations helps me a lot.
    by the way,
    “AuthenticatedSessionController” should be “AuthenticatedSession[s]Controller” and “@session[:user] = nil” in the destroy method should be “session[:user_id] = nil” I think.
    any way, thanks a lot.

Post a Comment
(Never published)