28 February 2010

Forcing Authlogic to handle 3 login parameters

Categories:  RoR  Ruby  Web

How to force RoR Authlogic authentication solution to handle 3 login parameters insted of standard two.

Authlogic is one the most popular authentication solutions for Ruby On Rails. It success comes from the high usability and flexibility. With it you can perform, almost any task needed to authenticate user in your Web application. Additionally, thanks to available authlogic plug-ins, you can integrate Open ID, Facebook or Twitter authentication into your site. If you are unfamiliar with using Authlogic you should, see this and this Ryan Bates screen cast or follow basic tutorial available in authlogic REDME file.

In 90% of web application you will need to provide two login information, user name or email and password. This type of authentication is available out of the box in authlogic. The project I was working on required me to have 3 login parameters. In my project I'm having many workshops, and every single workshop can have many users that can share and manipulate it's data. I would like to have a unique user name in scope of a single workshop, but between the workshops the users name can be repeated. First I thought it will hard be to achieve such goal, but when I patiently read all the API docs, the flexibility of authlogic and ruby language let me do it, in quite simple way. I will try to share my solution hoping that someone will find it useful or maybe will suggest me a better way to solve this problem.

I'm assuming that if you are reading this you know how the Ruby On Rails framework works, and you can read and understand the ruby source code. I will not try to explain every single line of code that you will find here. I will focus on explaining parts of the code that are important to let authlogic handle authorization using 3 login parameters.

We will start with presenting the view part of my project. Below you will find a login form partial.

View the partial source code
  1. <fieldset id="form">
  2. <legend>Submit login information</legend>
  3. <% form_remote_for @user_session,
  4. :url=>{:action=>'create'},
  5. :html=>{ :autocomplete => :off },
  6. :before => "Element.show('loging-indicator')",
  7. :success => "Element.hide('loging-indicator')" do |f| %>
  8. <%= f.error_messages %>
  9. <p>
  10. <%= f.label :workshop_unique, "Workshop id:" %><br />
  11. <%= f.text_field :workshop_unique, :value => @workshop, :size => 30 %> <br />
  12. <%= check_box_tag :remember_workshop, 1, checked = false %>
  13. <%= label_tag :remember_workshop, "Remeber workshop id" %>
  14. </p>
  15. <p>
  16. <%= f.label :username, "User name: " %><br />
  17. <%= f.text_field :username, :value => @username %> <br />
  18. <%= check_box_tag :remember_username, 1, checked = false %>
  19. <%= label_tag :remember_username, "Remember user name" %>
  20. </p>
  21. <p>
  22. <%= f.label :password, "Password:" %><br />
  23. <%= f.password_field :password %>
  24. </p>
  25. <p><%= f.submit "Zaloguj" %></p>
  26. <% end %>
  27. </fieldset>
  28. <div id="loging-indicator" style="display:none;">
  29. <span>Data manipulation in progress:<br />
  30. <%= image_tag("processingbar.gif",
  31. :align => "absmiddle",
  32. :border => 0) %>
  33. </span>
  34. </div>

Here we are constructing an Ajax based login form, using a form_remote_for helper method which operates on @user_session object. This object is typical implementation of Authlogic api, what is not typical here is that we have 3 login fields. The user will have to provide his workshop ID, his user name and his password to log in to the system. You will also find check boxes which are letting user remember his login information, but this is out of the scope of this article.

Now let us take a look on controller code which will handle all the data manipulation between the view and the model, during log in and log off faze.

View the controller source code
  1. class UserSessionsController < ApplicationController
  2.  
  3. # access control:
  4. # logged in users can destroy session (and be logged out)
  5. # anonymous users can create new session (login in to system)
  6. access_control :debug => true do
  7. allow logged_in, :to => [:destroy]
  8. allow anonymous, :to => [:new, :create]
  9. end
  10.  
  11. def new
  12. @user_session = UserSession.new
  13. @workshop = cookies[:workshop]
  14. @username = cookies[:username]
  15. end
  16.  
  17. def create
  18. if params[:remember_workshop]=="1"
  19. if cookies[:workshop] != nil
  20. cookies.delete :workshop
  21. end
  22. cookies[:workshop] = {:value=> params[:user_session][:workshop_unique],
  23. :expires => 1.year.from_now}
  24. end
  25. if params[:remember_username]=="1"
  26. if cookies[:username] != nil
  27. cookies.delete :username
  28. end
  29. cookies[:username] = {:value=> params[:user_session][:username],
  30. :expires => 1.year.from_now}
  31. end
  32. login = {:workshop_unique => params[:user_session][:workshop_unique],:username => params[:user_session][:username]}
  33. @user_session = UserSession.new(:username=>login,:password=>params[:user_session][:password])
  34. if @user_session.save
  35. flash[:notice] = "Login succsessfull."
  36. respond_to do |format|
  37. format.html { redirect_to root_url }
  38. format.js {
  39. render :update do |page|
  40. page.redirect_to root_url
  41. end
  42. }
  43. end
  44. else
  45. unless cookies[:workshop].nil?
  46. @workshop = cookies[:workshop]
  47. else
  48. @workshop = params[:user_session][:workshop_unique]
  49. end
  50. unless cookies[:username].nil?
  51. @username = cookies[:username]
  52. else
  53. @username = params[:user_session][:username]
  54. end
  55. respond_to do |format|
  56. format.html { render :action => 'new'}
  57. format.js {
  58. render :update do |page|
  59. page.replace_html :login, :partial => 'login_form'
  60. page.visual_effect(:shake,:login,:duration => 1)
  61. end
  62. }
  63. end
  64. end
  65. end
  66.  
  67. def destroy
  68. @user_session = UserSession.find
  69. @user_session.destroy
  70. flash[:notice] = "Log off succsesfull."
  71. current_user = nil
  72. redirect_to :controller => 'szws', :action => 'goodbye'
  73. end
  74.  
  75. end

