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.
ryan wrote:
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?
Posted on 27-Jul-07 at 6:25 pm | Permalink
Bernie Thompson wrote:
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!
Posted on 28-Jul-07 at 2:35 pm | Permalink
Yoshi wrote:
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.
Posted on 29-Dec-07 at 2:06 am | Permalink