Chapter 5: Login and Security

  1. Introduction: Tracking the Login Status

    Last week we have created a facility for the users to register. The data they filled in a web form were posted to the web server, and there the action Register in the User controller saved the newly created User data to the database.

    Today we will implement the Login function, which is quite specific. The information about the user who logged in should be preserved by the time he logs out. HTTP is a stateless protocol, which means it cannot normally preserve this kind of information for us. It only can get web pages and post user data back to the server, which offers some flexibility but is by no means a good solution for us.

    In web applications, data that should be preserved for a longer time are stored in a database. This may however be tricky with login: this particular kind of information should be stored for only a single computer from which the user actually logged in, and not to everyone who normally can access the database.

    To solve the problem Rails provides a special variable called session. It is capable to keep data between consecutive page requests, exactly as we need for storing the login information. Data will be kept in a session variable until they are erased (by us) or by timeout. Session variables are commonly used to implement information that should last as long as the user's session, like login functionality or shopping carts.

    By default, Rails uses disk-based sessions, that is, the session information is written to a file on the server's local disk and then retrieved using a special session cookie placed by Rails on the user's browser. There is also a better option which allows to store the essential data in the database, and in production application it is recommended as more efficient and reliable. However in this tutorial the default, disk-based session variable will be used.

    The session variable contains a collection of values (you can store more than one thing there). To distinguish between them, each should get a unique symbol. For our purposes we will use :user_id (notice the preceding colon, which says the Ruby interpreter that this is a symbol). Now, we can store the current user id in the session variable, like this:

    session[:user_id] = user.id

    An object that can contain a collection of values, each of them being identified by a symbol, is called a hash. You have already used a hash: that was the flash facility we've used last week. The hash returns the value of nil if no value corresponds to the given key. This means that we can use nil to mean "no user is logged in". If, on the other hand, it returns a value, we will know that some user has logged in.

    Therefore, you may use a simple test to check wether someone is logged or not, and do various things depending on this:

    <% if session[:user_id] != nil %> <p>Someone is logged in.</p> <% else %> <p>Waiting for someone to login...</p> <% end %>

    Notice that != means not equal to (and == stands for equal to).

    If you are logged in, session[:user_id] will provide you with the user id of the currently logged user. To retrieve the whole User object from the database you can use something like this:

    @user = User.find_by_id(session[:user_id])

    The find_by_id function is one of the most powerful features of Rails. For each data field you defined for your model class, Rails used its brightest magic to supply a corresponding find_by_ function. This function searches the database using the provided item as the key. So, you can search for the user knowing only its id with User.find_by_id, or knowing only its screen name by User.find_by_screen_name. Once your user is located you can use any data just like this:

    <p>My name is: <%= @user.screen_name %></p>

     

  2. Registration Login

    Before we create a separate login page, let's apply our login-tracking technique to allow newly registered users are automatically logged in. This will require adding only one line to the code that was already putting the user data into the database:

    app/controllers/user_controller.rb
    class UserController < ApplicationController
    
      ....
    
      def register
        @title = "Register"
    
        if request.post?
          @user = User.new(params[:user])
          if @user.save
            session[:user_id] = @user.id
            flash[:notice] = "User with login #{@user.screen_name} created!"
            redirect_to :action => :index
          end
        end
      end
    
    ...
    
    
    Exercise 5.1

    Create the User Hub Page, which would be accessible as index action of the User controller. Yes, you have already something in this action: if you went through the Chapter 4 and made all exercises, you should have displayed a list of all registered users on that page. But it was only a temporary function: the system is not supposed to reveal such information. Instead, you will now create a smart page which will display, for the beginning, the screen_name and e_mail of the currently logged user.

    This exercise consists of three smaller tasks:

    1. In the User controller, index action, find the currently logged user in the database.
    2. Using the information prepared in the previous step, display the information in the corresponding template.
    3. Now, modify the main menu of our application. We want the new User Hub page to substitute the Home Page (site/index) when anyone is logged. So:
      • If nobody is logged, the Home option should link to site/index, as it does so far.
      • If there is a logged user, the same Home option should link to user/index, and display the User Hub Page.

    Hint: if you need to break the session, or erase the session variable, just switch your browser off and then on again. All session data will be then nil.

  3. Login view and action

    Creating the Login view is easy: it will be very similar to the Register page. In fact, it is its simplified version:

    app/views/user/login.html.erb
    <h1>Login</h1>
    <%= error_messages_for :user %>
    <% form_for :user do |f| %>
      <p>
        <%= f.label :screen_name %>:
        <%= f.text_field :screen_name %>
      </p>
      <p>
        <%= f.label :password %>:
        <%= f.password_field :password  %>
      </p>
      <p>
        <%= f.submit "Login" %>
      </p>
    <% end %>
    
    <p>
      Not a member? <%= link_to "Register now!", :action => "register" %>
    </p>
    
    

    The login action also resembles its registration counterpart:

    app/controllers/user_controller.rb
    class UserController < ApplicationController
    
      ....
      
      def login
        @title = "Log in to Fakebook"
      
        if request.post?
          @user = User.new(params[:user])
          user = User.find_by_screen_name_and_password(@user.screen_name, @user.password)
          if user
            session[:user_id] = user.id
            flash[:notice] = "User #{user.screen_name} logged in"
            redirect_to :action => "index"
          else
            # Don't show the password in the view
            @user.password = nil
            flash[:notice] = "Invalid email/password combination"
          end
        end
      end
    
    

    Try out your application!

  4. Logging out

    The logout action is very simple; since we're using nil to indicate that no one is logged in, we just need to set session[:user_id] to nil, fill the flash with an appropriate message, and then redirect to the index page:

    app/controllers/user_controller.rb
    class UserController < ApplicationController
    
      ....
      
      def logout
        session[:user_id] = nil
        flash[:notice] = "Logged out"
        redirect_to :controller => :site, :action => :index
      end
    
    

    Notice that when redirecting to the index action, we additionally have to specify the controller.

    Exercise 5.2

    Create the working Login form. Unlike in fake_book, include it in the application Home Page, not in a separate page.

    Exercise 5.3

    Modify the logout function so that it displays the screen_name of the logged off user in the flash.

    Exercise 5.4

    Modify the main menu so that it displays:

    • Login and Register options, as it was so far, only when nobody is logged
    • Logout option, and no Login or Register, when someone is logged.
  5. Protecting Pages

    Login is not just art for art's sake. It is normally used to protect your sensitive information against unauthorised access.

    If you followed this Tutorial carefully, you will have no easy access to The User Hub Page while you log out. However try to logout, and then just navigate to http://localhost:3000/user. What I've got, was an execution error and a total mess. You may have been smarter and use an if statement to sort out such situations, something like this:

    app/controllers/user_controller.rb
    class UserController < ApplicationController
    
      ....
      
      def index
        @title = "User Index"
    	unless session[:user_id]
          flash[:notice] = "Please login first"
          redirect_to :action => :login
    	  return
    	end
    	# the rest of function follows here...  
    

    Check this again: http://localhost:3000/user. If there is no logged user, you will be automatically redirected to the Login page. This works fine, and will always restrain users from seeing this site, but there is one serious problem with this solution.

    If you have a lot of pages, and would like to restrict unathorised users from seeing all of them, you would have to put the above lines into the beginning of every action! Of course this is not a good solution, it is not DRY (remember what DRY stands for? Don't Repeat Yourself). And, of course, Rails provides an excellent solution which is filtering. By declaring a before_filter for entire controller we can very easily restrict access to every action:

    app/controllers/user_controller.rb
    class UserController < ApplicationController
    
      before_filter :protect, :except => [:login, :register]
    
      ....
      # all the usual stuff goes here...
      ....
      
    private
      # Protect a page against not logged users
      def protect
    	unless session[:user_id]
          flash[:notice] = "Please login first"
          redirect_to :action => :login
    	  return false
    	end
      end
    
    end
    

    The before_filter statement says that, before any action is executed, the protect function will be called first. Notice that we declared exceptions for the login and register that should apparently be available for not logged users. But the new rule will be strictly valid for all other actions in the User controller.

    Notice that in case of refusal, the protect function returns false. This is the signal for the framework not to go on with a normal action.

    Exercise 5.5

    Protect your application against unauthorised access - as described above.

Valid XHTML 1.0 Strict Valid CSS!