Creating user session controller, is typical approach when using authlogic API. This controller will take care of logging in and off our user. At the top of the source code you will find a small portion of code that is out of the scope of this article, this part is typical for acl9 authorization solution I use in this application. What is important here are our methods. New which is used to display a fresh new login form to user, create which takes care of logging in or rejecting logging in if user provided wrong credentials. The third method destroy is used to log of our user. If you take a look at new and destroy methods there is nothing special here. In destroy we are finding the @user_session object of the currently logged in user and destroying his session, it's straight forward the authlogic API in use.

What is the most important here is our create method. First part is cookie handling, we will skip this part, and look at lines below it. Here you will find a @user_session object creation. Normally we would construct an object, passing to it two parameters, taken from request parameters. We would do it in 90% of cases but here you can find the trick i did, to make my 3 parameter login work. As constructor of session object can accept only username and password parameters. Authloghic is passing only those two parameters to model which is handling all authorization process. What we can do here is first we will construct an associative array which will store both our workshop unique and username then we will assign it to login object. Then we will pass this login object as username, in our UserSession.new constructor.

That is all I did to make 3 parameters login work at controller level. Of course thats not all the job we have to do to make authlogic properly verified our 3 parameters login. The last part is our model. We will have to look at two models, first is user_session model second is user model. Lets start with user_session model.

View the user_session model source code
  1. class UserSession < Authlogic::Session::Base
  2. find_by_login_method :find_user_in_workshop
  3. #Make sure the right messege is being displayed to user if he provides incorrect login information
  4. generalize_credentials_error_messages "You provided wrong login information, please try again"
  5.  
  6. @workshop_unique
  7.  
  8. def workshop_unique
  9. @workshop_unique
  10. end
  11.  
  12. def worksop_unique=(value)
  13. @workshop_unique=value
  14. end
  15.  
  16. end

As you can see the UserSession class derives from Authlogic::Session::Base, another typical thing for authlogic API. At the top of this source file you will find the most important thing. By defining find_by_login_method, you can override standard method of finding user in our system. As author wrote in API docs: "sky is the limit here", and he is right. We will look at our find_user_in_workshop method in user model source file. Below you can find a definition for error message that will be displayed to our user in case of providing wrong credentials. The last thing in the user_session model is definition of @workshop_unique instance variable. I added this to be able to use it in my view form_remote_for helper method. Look at the view source code. Now let's take a look at user model.

View the user model source code
  1. class User < ActiveRecord::Base
  2. #This is authentication subject
  3. acts_as_authentic do |c|
  4. c.validate_email_field = false
  5. c.validate_login_field = true
  6. #make sure that one workshop wont be able to have two users of the same name
  7. c.validations_scope = :workshop_id
  8. c.logged_in_timeout=30.minutes;
  9. c.maintain_sessions=false
  10. c.ignore_blank_passwords=false
  11. end
  12.  
  13. belongs_to :workshop
  14.  
  15. def self.find_user_in_workshop(login)
  16. workshop_unique=login[:workshop_unique]
  17. username = login[:username]
  18. workshop = Workshop.find_by_workshop_unique(workshop_unique)
  19. unless workshop.nil?
  20. find_by_username_and_workshop_id(username,workshop.id)
  21. else
  22. return nil
  23. end
  24. end
  25.  
  26. end

What you can see at the top of this source file, is authentication subject configuration. We are setting up validation of our login field and ignoring validation of email field. Next we are making sure that our user login will be unique in scope of our workshop. We are setting up session timeout. As you can see I'm also preventing session maintain because in my system one user can create account for another. I don't want to automatically re log user to new account after account creation. And the last thing is disabling ignore blank password option.

The second thing is typical relationship configuration as we know every user can belong to one workshop.

The last but not least thing : find_user_in_workshop method definition. This method is taking our login parameter, which is an associative array containing workshop_unique and username, taken from our login form. What we are doing here is first we are trying to find workshop by it's unique id. If this is successful, we are finding user using both username and workshop.id. And thats it, this is solution that is working quite well in system I'm building.

Sources:




Comments

If you have found something wrong with the information provided above or maybe you just want to speak your mind about it, feel free to leave a comment.
All comments will show up on page after being approved. Sorry for such policy but I want to make sure that my site will be free of abusive or vulgar content. I don't mind being criticized just do it using right words.

Leave a comment