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. Image may be NSFW.
Clik here to view.
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.