Chapter 5: Login and Security
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.idAn 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>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.1Create 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:
- In the User controller, index action, find the currently logged user in the database.
- Using the information prepared in the previous step, display the information in the corresponding template.
- 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.
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 endTry out your application!
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 endNotice that when redirecting to the index action, we additionally have to specify the controller.
Exercise 5.2Create the working Login form. Unlike in fake_book, include it in the application Home Page, not in a separate page.
Exercise 5.3Modify the logout function so that it displays the screen_name of the logged off user in the flash.
Exercise 5.4Modify 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.
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 endThe 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.5Protect your application against unauthorised access - as described above.
Copyright (C) 2009-2011 by Jaroslaw Francik

