Chapter 8 Log in, log out

Now that new users can sign up for our site (Chapter 7), it’s time to give them the ability to log in and log out. We’ll be implementing all three of the most common models for login/logout behavior on the web: “forgetting” users on browser close (Section 8.1 and Section 8.2), automatically remembering users (Section 8.4), and optionally remembering users based on the value of a “remember me” checkbox (Section 8.4.5).1

The authentication system we develop in this chapter will allow us to customize the site and implement an authorization model based on login status and identity of the current user. For example, in this chapter we’ll update the site header with login/logout links and a profile link. In Chapter 9, we’ll impose a security model in which only logged-in users can visit the user index page, only the correct user can access the page for editing their information, and only administrative users can delete other users from the database. Finally, in Chapter 11, we’ll use the identity of a logged-in user to create microposts associated with that user, and in Chapter 12 we’ll allow the current user to follow other users of the application (thereby receiving a feed of their microposts).

This is a long and challenging chapter covering many detailed aspects of login common systems, so I recommend focusing on completing it section by section. In addition, many readers have reported benefiting from going through it a second time.

8.1 Sessions

HTTP is a stateless protocol, treating each request as an independent transaction that is unable to use information from any previous requests. This means there is no way within the hypertext transfer protocol to remember a user’s identity from page to page; instead, web applications requiring user login must use a session, which is a semi-permanent connection between two computers (such as a client computer running a web browser and a server running Rails).

The most common techniques for implementing sessions in Rails involve using cookies, which are small pieces of text placed on the user’s browser. Because cookies persist from one page to the next, they can store information (such as a user id) that can be used by the application to retrieve the logged-in user from the database. In this section and Section 8.2, we’ll use the Rails method called session to make temporary sessions that expire automatically on browser close,2 and then in Section 8.4 we’ll add longer-lived sessions using another Rails method called cookies.

It’s convenient to model sessions as a RESTful resource: visiting the login page will render a form for new sessions, logging in will create a session, and logging out will destroy it. Unlike the Users resource, which uses a database back-end (via the User model) to persist data, the Sessions resource will use cookies, and much of the work involved in login comes from building this cookie-based authentication machinery. In this section and the next, we’ll prepare for this work by constructing a Sessions controller, a login form, and the relevant controller actions. We’ll then complete user login in Section 8.2 by adding the necessary session-manipulation code.

As in previous chapters, we’ll do our work on a topic branch and merge in the changes at the end:

$ git checkout master
$ git checkout -b log-in-log-out

8.1.1 Sessions controller

The elements of logging in and out correspond to particular REST actions of the Sessions controller: the login form is handled by the new action (covered in this section), actually logging in is handled by sending a POST request to the create action (Section 8.2), and logging out is handled by sending a DELETE request to the destroy action (Section 8.3). (Recall the association of HTTP verbs with REST actions from Table 7.1.)

To get started, we’ll generate a Sessions controller with a new action:

$ rails generate controller Sessions new

(Including new actually generates views as well, which is why we don’t include actions like create and destroy that don’t correspond to views.) Following the model from Section 7.2 for the signup page, our plan is to create a login form for creating new sessions, as mocked up in Figure 8.1.

images/figures/login_mockup
Figure 8.1: A mockup of the login form.

Unlike the Users resource, which used the special resources method to obtain a full suite of RESTful routes automatically (Listing 7.3), the Sessions resource will use only named routes, handling GET and POST requests with the login route and DELETE request with the logout route. The result appears in Listing 8.1 (which also deletes the unneeded routes generated by rails generate controller).

Listing 8.1: Adding a resource to get the standard RESTful actions for sessions. config/routes.rb
Rails.application.routes.draw do
  root                'static_pages#home'
  get    'help'    => 'static_pages#help'
  get    'about'   => 'static_pages#about'
  get    'contact' => 'static_pages#contact'
  get    'signup'  => 'users#new'
  get    'login'   => 'sessions#new'
  post   'login'   => 'sessions#create'
  delete 'logout'  => 'sessions#destroy'
  resources :users
end

The routes defined in Listing 8.1 correspond to URLs and actions similar to those for users (Table 7.1), as shown in Table 8.1.

HTTP request URL Named route Action Purpose
GET /login login_path new page for a new session (login)
POST /login login_path create create a new session (login)
DELETE /logout logout_path destroy delete a session (log out)
Table 8.1: Routes provided by the sessions rules in Listing 8.1.

Since we’ve now added several custom named routes, it’s useful to look at the complete list of the routes for our application, which we can generate using rake routes:

$ bundle exec rake routes
 Prefix Verb   URI Pattern               Controller#Action
     root GET    /                         static_pages#home
     help GET    /help(.:format)           static_pages#help
    about GET    /about(.:format)          static_pages#about
  contact GET    /contact(.:format)        static_pages#contact
   signup GET    /signup(.:format)         users#new
    login GET    /login(.:format)          sessions#new
          POST   /login(.:format)          sessions#create
   logout DELETE /logout(.:format)         sessions#destroy
    users GET    /users(.:format)          users#index
          POST   /users(.:format)          users#create
 new_user GET    /users/new(.:format)      users#new
edit_user GET    /users/:id/edit(.:format) users#edit
     user GET    /users/:id(.:format)      users#show
          PATCH  /users/:id(.:format)      users#update
          PUT    /users/:id(.:format)      users#update
          DELETE /users/:id(.:format)      users#destroy

It’s not necessary to understand the results in detail, but viewing the routes in this manner gives us a high-level overview of the actions supported by our application.

8.1.2 Login form

Having defined the relevant controller and route, now we’ll fill in the view for new sessions, i.e., the login form. Comparing Figure 8.1 with Figure 7.11, we see that the login form is similar in appearance to the signup form, except with two fields (email and password) in place of four.

As seen in Figure 8.2, when the login information is invalid we want to re-render the login page and display an error message. In Section 7.3.3, we used an error-messages partial to display error messages, but we saw in that section that those messages are provided automatically by Active Record. This won’t work for session creation errors because the session isn’t an Active Record object, so we’ll render the error as a flash message instead.

images/figures/login_failure_mockup
Figure 8.2: A mockup of login failure.

Recall from Listing 7.13 that the signup form uses the form_for helper, taking as an argument the user instance variable @user:

<%= form_for(@user) do |f| %>
  .
  .
  .
<% end %>

The main difference between the session form and the signup form is that we have no Session model, and hence no analogue for the @user variable. This means that, in constructing the new session form, we have to give form_for slightly more information; in particular, whereas

form_for(@user)

allows Rails to infer that the action of the form should be to POST to the URL /users, in the case of sessions we need to indicate the name of the resource and the corresponding URL:3

form_for(:session, url: login_path)

With the proper form_for in hand, it’s easy to make a login form to match the mockup in Figure 8.1 using the signup form (Listing 7.13) as a model, as shown in Listing 8.2.

Listing 8.2: Code for the login form. app/views/sessions/new.html.erb
<% provide(:title, "Log in") %>
<h1>Log in</h1>

<div class="row">
  <div class="col-md-6 col-md-offset-3">
    <%= form_for(:session, url: login_path) do |f| %>

      <%= f.label :email %>
      <%= f.email_field :email, class: 'form-control' %>

      <%= f.label :password %>
      <%= f.password_field :password, class: 'form-control' %>

      <%= f.submit "Log in", class: "btn btn-primary" %>
    <% end %>

    <p>New user? <%= link_to "Sign up now!", signup_path %></p>
  </div>
</div>

Note that we’ve added a link to the signup page for convenience. With the code in Listing 8.2, the login form appears as in Figure 8.3. (Because the “Log in” navigation link hasn’t yet been filled in, you’ll have to type the /login URL directly into your address bar. We’ll fix this blemish in Section 8.2.3.)

images/figures/login_form
Figure 8.3: The login form.

The generated form HTML appears in Listing 8.3.

Listing 8.3: HTML for the login form produced by Listing 8.2.
<form accept-charset="UTF-8" action="/login" method="post">
  <input name="utf8" type="hidden" value="&#x2713;" />
  <input name="authenticity_token" type="hidden"
         value="NNb6+J/j46LcrgYUC60wQ2titMuJQ5lLqyAbnbAUkdo=" />
  <label for="session_email">Email</label>
  <input class="form-control" id="session_email"
         name="session[email]" type="text" />
  <label for="session_password">Password</label>
  <input id="session_password" name="session[password]"
         type="password" />
  <input class="btn btn-primary" name="commit" type="submit"
       value="Log in" />
</form>

Comparing Listing 8.3 with Listing 7.15, you might be able to guess that submitting this form will result in a params hash where params[:session][:email] and params[:session][:password] correspond to the email and password fields.

8.1.3 Finding and authenticating a user

As in the case of creating users (signup), the first step in creating sessions (login) is to handle invalid input. We’ll start by reviewing what happens when a form gets submitted, and then arrange for helpful error messages to appear in the case of login failure (as mocked up in Figure 8.2.) Then we’ll lay the foundation for successful login (Section 8.2) by evaluating each login submission based on the validity of its email/password combination.

Let’s start by defining a minimalist create action for the Sessions controller, along with empty new and destroy actions (Listing 8.4). The create action in Listing 8.4 does nothing but render the new view, but it’s enough to get us started. Submitting the /sessions/new form then yields the result shown in Figure 8.4.

Listing 8.4: A preliminary version of the Sessions create action. app/controllers/sessions_controller.rb
class SessionsController < ApplicationController

  def new
  end

  def create
    render 'new'
  end

  def destroy
  end
end
images/figures/initial_failed_login_3rd_edition
Figure 8.4: The initial failed login, with create as in Listing 8.4.

Carefully inspecting the debug information in Figure 8.4 shows that, as hinted at the end of Section 8.1.2, the submission results in a params hash containing the email and password under the key session, which (omitting some irrelevant details used internally by Rails) appears as follows:

---
session:
  email: 'user@example.com'
  password: 'foobar'
commit: Log in
action: create
controller: sessions

As with the case of user signup (Figure 7.15), these parameters form a nested hash like the one we saw in Listing 4.10. In particular, params contains a nested hash of the form

{ session: { password: "foobar", email: "user@example.com" } }

This means that

params[:session]

is itself a hash:

{ password: "foobar", email: "user@example.com" }

As a result,

params[:session][:email]

is the submitted email address and

params[:session][:password]

is the submitted password.

In other words, inside the create action the params hash has all the information needed to authenticate users by email and password. Not coincidentally, we already have exactly the methods we need: the User.find_by method provided by Active Record (Section 6.1.4) and the authenticate method provided by has_secure_password (Section 6.3.4). Recalling that authenticate returns false for an invalid authentication (Section 6.3.4), our strategy for user login can be summarized as shown in Listing 8.5.

Listing 8.5: Finding and authenticating a user. app/controllers/sessions_controller.rb
class SessionsController < ApplicationController

  def new
  end

  def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      # Log the user in and redirect to the user's show page.
    else
      # Create an error message.
      render 'new'
    end
  end

  def destroy
  end
end

The first highlighted line in Listing 8.5 pulls the user out of the database using the submitted email address. (Recall from Section 6.2.5 that email addresses are saved as all lower-case, so here we use the downcase method to ensure a match when the submitted address is valid.) The next line can be a bit confusing but is fairly common in idiomatic Rails programming:

user && user.authenticate(params[:session][:password])

This uses && (logical and) to determine if the resulting user is valid. Taking into account that any object other than nil and false itself is true in a boolean context (Section 4.2.3), the possibilities appear as in Table 8.2. We see from Table 8.2 that the if statement is true only if a user with the given email both exists in the database and has the given password, exactly as required.

User Password a && b
nonexistent anything (nil && [anything]) == false
valid user wrong password (true && false) == false
valid user right password (true && true) == true
Table 8.2: Possible results of user && user.authenticate(…).

8.1.4 Rendering with a flash message

Recall from Section 7.3.3 that we displayed signup errors using the User model error messages. These errors are associated with a particular Active Record object, but this strategy won’t work here because the session isn’t an Active Record model. Instead, we’ll put a message in the flash to be displayed upon failed login. A first, slightly incorrect, attempt appears in Listing 8.6.

Listing 8.6: An (unsuccessful) attempt at handling failed login. app/controllers/sessions_controller.rb
class SessionsController < ApplicationController

  def new
  end

  def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      # Log the user in and redirect to the user's show page.
    else
      flash[:danger] = 'Invalid email/password combination' # Not quite right!
      render 'new'
    end
  end

  def destroy
  end
end

Because of the flash message display in the site layout (Listing 7.25), the flash[:danger] message automatically gets displayed; because of the Bootstrap CSS, it automatically gets nice styling (Figure 8.5).

images/figures/failed_login_flash_3rd_edition
Figure 8.5: The flash message for a failed login.

Unfortunately, as noted in the text and in the comment in Listing 8.6, this code isn’t quite right. The page looks fine, though, so what’s the problem? The issue is that the contents of the flash persist for one request, but—unlike a redirect, which we used in Listing 7.24—re-rendering a template with render doesn’t count as a request. The result is that the flash message persists one request longer than we want. For example, if we submit invalid login information and then click on the Home page, the flash gets displayed a second time (Figure 8.6). Fixing this blemish is the task of Section 8.1.5.

images/figures/flash_persistence_3rd_edition
Figure 8.6: An example of flash persistence.

8.1.5 A flash test

The incorrect flash behavior is a minor bug in our application. According to the testing guidelines from Box 3.3, this is exactly the sort of situation where we should write a test to catch the error so that it doesn’t recur. We’ll thus write a short integration test for the login form submission before proceeding. In addition to documenting the bug and preventing a regression, this will also give us a good foundation for further integration tests of login and logout.

We start by generating an integration test for our application’s login behavior:

$ rails generate integration_test users_login
      invoke  test_unit
      create    test/integration/users_login_test.rb

Next, we need a test to capture the sequence shown in Figure 8.5 and Figure 8.6. The basic steps appear as follows:

  1. Visit the login path.
  2. Verify that the new sessions form renders properly.
  3. Post to the sessions path with an invalid params hash.
  4. Verify that the new sessions form gets re-rendered and that a flash message appears.
  5. Visit another page (such as the Home page).
  6. Verify that the flash message doesn’t appear on the new page.

A test implementing the above steps appears in Listing 8.7.

Listing 8.7: A test to catch unwanted flash persistence. red test/integration/users_login_test.rb
require 'test_helper'

class UsersLoginTest < ActionDispatch::IntegrationTest

  test "login with invalid information" do
    get login_path
    assert_template 'sessions/new'
    post login_path, session: { email: "", password: "" }
    assert_template 'sessions/new'
    assert_not flash.empty?
    get root_path
    assert flash.empty?
  end
end

After adding the test in Listing 8.7, the login test should be red:

Listing 8.8: red
$ bundle exec rake test TEST=test/integration/users_login_test.rb

This shows how to run one (and only one) test file using the argument TEST and the full path to the file.

The way to get the failing test in Listing 8.7 to pass is to replace flash with the special variant flash.now, which is specifically designed for displaying flash messages on rendered pages. Unlike the contents of flash, the contents of flash.now disappear as soon as there is an additional request, which is exactly the behavior we’ve tested in Listing 8.7. With this substitution, the corrected application code appears as in Listing 8.9.

Listing 8.9: Correct code for failed login. green app/controllers/sessions_controller.rb
class SessionsController < ApplicationController

  def new
  end

  def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      # Log the user in and redirect to the user's show page.
    else
      flash.now[:danger] = 'Invalid email/password combination'
      render 'new'
    end
  end

  def destroy
  end
end

We can then verify that both the login integration test and the full test suite are green:

Listing 8.10: green
$ bundle exec rake test TEST=test/integration/users_login_test.rb
$ bundle exec rake test

8.2 Logging in

Now that our login form can handle invalid submissions, the next step is to handle valid submissions correctly by actually logging a user in. In this section, we’ll log the user in with a temporary session cookie that expires automatically upon browser close. In Section 8.4, we’ll add sessions that persist even after closing the browser.

Implementing sessions will involve defining a large number of related functions for use across multiple controllers and views. You may recall from Section 4.2.5 that Ruby provides a module facility for packaging such functions in one place. Conveniently, a Sessions helper module was generated automatically when generating the Sessions controller (Section 8.1.1). Moreover, such helpers are automatically included in Rails views; by including the module into the base class of all controllers (the Application controller), we arrange to make them available in our controllers as well (Listing 8.11).

Listing 8.11: Including the Sessions helper module into the Application controller. app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception
  include SessionsHelper
end

With this configuration complete, we’re now ready to write the code to log users in.

8.2.1 The log_in method

Logging a user in is simple with the help of the session method defined by Rails. (This method is separate and distinct from the Sessions controller generated in Section 8.1.1.) We can treat session as if it were a hash, and assign to it as follows:

session[:user_id] = user.id

This places a temporary cookie on the user’s browser containing an encrypted version of the user’s id, which allows us to retrieve the id on subsequent pages using session[:user_id]. In contrast to the persistent cookie created by the cookies method (Section 8.4), the temporary cookie created by the session method expires immediately when the browser is closed.

Because we’ll want to use the same login technique in a couple of different places, we’ll define a method called log_in in the Sessions helper, as shown in Listing 8.12.

Listing 8.12: The log_in function. app/helpers/sessions_helper.rb
module SessionsHelper

  # Logs in the given user.
  def log_in(user)
    session[:user_id] = user.id
  end
end

Because temporary cookies created using the session method are automatically encrypted, the code in Listing 8.12 is secure, and there is no way for an attacker to use the session information to log in as the user. This applies only to temporary sessions initiated with the session method, though, and is not the case for persistent sessions created using the cookies method. Permanent cookies are vulnerable to a session hijacking attack, so in Section 8.4 we’ll have to be much more careful about the information we place on the user’s browser.

With the log_in method defined in Listing 8.12, we’re now ready to complete the session create action by logging the user in and redirecting to the user’s profile page. The result appears in Listing 8.13.4

Listing 8.13: Logging in a user. app/controllers/sessions_controller.rb
class SessionsController < ApplicationController

  def new
  end

  def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      log_in user
      redirect_to user
    else
      flash.now[:danger] = 'Invalid email/password combination'
      render 'new'
    end
  end

  def destroy
  end
end

Note the compact redirect

redirect_to user

which we saw before in Section 7.4.1. Rails automatically converts this to the route for the user’s profile page:

user_url(user)

With the create action defined in Listing 8.13, the login form defined in Listing 8.2 should now be working. It doesn’t have any effects on the application display, though, so short of inspecting the browser session directly there’s no way to tell that you’re logged in. As a first step toward enabling more visible changes, in Section 8.2.2 we’ll retrieve the current user from the database using the id in the session. In Section 8.2.3, we’ll change the links on the application layout, including a URL to the current user’s profile.

8.2.2 Current user

Having placed the user’s id securely in the temporary session, we are now in a position to retrieve it on subsequent pages, which we’ll do by defining a current_user method to find the user in the database corresponding to the session id. The purpose of current_user is to allow constructions such as

<%= current_user.name %>

and

redirect_to current_user

To find the current user, one possibility is to use the find method, as on the user profile page (Listing 7.5):

User.find(session[:user_id])

But recall from Section 6.1.4 that find raises an exception if the user id doesn’t exist. This behavior is appropriate on the user profile page because it will only happen if the id is invalid, but in the present case session[:user_id] will often be nil (i.e., for non-logged-in users). To handle this possibility, we’ll use the same find_by method used to find by email address in the create method, with id in place of email:

User.find_by(id: session[:user_id])

Rather than raising an exception, this method returns nil (indicating no such user) if the id is invalid.

We could now define the current_user method as follows:

def current_user
  User.find_by(id: session[:user_id])
end

This would work fine, but it would hit the database multiple times if, e.g., current_user appeared multiple times on a page. Instead, we’ll follow a common Ruby convention by storing the result of User.find_by in an instance variable, which hits the database the first time but returns the instance variable immediately on subsequent invocations:5

if @current_user.nil?
  @current_user = User.find_by(id: session[:user_id])
else
  @current_user
end

Recalling the or operator || seen in Section 4.2.3, we can rewrite this as follows:

@current_user = @current_user || User.find_by(id: session[:user_id])

Because a User object is true in a boolean context, the call to find_by only gets executed if @current_user hasn’t yet been assigned.

Although the preceding code would work, it’s not idiomatically correct Ruby; instead, the proper way to write the assignment to @current_user is like this:

@current_user ||= User.find_by(id: session[:user_id])

This uses the potentially confusing but frequently used ||= (“or equals”) operator (Box 8.1).

Box 8.1. What the *$@! is ||= ?

The ||= (“or equals”) assignment operator is a common Ruby idiom and is thus important for aspiring Rails developers to recognize. Although at first it may seem mysterious, or equals is easy to understand by analogy.

We start by noting the common pattern of incrementing a variable:

  x = x + 1

Many languages provide a syntactic shortcut for this operation; in Ruby (and in C, C++, Perl, Python, Java, etc.), it can also appear as follows:

  x += 1

Analogous constructs exist for other operators as well:

  $ rails console
  >> x = 1
  => 1
  >> x += 1
  => 2
  >> x *= 3
  => 6
  >> x -= 8
  => -2
  >> x /= 2
  => -1

In each case, the pattern is that x = x O y and x O= y are equivalent for any operator O.

Another common Ruby pattern is assigning to a variable if it’s nil but otherwise leaving it alone. Recalling the or operator || seen in Section 4.2.3, we can write this as follows:

  >> @foo
  => nil
  >> @foo = @foo || "bar"
  => "bar"
  >> @foo = @foo || "baz"
  => "bar"

Since nil is false in a boolean context, the first assignment to @foo is nil || "bar", which evaluates to "bar". Similarly, the second assignment is @foo || "baz", i.e., "bar" || "baz", which also evaluates to "bar". This is because anything other than nil or false is true in a boolean context, and the series of || expressions terminates after the first true expression is evaluated. (This practice of evaluating || expressions from left to right and stopping on the first true value is known as short-circuit evaluation. The same principle applies to && statements, except in this case evaluation stops on the first false value.)

Comparing the console sessions for the various operators, we see that @foo = @foo || "bar" follows the x = x O y pattern with || in the place of O:

  x    =   x   +   1      ->     x     +=   1
  x    =   x   *   3      ->     x     *=   3
  x    =   x   -   8      ->     x     -=   8
  x    =   x   /   2      ->     x     /=   2
  @foo = @foo || "bar"    ->     @foo ||= "bar"

Thus we see that @foo = @foo || "bar" and @foo ||= "bar" are equivalent. In the context of the current user, this suggests the following construction:

@current_user ||= User.find_by(id: session[:user_id])

Voilà !

(By the way, under the hood Ruby actually evaluates the expression @foo || @foo = "bar", which avoids an unnecessary assignment when @foo is nil or false. But this expression doesn’t explain the ||= notation as well, so the above discussion uses the nearly equivalent @foo = @foo || "bar".)

Applying the results of the above discussion yields the succinct current_user method shown in Listing 8.14.

Listing 8.14: Finding the current user in the session. app/helpers/sessions_helper.rb
module SessionsHelper

  # Logs in the given user.
  def log_in(user)
    session[:user_id] = user.id
  end

  # Returns the current logged-in user (if any).
  def current_user
    @current_user ||= User.find_by(id: session[:user_id])
  end
end

With the working current_user method in Listing 8.14, we’re now in a position to make changes to our application based on user login status.

8.2.4 Testing layout changes

Having verified by hand that the application is behaving properly upon successful login, before moving on we’ll write an integration test to capture that behavior and catch regressions. We’ll build on the test from Listing 8.7 and write a series of steps to verify the following sequence of actions:

  1. Visit the login path.
  2. Post valid information to the sessions path.
  3. Verify that the login link disappears.
  4. Verify that a logout link appears
  5. Verify that a profile link appears.

In order to see these changes, our test needs to log in as a previously registered user, which means that such a user must already exist in the database. The default Rails way to do this is to use fixtures, which are a way of organizing data to be loaded into the test database. We discovered in Section 6.2.5 that we needed to delete the default fixtures so that our email uniqueness tests would pass (Listing 6.30). Now we’re ready to start filling in that empty file with custom fixtures of our own.

In the present case, we need only one user, whose information should consist of a valid name and email address. Because we’ll need to log the user in, we also have to include a valid password to compare with the password submitted to the Sessions controller’s create action. Referring to the data model in Figure 6.8, we see that this means creating a password_digest attribute for the user fixture, which we’ll accomplish by defining a digest method of our own.

As discussed in Section 6.3.1, the password digest is created using bcrypt (via has_secure_password), so we’ll need to create the fixture password using the same method. By inspecting the secure password source code, we find that this method is

BCrypt::Password.create(string, cost: cost)

where string is the string to be hashed and cost is the cost parameter that determines the computational cost to calculate the hash. Using a high cost makes it computationally intractable to use the hash to determine the original password, which is an important security precaution in a production environment, but in tests we want the digest method to be as fast as possible. The secure password source code has a line for this as well:

cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
                                              BCrypt::Engine.cost

This rather obscure code, which you don’t need to understand in detail, arranges for precisely the behavior described above: it uses the minimum cost parameter in tests and a normal (high) cost parameter in production. (We’ll learn more about the strange ?-: notation in Section 8.4.5.)

There are several places we could put the resulting digest method, but we’ll have an opportunity in Section 8.4.1 to reuse digest in the User model. This suggests placing the method in user.rb. Because we won’t necessarily have access to a user object when calculating the digest (as will be the case in the fixtures file), we’ll attach the digest method to the User class itself, which (as we saw briefly in Section 4.4.1) makes it a class method. The result appears in Listing 8.18.

Listing 8.18: Adding a digest method for use in fixtures. app/models/user.rb
class User < ActiveRecord::Base
  before_save { self.email = email.downcase }
  validates :name,  presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
                    format: { with: VALID_EMAIL_REGEX },
                    uniqueness: { case_sensitive: false }
  has_secure_password
  validates :password, presence: true, length: { minimum: 6 }

  # Returns the hash digest of the given string.
  def User.digest(string)
    cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
                                                  BCrypt::Engine.cost
    BCrypt::Password.create(string, cost: cost)
  end
end

With the digest method from Listing 8.18, we are now ready to create a user fixture for a valid user, as shown in Listing 8.19.10

Listing 8.19: A fixture for testing user login. test/fixtures/users.yml
michael:
  name: Michael Example
  email: michael@example.com
  password_digest: <%= User.digest('password') %>

Note in particular that fixtures support embedded Ruby, which allows us to use

<%= User.digest('password') %>

to create the valid password digest for the test user.

Although we’ve defined the password_digest attribute required by has_secure_password, sometimes it’s convenient to refer to the plain (virtual) password as well. Unfortunately, this is impossible to arrange with fixtures, and adding a password attribute to Listing 8.19 causes Rails to complain that there is no such column in the database (which is true). We’ll make do by adopting the convention that all fixture users have the same password (’password’).

Having created a fixture with a valid user, we can retrieve it inside a test as follows:

user = users(:michael)

Here users corresponds to the fixture filename users.yml, while the symbol :michael references user with the key shown in Listing 8.19.

With the fixture user as above, we can now write a test for the layout links by converting the sequence enumerated at the beginning of this section into code, as shown in Listing 8.20.

Listing 8.20: A test for user logging in with valid information. green test/integration/users_login_test.rb
require 'test_helper'

class UsersLoginTest < ActionDispatch::IntegrationTest

  def setup
    @user = users(:michael)
  end
  .
  .
  .
  test "login with valid information" do
    get login_path
    post login_path, session: { email: @user.email, password: 'password' }
    assert_redirected_to @user
    follow_redirect!
    assert_template 'users/show'
    assert_select "a[href=?]", login_path, count: 0
    assert_select "a[href=?]", logout_path
    assert_select "a[href=?]", user_path(@user)
  end
end

Here we’ve used

assert_redirected_to @user

to check the right redirect target and

follow_redirect!

to actually visit the target page. Listing 8.20 also verifies that the login link disappears by verifying that there are zero login path links on the page:

assert_select "a[href=?]", login_path, count: 0

By including the extra count: 0 option, we tell assert_select that we expect there to be zero links matching the given pattern. (Compare to count: 2 in Listing 5.26, which checks for exactly two matching links.)

Because the application code was already working, this test should be green:

Listing 8.21: green
$ bundle exec rake test TEST=test/integration/users_login_test.rb \
>                       TESTOPTS="--name test_login_with_valid_information"

This shows how to run a specific test within a test file by passing the option

TESTOPTS="--name test_login_with_valid_information"

containing the name of the test. (A test’s name is just the word “test” and the words in the test description joined using underscores.) Note that the > on the second line is a “line continuation” character inserted automatically by the shell, and should not be typed literally.

8.2.5 Login upon signup

Although our authentication system is now working, newly registered users might be confused, as they are not logged in by default. Because it would be strange to force users to log in immediately after signing up, we’ll log in new users automatically as part of the signup process. To arrange this behavior, all we need to do is add a call to log_in in the Users controller create action, as shown in Listing 8.22.11

Listing 8.22: Logging in the user upon signup. app/controllers/users_controller.rb
class UsersController < ApplicationController

  def show
    @user = User.find(params[:id])
  end

  def new
    @user = User.new
  end

  def create
    @user = User.new(user_params)
    if @user.save
      log_in @user
      flash[:success] = "Welcome to the Sample App!"
      redirect_to @user
    else
      render 'new'
    end
  end

  private

    def user_params
      params.require(:user).permit(:name, :email, :password,
                                   :password_confirmation)
    end
end

To test the behavior from Listing 8.22, we can add a line to the test from Listing 7.26 to check that the user is logged in. It’s helpful in this context to define an is_logged_in? helper method to parallel the logged_in? helper defined in Listing 8.15, which returns true if there’s a user id in the (test) session and false otherwise (Listing 8.23). (Because helper methods aren’t available in tests, we can’t use the current_user as in Listing 8.15, but the session method is available, so we use that instead.) Here we use is_logged_in? instead of logged_in? so that the test helper and Sessions helper methods have different names, which prevents them from being mistaken for each other.12

Listing 8.23: A boolean method for login status inside tests. test/test_helper.rb
ENV['RAILS_ENV'] ||= 'test'
.
.
.
class ActiveSupport::TestCase
  fixtures :all

  # Returns true if a test user is logged in.
  def is_logged_in?
    !session[:user_id].nil?
  end
end

With the code in Listing 8.23, we can assert that the user is logged in after signup using the line shown in Listing 8.24.

Listing 8.24: A test of login after signup. green test/integration/users_signup_test.rb
require 'test_helper'

class UsersSignupTest < ActionDispatch::IntegrationTest
  .
  .
  .
  test "valid signup information" do
    get signup_path
    assert_difference 'User.count', 1 do
      post_via_redirect users_path, user: { name:  "Example User",
                                            email: "user@example.com",
                                            password:              "password",
                                            password_confirmation: "password" }
    end
    assert_template 'users/show'
    assert is_logged_in?
  end
end

At this point, the test suite should still be green:

Listing 8.25: green
$ bundle exec rake test

8.3 Logging out

As discussed in Section 8.1, our authentication model is to keep users logged in until they log out explicitly. In this section, we’ll add this necessary logout capability. Because the “Log out” link has already been defined (Listing 8.16), all we need is to write a valid controller action to destroy user sessions.

So far, the Sessions controller actions have followed the RESTful convention of using new for a login page and create to complete the login. We’ll continue this theme by using a destroy action to delete sessions, i.e., to log out. Unlike the login functionality, which we use in both Listing 8.13 and Listing 8.22, we’ll only be logging out in one place, so we’ll put the relevant code directly in the destroy action. As we’ll see in Section 8.4.6, this design (with a little refactoring) will also make the authentication machinery easier to test.

Logging out involves undoing the effects of the log_in method from Listing 8.12, which involves deleting the user id from the session.13 To do this, we use the delete method as follows:

session.delete(:user_id)

We’ll also set the current user to nil, although in the present case this won’t matter because of an immediate redirect to the root URL.14 As with log_in and associated methods, we’ll put the resulting log_out method in the Sessions helper module, as shown in Listing 8.26.

Listing 8.26: The log_out method. app/helpers/sessions_helper.rb
module SessionsHelper

  # Logs in the given user.
  def log_in(user)
    session[:user_id] = user.id
  end
  .
  .
  .
  # Logs out the current user.
  def log_out
    session.delete(:user_id)
    @current_user = nil
  end
end

We can put the log_out method to use in the Sessions controller’s destroy action, as shown in Listing 8.27.

Listing 8.27: Destroying a session (user logout). app/controllers/sessions_controller.rb
class SessionsController < ApplicationController

  def new
  end

  def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      log_in user
      redirect_to user
    else
      flash.now[:danger] = 'Invalid email/password combination'
      render 'new'
    end
  end

  def destroy
    log_out
    redirect_to root_url
  end
end

To test the logout machinery, we can add some steps to the user login test from Listing 8.20. After logging in, we use delete to issue a DELETE request to the logout path (Table 8.1) and verify that the user is logged out and redirected to the root URL. We also check that the login link reappears and that the logout and profile links disappear. The new steps appear in Listing 8.28.

Listing 8.28: A test for user logout. green test/integration/users_login_test.rb
require 'test_helper'

class UsersLoginTest < ActionDispatch::IntegrationTest
  .
  .
  .
  test "login with valid information followed by logout" do
    get login_path
    post login_path, session: { email: @user.email, password: 'password' }
    assert is_logged_in?
    assert_redirected_to @user
    follow_redirect!
    assert_template 'users/show'
    assert_select "a[href=?]", login_path, count: 0
    assert_select "a[href=?]", logout_path
    assert_select "a[href=?]", user_path(@user)
    delete logout_path
    assert_not is_logged_in?
    assert_redirected_to root_url
    follow_redirect!
    assert_select "a[href=?]", login_path
    assert_select "a[href=?]", logout_path,      count: 0
    assert_select "a[href=?]", user_path(@user), count: 0
  end
end

(Now that we have is_logged_in? available in tests, we’ve also thrown in a bonus assert is_logged_in? immediately after posting valid information to the sessions path.)

With the session destroy action thus defined and tested, the initial signup/login/logout triumvirate is complete, and the test suite should be green:

Listing 8.29: green
$ bundle exec rake test

8.4 Remember me

The login system we finished in Section 8.2 is self-contained and fully functional, but most websites have the additional capability of remembering users’ sessions even after they close their browsers. In this section, we’ll start by remembering user logins by default, expiring their sessions only when they explicitly log out. In Section 8.4.5, we’ll enable a common alternative model, a “remember me” checkbox that allows users to opt out of being remembered. Both of these models are professional-grade, with the first used by sites such as GitHub and Bitbucket, and the second used by sites such as Facebook and Twitter.

8.4.1 Remember token and digest

In Section 8.2, we used the Rails session method to store the user’s id, but this information disappears when the user closes their browser. In this section, we’ll take the first step toward persistent sessions by generating a remember token appropriate for creating permanent cookies using the cookies method, together with a secure remember digest for authenticating those tokens.

As noted in Section 8.2.1, information stored using session is automatically secure, but this is not the case with information stored using cookies. In particular, persistent cookies are vulnerable to session hijacking, in which an attacker uses a stolen remember token to log in as a particular user. There are four main ways to steal cookies: (1) using a packet sniffer to detect cookies being passed over insecure networks,15 (2) compromising a database containing remember tokens, (3) using cross-site scripting (XSS), and (4) gaining physical access to a machine with a logged-in user. We prevented the first problem in Section 7.5 by using Secure Sockets Layer (SSL) site-wide, which protects network data from packet sniffers. We’ll prevent the second problem by storing a hash digest of the remember token instead of the token itself, in much the same way that we stored password digests instead of raw passwords in Section 6.3. Rails automatically prevents the third problem by escaping any content inserted into view templates. Finally, although there’s no iron-clad way to stop attackers who have physical access to a logged-in computer, we’ll minimize the fourth problem by changing tokens every time a user logs out and by taking care to cryptographically sign any potentially sensitive information we place on the browser.

With these design and security considerations in mind, our plan for creating persistent sessions appears as follows:

  1. Create a random string of digits for use as a remember token.
  2. Place the token in the browser cookies with an expiration date far in the future.
  3. Save the hash digest of the token to the database.
  4. Place an encrypted version of the user’s id in the browser cookies.
  5. When presented with a cookie containing a persistent user id, find the user in the database using the given id, and verify that the remember token cookie matches the associated hash digest from the database.

Note how similar the final step is to logging a user in, where we retrieve the user by email address and then verify (using the authenticate method) that the submitted password matches the password digest (Listing 8.5). As a result, our implementation will parallel aspects of has_secure_password.

We’ll start by adding the required remember_digest attribute to the User model, as shown in Figure 8.9.

user_model_remember_digest
Figure 8.9: The User model with an added remember_digest attribute.

To add the data model from Figure 8.9 to our application, we’ll generate a migration:

$ rails generate migration add_remember_digest_to_users remember_digest:string

(Compare to the password digest migration in Section 6.3.1.) As in previous migrations, we’ve used a migration name that ends in _to_users to tell Rails that the migration is designed to alter the users table in the database. Because we also included the attribute (remember_digest) and type (string), Rails generates a default migration for us, as shown in Listing 8.30.

Listing 8.30: The generated migration for the remember digest. db/migrate/[timestamp]_add_remember_digest_to_users.rb
class AddRememberDigestToUsers < ActiveRecord::Migration
  def change
    add_column :users, :remember_digest, :string
  end
end

Because we don’t expect to retrieve users by remember digest, there’s no need to put an index on the remember_digest column, and we can use the default migration as generated above:

$ bundle exec rake db:migrate

Now we have to decide what to use as a remember token. There are many mostly equivalent possibilities—essentially, any long random string will do. The urlsafe_base64 method from the SecureRandom module in the Ruby standard library fits the bill:16 it returns a random string of length 22 composed of the characters A–Z, a–z, 0–9, “-”, and “_” (for a total of 64 possibilities, thus “base64”). A typical base64 string appears as follows:

$ rails console
>> SecureRandom.urlsafe_base64
=> "q5lt38hQDc_959PVoo6b7A"

Just as it’s perfectly fine if two users have the same password,17 there’s no need for remember tokens to be unique, but it’s more secure if they are.18 In the case of the base64 string above, each of the 22 characters has 64 possibilities, so the probability of two remember tokens colliding is a negligibly small \( 1/64^{22} = 2^{-132} \approx 10^{-40} \). As a bonus, by using base64 strings specifically designed to be safe in URLs (as indicated by the name urlsafe_base64), we’ll be able to use the same token generator to make account activation and password reset links in Chapter 10.

Remembering users involves creating a remember token and saving the digest of the token to the database. We’ve already defined a digest method for use in the test fixtures (Listing 8.18), and we can use the results of the discussion above to create a new_token method to create a new token. As with digest, the new token method doesn’t need a user object, so we’ll make it a class method.19 The result is the User model shown in Listing 8.31.

Listing 8.31: Adding a method for generating tokens. app/models/user.rb
class User < ActiveRecord::Base
  before_save { self.email = email.downcase }
  validates :name,  presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
                    format: { with: VALID_EMAIL_REGEX },
                    uniqueness: { case_sensitive: false }
  has_secure_password
  validates :password, presence: true, length: { minimum: 6 }

  # Returns the hash digest of the given string.
  def User.digest(string)
    cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
                                                  BCrypt::Engine.cost
    BCrypt::Password.create(string, cost: cost)
  end

  # Returns a random token.
  def User.new_token
    SecureRandom.urlsafe_base64
  end
end

Our plan for the implementation is to make a user.remember method that associates a remember token with the user and saves the corresponding remember digest to the database. Because of the migration in Listing 8.30, the User model already has a remember_digest attribute, but it doesn’t yet have a remember_token attribute. We need a way to make a token available via user.remember_token (for storage in the cookies) without storing it in the database. We solved a similar issue with secure passwords in Section 6.3, which paired a virtual password attribute with a secure password_digest attribute in the database. In that case, the virtual password attribute was created automatically by has_secure_password, but we’ll have to write the code for a remember_token ourselves. The way to do this is to use attr_accessor to create an accessible attribute, which we saw before in Section 4.4.5:

class User < ActiveRecord::Base
  attr_accessor :remember_token
  .
  .
  .
  def remember
    self.remember_token = ...
    update_attribute(:remember_digest, ...)
  end
end

Note the form of the assignment in the first line of the remember method. Because of the way Ruby handles assignments inside objects, without self the assignment would create a local variable called remember_token, which isn’t what we want. Using self ensures that assignment sets the user’s remember_token attribute. (Now you know why the before_save callback from Listing 6.31 uses self.email instead of just email.) Meanwhile, the second line of remember uses the update_attribute method to update the remember digest. (As noted in Section 6.1.5, this method bypasses the validations, which is necessary in this case because we don’t have access to the user’s password or confirmation.)

With these considerations in mind, we can create a valid token and associated digest by first making a new remember token using User.new_token, and then updating the remember digest with the result of applying User.digest. This procedure gives the remember method shown in Listing 8.32.

Listing 8.32: Adding a remember method to the User model. green app/models/user.rb
class User < ActiveRecord::Base
  attr_accessor :remember_token
  before_save { self.email = email.downcase }
  validates :name,  presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
                    format: { with: VALID_EMAIL_REGEX },
                    uniqueness: { case_sensitive: false }
  has_secure_password
  validates :password, presence: true, length: { minimum: 6 }

  # Returns the hash digest of the given string.
  def User.digest(string)
    cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
                                                  BCrypt::Engine.cost
    BCrypt::Password.create(string, cost: cost)
  end

  # Returns a random token.
  def User.new_token
    SecureRandom.urlsafe_base64
  end

  # Remembers a user in the database for use in persistent sessions.
  def remember
    self.remember_token = User.new_token
    update_attribute(:remember_digest, User.digest(remember_token))
  end
end

8.4.2 Login with remembering

Having created a working user.remember method, we’re now in a position to create a persistent session by storing a user’s (encrypted) id and remember token as permanent cookies on the browser. The way to do this is with the cookies method, which (as with session) we can treat as a hash. A cookie consists of two pieces of information, a value and an optional expires date. For example, we could make a persistent session by creating a cookie with value equal to the remember token that expires 20 years from now:

cookies[:remember_token] = { value:   remember_token,
                             expires: 20.years.from_now.utc }

(This uses one of the convenient Rails time helpers, as discussed in Box 8.2.) This pattern of setting a cookie that expires 20 years in the future is so common that Rails has a special permanent method to implement it, so that we can simply write

cookies.permanent[:remember_token] = remember_token

This causes Rails to set the expiration to 20.years.from_now automatically.

Box 8.2. Cookies expire 20.years.from_now

You may recall from Section 4.4.2 that Ruby lets you add methods to any class, even built-in ones. In that section, we added a palindrome? method to the String class (and discovered as a result that "deified" is a palindrome), and we also saw how Rails adds a blank? method to class Object (so that "".blank?, " ".blank?, and nil.blank? are all true). The cookies.permanent method, which creates “permanent” cookies with an expiration 20.years.from_now, gives yet another example of this practice through one of Rails’ time helpers, which are methods added to Fixnum (the base class for integers):

  $ rails console
  >> 1.year.from_now
  => Sun, 09 Aug 2015 16:48:17 UTC +00:00
  >> 10.weeks.ago
  => Sat, 31 May 2014 16:48:45 UTC +00:00

Rails adds other helpers, too:

  >> 1.kilobyte
  => 1024
  >> 5.megabytes
  => 5242880

These are useful for upload validations, making it easy to restrict, say, image uploads to 5.megabytes.

Although it should be used with caution, the flexibility to add methods to built-in classes allows for extraordinarily natural additions to plain Ruby. Indeed, much of the elegance of Rails ultimately derives from the malleability of the underlying Ruby language.

To store the user’s id in the cookies, we could follow the pattern used with the session method (Listing 8.12) using something like

cookies[:user_id] = user.id

Because it places the id as plain text, this method exposes the form of the application’s cookies and makes it easier for an attacker to compromise user accounts. To avoid this problem, we’ll use a signed cookie, which securely encrypts the cookie before placing it on the browser:

cookies.signed[:user_id] = user.id

Because we want the user id to be paired with the permanent remember token, we should make it permanent as well, which we can do by chaining the signed and permanent methods:

cookies.permanent.signed[:user_id] = user.id

After the cookies are set, on subsequent page views we can retrieve the user with code like

User.find_by(id: cookies.signed[:user_id])

where cookies.signed[:user_id] automatically decrypts the user id cookie. We can then use bcrypt to verify that cookies[:remember_token] matches the remember_digest generated in Listing 8.32. (In case you’re wondering why we don’t just use the signed user id, without the remember token, this would allow an attacker with possession of the encrypted id to log in as the user in perpetuity. In the present design, an attacker with both cookies can log in as the user only until the user logs out.)

The final piece of the puzzle is to verify that a given remember token matches the user’s remember digest, and in this context there are a couple of equivalent ways to use bcrypt to verify a match. If you look at the secure password source code, you’ll find a comparison like this:20

BCrypt::Password.new(password_digest) == unencrypted_password

In our case, the analogous code would look like this:

BCrypt::Password.new(remember_digest) == remember_token

If you think about it, this code is really strange: it appears to be comparing a bcrypt password digest directly with a token, which would imply decrypting the digest in order to compare using ==. But the whole point of using bcrypt is for hashing to be irreversible, so this can’t be right. Indeed, digging into the source code of the bcrypt gem verifies that the comparison operator == is being redefined, and under the hood the comparison above is equivalent to the following:

BCrypt::Password.new(remember_digest).is_password?(remember_token)

Instead of ==, this uses the boolean method is_password? to perform the comparison. Because its meaning is a little clearer, we’ll prefer this second comparison form in the application code.

The above discussion suggests putting the digest–token comparison into an authenticated? method in the User model, which plays a similar role to the authenticate method provided by has_secure_password for authenticating a user (Listing 8.13). The implementation appears in Listing 8.33. (Although the authenticated? method in Listing 8.33 is tied specifically to the remember digest, it will turn out to be useful in other contexts as well, and we’ll generalize it in Chapter 10.)

Listing 8.33: Adding an authenticated? method to the User model. app/models/user.rb
class User < ActiveRecord::Base
  attr_accessor :remember_token
  before_save { self.email = email.downcase }
  validates :name,  presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
                    format: { with: VALID_EMAIL_REGEX },
                    uniqueness: { case_sensitive: false }
  has_secure_password
  validates :password, presence: true, length: { minimum: 6 }

  # Returns the hash digest of the given string.
  def User.digest(string)
    cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
                                                  BCrypt::Engine.cost
    BCrypt::Password.create(string, cost: cost)
  end

  # Returns a random token.
  def User.new_token
    SecureRandom.urlsafe_base64
  end

  # Remembers a user in the database for use in persistent sessions.
  def remember
    self.remember_token = User.new_token
    update_attribute(:remember_digest, User.digest(remember_token))
  end

  # Returns true if the given token matches the digest.
  def authenticated?(remember_token)
    BCrypt::Password.new(remember_digest).is_password?(remember_token)
  end
end

Note that the remember_token argument in the authenticated? method defined in Listing 8.33 is not the same as the accessor that we defined in Listing 8.32 using attr_accessor :remember_token; instead, it is a variable local to the method. (Because the argument refers to the remember token, it is not uncommon to use a method argument that has the same name.) Also note the use of the remember_digest attribute, which is the same as self.remember_digest and, like name and email in Chapter 6, is created automatically by Active Record based on the name of the corresponding database column (Listing 8.30).

We’re now in a position to remember a logged-in user, which we’ll do by adding a remember helper to go along with log_in, as shown in Listing 8.34.

Listing 8.34: Logging in and remembering a user. app/controllers/sessions_controller.rb
class SessionsController < ApplicationController

  def new
  end

  def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      log_in user
      remember user
      redirect_to user
    else
      flash.now[:danger] = 'Invalid email/password combination'
      render 'new'
    end
  end

  def destroy
    log_out
    redirect_to root_url
  end
end

As with log_in, Listing 8.34 defers the real work to the Sessions helper, where we define a remember method that calls user.remember, thereby generating a remember token and saving its digest to the database. It then uses cookies to create permanent cookies for the user id and remember token as described above. The result appears in Listing 8.35.

Listing 8.35: Remembering the user. app/helpers/sessions_helper.rb
module SessionsHelper

  # Logs in the given user.
  def log_in(user)
    session[:user_id] = user.id
  end

  # Remembers a user in a persistent session.
  def remember(user)
    user.remember
    cookies.permanent.signed[:user_id] = user.id
    cookies.permanent[:remember_token] = user.remember_token
  end

  # Returns the current logged-in user (if any).
  def current_user
    @current_user ||= User.find_by(id: session[:user_id])
  end

  # Returns true if the user is logged in, false otherwise.
  def logged_in?
    !current_user.nil?
  end

  # Logs out the current user.
  def log_out
    session.delete(:user_id)
    @current_user = nil
  end
end

With the code in Listing 8.35, a user logging in will be remembered in the sense that their browser will get a valid remember token, but it doesn’t yet do us any good because the current_user method defined in Listing 8.14 knows only about the temporary session:

@current_user ||= User.find_by(id: session[:user_id])

In the case of persistent sessions, we want to retrieve the user from the temporary session if session[:user_id] exists, but otherwise we should look for cookies[:user_id] to retrieve (and log in) the user corresponding to the persistent session. We can accomplish this as follows:

if session[:user_id]
  @current_user ||= User.find_by(id: session[:user_id])
elsif cookies.signed[:user_id]
  user = User.find_by(id: cookies.signed[:user_id])
  if user && user.authenticated?(cookies[:remember_token])
    log_in user
    @current_user = user
  end
end

(This follows the same user && user.authenticated pattern we saw in Listing 8.5.) The code above will work, but note the repeated use of both session and cookies. We can eliminate this duplication as follows:

if (user_id = session[:user_id])
  @current_user ||= User.find_by(id: user_id)
elsif (user_id = cookies.signed[:user_id])
  user = User.find_by(id: user_id)
  if user && user.authenticated?(cookies[:remember_token])
    log_in user
    @current_user = user
  end
end

This uses the common but potentially confusing construction

if (user_id = session[:user_id])

Despite appearances, this is not a comparison (which would use double-equals ==), but rather is an assignment. If you were to read it in words, you wouldn’t say “If user id equals session of user id…”, but rather something like “If session of user id exists (while setting user id to session of user id)…”.21

Defining the current_user helper as discussed above leads to the implementation shown in Listing 8.36.

Listing 8.36: Updating current_user for persistent sessions. red app/helpers/sessions_helper.rb
module SessionsHelper

  # Logs in the given user.
  def log_in(user)
    session[:user_id] = user.id
  end

  # Remembers a user in a persistent session.
  def remember(user)
    user.remember
    cookies.permanent.signed[:user_id] = user.id
    cookies.permanent[:remember_token] = user.remember_token
  end

  # Returns the user corresponding to the remember token cookie.
  def current_user
    if (user_id = session[:user_id])
      @current_user ||= User.find_by(id: user_id)
    elsif (user_id = cookies.signed[:user_id])
      user = User.find_by(id: user_id)
      if user && user.authenticated?(cookies[:remember_token])
        log_in user
        @current_user = user
      end
    end
  end

  # Returns true if the user is logged in, false otherwise.
  def logged_in?
    !current_user.nil?
  end

  # Logs out the current user.
  def log_out
    session.delete(:user_id)
    @current_user = nil
  end
end

With the code as in Listing 8.36, newly logged in users are correctly remembered, as you can verify by logging in, closing the browser, and checking that you’re still logged in when you restart the sample application and revisit the sample application. If you want, you can even inspect the browser cookies to see the result directly (Figure 8.10).22

There’s only one problem with our application as it stands: short of clearing their browser cookies (or waiting 20 years), there’s no way for users to log out. This is exactly the sort of thing our test suite should catch, and indeed it should currently be red:

Listing 8.37: red
$ bundle exec rake test

8.4.3 Forgetting users

To allow users to log out, we’ll define methods to forget users in analogy with the ones to remember them. The resulting user.forget method just undoes user.remember by updating the remember digest with nil, as shown in Listing 8.38.

Listing 8.38: Adding a forget method to the User model. app/models/user.rb
class User < ActiveRecord::Base
  attr_accessor :remember_token
  before_save { self.email = email.downcase }
  validates :name,  presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
                    format: { with: VALID_EMAIL_REGEX },
                    uniqueness: { case_sensitive: false }
  has_secure_password
  validates :password, presence: true, length: { minimum: 6 }

  # Returns the hash digest of the given string.
  def User.digest(string)
    cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
                                                  BCrypt::Engine.cost
    BCrypt::Password.create(string, cost: cost)
  end

  # Returns a random token.
  def User.new_token
    SecureRandom.urlsafe_base64
  end

  # Remembers a user in the database for use in persistent sessions.
  def remember
    self.remember_token = User.new_token
    update_attribute(:remember_digest, User.digest(remember_token))
  end

  # Returns true if the given token matches the digest.
  def authenticated?(remember_token)
    BCrypt::Password.new(remember_digest).is_password?(remember_token)
  end

  # Forgets a user.
  def forget
    update_attribute(:remember_digest, nil)
  end
end

With the code in Listing 8.38, we’re now ready to forget a permanent session by adding a forget helper and calling it from the log_out helper (Listing 8.39). As seen in Listing 8.39, the forget helper calls user.forget and then deletes the user_id and remember_token cookies.

Listing 8.39: Logging out from a persistent session. app/helpers/sessions_helper.rb
module SessionsHelper

  # Logs in the given user.
  def log_in(user)
    session[:user_id] = user.id
  end
  .
  .
  .
  # Forgets a persistent session.
  def forget(user)
    user.forget
    cookies.delete(:user_id)
    cookies.delete(:remember_token)
  end

  # Logs out the current user.
  def log_out
    forget(current_user)
    session.delete(:user_id)
    @current_user = nil
  end
end

8.4.4 Two subtle bugs

There are two closely related subtleties left to address. The first subtlety is that, even though the “Log out” link appears only when logged-in, a user could potentially have multiple browser windows open to the site. If the user logged out in one window, thereby setting current_user to nil, clicking the “Log out” link in a second window would result in an error because of forget(current_user) in the log_out method (Listing 8.39).23 We can avoid this by logging out only if the user is logged in.

The second subtlety is that a user could be logged in (and remembered) in multiple browsers, such as Chrome and Firefox, which causes a problem if the user logs out in the first browser but not the second, and then closes and re-opens the second one.24 For example, suppose that the user logs out in Firefox, thereby setting the remember digest to nil (via user.forget in Listing 8.38). The application will still work in Firefox; because the log_out method in Listing 8.39 deletes the user’s id, both highlighted conditionals are false:

# Returns the user corresponding to the remember token cookie.
def current_user
  if (user_id = session[:user_id])
    @current_user ||= User.find_by(id: user_id)
  elsif (user_id = cookies.signed[:user_id])
    user = User.find_by(id: user_id)
    if user && user.authenticated?(cookies[:remember_token])
      log_in user
      @current_user = user
    end
  end
end

As a result, evaluation falls off the end of the current_user method, thereby returning nil as required.

In contrast, if we close Chrome, we set session[:user_id] to nil (because all session variables expire automatically on browser close), but the user_id cookie will still be present. This means that the corresponding user will still be pulled out of the database:

# Returns the user corresponding to the remember token cookie.
def current_user
  if (user_id = session[:user_id])
    @current_user ||= User.find_by(id: user_id)
  elsif (user_id = cookies.signed[:user_id])
    user = User.find_by(id: user_id)
    if user && user.authenticated?(cookies[:remember_token])
      log_in user
      @current_user = user
    end
  end
end

Consequently, the inner if conditional will be evaluated:

user && user.authenticated?(cookies[:remember_token])

In particular, because user isn’t nil, the second expression will be evaluated, which raises an error. This is because the user’s remember digest was deleted as part of logging out (Listing 8.38) in Firefox, so when we access the application in Chrome we end up calling

BCrypt::Password.new(remember_digest).is_password?(remember_token)

with a nil remember digest, thereby raising an exception inside the bcrypt library. To fix this, we want authenticated? to return false instead.

These are exactly the sorts of subtleties that benefit from test-driven development, so we’ll write tests to catch the two errors before correcting them. We first get the integration test from Listing 8.28 to red, as shown in Listing 8.40.

Listing 8.40: A test for user logout. red test/integration/users_login_test.rb
require 'test_helper'

class UsersLoginTest < ActionDispatch::IntegrationTest
  .
  .
  .
  test "login with valid information followed by logout" do
    get login_path
    post login_path, session: { email: @user.email, password: 'password' }
    assert is_logged_in?
    assert_redirected_to @user
    follow_redirect!
    assert_template 'users/show'
    assert_select "a[href=?]", login_path, count: 0
    assert_select "a[href=?]", logout_path
    assert_select "a[href=?]", user_path(@user)
    delete logout_path
    assert_not is_logged_in?
    assert_redirected_to root_url
    # Simulate a user clicking logout in a second window.
    delete logout_path
    follow_redirect!
    assert_select "a[href=?]", login_path
    assert_select "a[href=?]", logout_path,      count: 0
    assert_select "a[href=?]", user_path(@user), count: 0
  end
end

The second call to delete logout_path in Listing 8.40 should raise an error due to the missing current_user, leading to a red test suite:

Listing 8.41: red
$ bundle exec rake test

The application code simply involves calling log_out only if logged_in? is true, as shown in Listing 8.42.

Listing 8.42: Only logging out if logged in. green app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
  .
  .
  .
  def destroy
    log_out if logged_in?
    redirect_to root_url
  end
end

The second case, involving a scenario with two different browsers, is harder to simulate with an integration test, but it’s easy to check in the User model test directly. All we need is to start with a user that has no remember digest (which is true for the @user variable defined in the setup method) and then call authenticated?, as shown in Listing 8.43. (Note that we’ve just left the remember token blank; it doesn’t matter what its value is, because the error occurs before it ever gets used.)

Listing 8.43: A test of authenticated? with a nonexistent digest. red test/models/user_test.rb
require 'test_helper'

class UserTest < ActiveSupport::TestCase

  def setup
    @user = User.new(name: "Example User", email: "user@example.com",
                     password: "foobar", password_confirmation: "foobar")
  end
  .
  .
  .
  test "authenticated? should return false for a user with nil digest" do
    assert_not @user.authenticated?('')
  end
end

Because BCrypt::Password.new(nil) raises an error, the test suite should now be red:

Listing 8.44: red
$ bundle exec rake test

To fix the error and get to green, all we need to do is return false if the remember digest is nil, as shown in Listing 8.45.

Listing 8.45: Updating authenticated? to handle a nonexistent digest. green app/models/user.rb
class User < ActiveRecord::Base
  .
  .
  .
  # Returns true if the given token matches the digest.
  def authenticated?(remember_token)
    return false if remember_digest.nil?
    BCrypt::Password.new(remember_digest).is_password?(remember_token)
  end

  # Forgets a user.
  def forget
    update_attribute(:remember_digest, nil)
  end
end

This uses the return keyword to return immediately if the remember digest is nil, which is a common way to emphasize that the rest of the method gets ignored in that case. The equivalent code

if remember_digest.nil?
  false
else
  BCrypt::Password.new(remember_digest).is_password?(remember_token)
end

would also work fine, but I prefer the explicitness of the version in Listing 8.45 (which also happens to be slightly shorter).

With the code in Listing 8.45, our full test suite should be green, and both subtleties should now be addressed:

Listing 8.46: green
$ bundle exec rake test

8.4.5 “Remember me” checkbox

With the code in Section 8.4.3, our application has a complete, professional-grade authentication system. As a final step, we’ll see how to make staying logged in optional using a “remember me” checkbox. A mockup of the login form with such a checkbox appears in Figure 8.11.

images/figures/login_remember_me_mockup
Figure 8.11: A mockup of a “remember me” checkbox.

To write the implementation, we start by adding a checkbox to the login form from Listing 8.2. As with labels, text fields, password fields, and submit buttons, checkboxes can be created with a Rails helper method. In order to get the styling right, though, we have to nest the checkbox inside the label, as follows:

<%= f.label :remember_me, class: "checkbox inline" do %>
  <%= f.check_box :remember_me %>
  <span>Remember me on this computer</span>
<% end %>

Putting this into the login form gives the code shown in Listing 8.47.

Listing 8.47: Adding a “remember me” checkbox to the login form. app/views/sessions/new.html.erb
<% provide(:title, "Log in") %>
<h1>Log in</h1>

<div class="row">
  <div class="col-md-6 col-md-offset-3">
    <%= form_for(:session, url: login_path) do |f| %>

      <%= f.label :email %>
      <%= f.email_field :email, class: 'form-control' %>

      <%= f.label :password %>
      <%= f.password_field :password, class: 'form-control' %>

      <%= f.label :remember_me, class: "checkbox inline" do %>
        <%= f.check_box :remember_me %>
        <span>Remember me on this computer</span>
      <% end %>

      <%= f.submit "Log in", class: "btn btn-primary" %>
    <% end %>

    <p>New user? <%= link_to "Sign up now!", signup_path %></p>
  </div>
</div>

In Listing 8.47, we’ve included the CSS classes checkbox and inline, which Bootstrap uses to put the checkbox and the text (“Remember me on this computer”) in the same line. In order to complete the styling, we need just a few more CSS rules, as shown in Listing 8.48. The resulting login form appears in Figure 8.12.

Listing 8.48: CSS for the “remember me” checkbox. app/assets/stylesheets/custom.css.scss
.
.
.
/* forms */
.
.
.
.checkbox {
  margin-top: -10px;
  margin-bottom: 10px;
  span {
    margin-left: 20px;
    font-weight: normal;
  }
}

#session_remember_me {
  width: auto;
  margin-left: 0;
}
images/figures/login_form_remember_me
Figure 8.12: The login form with an added “remember me” checkbox.

Having edited the login form, we’re now ready to remember users if they check the checkbox and forget them otherwise. Incredibly, because of all our work in the previous sections, the implementation can be reduced to one line. We start by noting that the params hash for submitted login forms now includes a value based on the checkbox (as you can verify by submitting the form in Listing 8.47 with invalid information and inspecting the values in the debug section of the page). In particular, the value of

params[:session][:remember_me]

is ’1’ if the box is checked and ’0’ if it isn’t.

By testing the relevant value of the params hash, we can now remember or forget the user based on the value of the submission:25

if params[:session][:remember_me] == '1'
  remember(user)
else
  forget(user)
end

As explained in Box 8.3, this sort of if-then branching structure can be converted to one line using the ternary operator as follows:26

params[:session][:remember_me] == '1' ? remember(user) : forget(user)

Adding this to the Sessions controller’s create method leads to the amazingly compact code shown in Listing 8.49. (Now you’re in a position to understand the code in Listing 8.18, which uses the ternary operator to define the bcrypt cost variable.)

Listing 8.49: Handling the submission of the “remember me” checkbox. app/controllers/sessions_controller.rb
class SessionsController < ApplicationController

  def new
  end

  def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      log_in user
      params[:session][:remember_me] == '1' ? remember(user) : forget(user)
      redirect_to user
    else
      flash.now[:danger] = 'Invalid email/password combination'
      render 'new'
    end
  end

  def destroy
    log_out if logged_in?
    redirect_to root_url
  end
end

With the implementation in Listing 8.49, our login system is complete, as you can verify by checking or unchecking the box in your browser.

Box 8.3. 10 types of people

There’s an old joke that there are 10 kinds of people in the world: those who understand binary and those who don’t (10, of course, being 2 in binary). In this spirit, we can say that there are 10 kinds of people in the world: those who like the ternary operator, those who don’t, and those who don’t yet know about it. (If you happen to be in the third category, soon you won’t be any longer.)

When you do a lot of programming, you quickly learn that one of the most common bits of control flow goes something like this:

  if boolean?
    do_one_thing
  else
    do_something_else
  end

Ruby, like many other languages (including C/C++, Perl, PHP, and Java), allows you to replace this with a much more compact expression using the ternary operator (so called because it consists of three parts):

  boolean? ? do_one_thing : do_something_else

You can also use the ternary operator to replace assignment, so that

  if boolean?
    var = foo
  else
    var = bar
  end

becomes

  var = boolean? ? foo : bar

Finally, it’s often convenient to use the ternary operator in a function’s return value:

  def foo
    do_stuff
    boolean? ? "bar" : "baz"
  end

Since Ruby implicitly returns the value of the last expression in a function, here the foo method returns "bar" or "baz" depending on whether boolean? is true or false.

8.4.6 Remember tests

Although our “remember me” functionality is now working, it’s important to write some tests to verify its behavior. One reason is to catch implementation errors, as discussed in a moment. Even more important, though, is that the core user persistence code is in fact completely untested at present. Fixing these issues will require some trickery, but the result will be a far more powerful test suite.

Testing the “remember me” box

When I originally implemented the checkbox handling in Listing 8.49, instead of the correct

params[:session][:remember_me] == '1' ? remember(user) : forget(user)

I actually used

params[:session][:remember_me] ? remember(user) : forget(user)

In this context, params[:session][:remember_me] is either ’0’ or ’1’, both of which are true in a boolean context, so the resulting expression is always true, and the application acts as if the checkbox is always checked. This is exactly the kind of error a test can catch.

Because remembering users requires that they be logged in, our first step is to define a helper to log users in inside tests. In Listing 8.20, we logged a user in using the post method and a valid session hash, but it’s cumbersome to do this every time. To avoid needless repetition, we’ll write a helper method called log_in_as to log in for us.

Our method for logging a user in depends on the type of test. Inside integration tests, we can post to the sessions path as in Listing 8.20, but in other tests (such as controller and model tests) this won’t work, and we need to manipulate the session method directly. As a result, log_in_as should detect the kind of test being used and adjust accordingly. We can tell the difference between integration tests and other kinds of tests using Ruby’s convenient defined? method, which returns true if its argument is defined and false otherwise. In the present case, the post_via_redirect method (seen before in Listing 7.26) is available only in integration tests, so the code

defined?(post_via_redirect) ...

will return true inside an integration test and false otherwise. This suggests defining an integration_test? boolean method and writing an if-then statement schematically as follows:

if integration_test?
  # Log in by posting to the sessions path
else
  # Log in using the session
end

Filling in the comments with code leads to the log_in_as helper method shown in Listing 8.50. (This is a fairly advanced method, so you are doing well if you can read it with full comprehension.)

Listing 8.50: Adding a log_in_as helper. test/test_helper.rb
ENV['RAILS_ENV'] ||= 'test'
.
.
.
class ActiveSupport::TestCase
  fixtures :all

  # Returns true if a test user is logged in.
  def is_logged_in?
    !session[:user_id].nil?
  end

  # Logs in a test user.
  def log_in_as(user, options = {})
    password    = options[:password]    || 'password'
    remember_me = options[:remember_me] || '1'
    if integration_test?
      post login_path, session: { email:       user.email,
                                  password:    password,
                                  remember_me: remember_me }
    else
      session[:user_id] = user.id
    end
  end

  private

    # Returns true inside an integration test.
    def integration_test?
      defined?(post_via_redirect)
    end
end

Note that, for maximum flexibility, the log_in_as method in Listing 8.50 accepts an options hash (as in Listing 7.31), with default options for the password and for the “remember me” checkbox set to ’password’ and ’1’, respectively. In particular, because hashes return nil for nonexistent keys, code like

remember_me = options[:remember_me] || '1'

evaluates to the given option if present and to the default otherwise (an application of the short-circuit evaluation described in Box 8.1).

To verify the behavior of the “remember me” checkbox, we’ll write two tests, one each for submitting with and without the checkbox checked. This is easy using the login helper defined in Listing 8.50, with the two cases appearing as

log_in_as(@user, remember_me: '1')

and

log_in_as(@user, remember_me: '0')

(Because ’1’ is the default value of remember_me, we could omit the corresponding option in the first case above, but I’ve included it to make the parallel structure more apparent.)

After logging in, we can check if the user has been remembered by looking for the remember_token key in the cookies. Ideally, we would check that the cookie’s value is equal to the user’s remember token, but as currently designed there’s no way for the test to get access to it: the user variable in the controller has a remember token attribute, but (because remember_token is virtual) the @user variable in the test doesn’t. Fixing this minor blemish is left as an exercise (Section 8.6), but for now we can just test to see if the relevant cookie is nil or not.

There’s one more subtlety, which is that for some reason inside tests the cookies method doesn’t work with symbols as keys, so that

cookies[:remember_token]

is always nil. Luckily, cookies does work with string keys, so that

cookies['remember_token']

has the value we need. The resulting tests appear in Listing 8.51. (Recall from Listing 8.20 that users(:michael) references the fixture user from Listing 8.19.)

Listing 8.51: A test of the “remember me” checkbox. green test/integration/users_login_test.rb
require 'test_helper'

class UsersLoginTest < ActionDispatch::IntegrationTest

  def setup
    @user = users(:michael)
  end
  .
  .
  .
  test "login with remembering" do
    log_in_as(@user, remember_me: '1')
    assert_not_nil cookies['remember_token']
  end

  test "login without remembering" do
    log_in_as(@user, remember_me: '0')
    assert_nil cookies['remember_token']
  end
end

Assuming you didn’t make the same implementation mistake I did, the tests should be green:

Listing 8.52: green
$ bundle exec rake test

Testing the remember branch

In Section 8.4.2, we verified by hand that the persistent session implemented in the preceding sections is working, but in fact the relevant branch in the current_user method is currently completely untested. My favorite way to handle this kind of situation is to raise an exception in the suspected untested block of code: if the code isn’t covered, the tests will still pass; if it is covered, the resulting error will identify the relevant test. The result in the present case appears in Listing 8.53.

Listing 8.53: Raising an exception in an untested branch. green app/helpers/sessions_helper.rb
module SessionsHelper
  .
  .
  .
  # Returns the user corresponding to the remember token cookie.
  def current_user
    if (user_id = session[:user_id])
      @current_user ||= User.find_by(id: user_id)
    elsif (user_id = cookies.signed[:user_id])
      raise       # The tests still pass, so this branch is currently untested.
      user = User.find_by(id: user_id)
      if user && user.authenticated?(cookies[:remember_token])
        log_in user
        @current_user = user
      end
    end
  end
  .
  .
  .
end

At this point, the tests are green:

Listing 8.54: green
$ bundle exec rake test

This is a problem, of course, because the code in Listing 8.53 is broken. Moreover, persistent sessions are cumbersome to check by hand, so if we ever want to refactor the current_user method (as we will in Chapter 10) it’s important to test it.

Because the log_in_as helper method defined in Listing 8.50 automatically sets session[:user_id], testing the “remember” branch of the current_user method is difficult in an integration test. Luckily, we can bypass this restriction by testing the current_user method directly in a Sessions helper test, whose file we have to create:

$ touch test/helpers/sessions_helper_test.rb

The test sequence is simple:

  1. Define a user variable using the fixtures.
  2. Call the remember method to remember the given user.
  3. Verify that current_user is equal to the given user.

Because the remember method doesn’t set session[:user_id], this procedure will test the desired “remember” branch. The result appears in Listing 8.55.

Listing 8.55: A test for persistent sessions. test/helpers/sessions_helper_test.rb
require 'test_helper'

class SessionsHelperTest < ActionView::TestCase

  def setup
    @user = users(:michael)
    remember(@user)
  end

  test "current_user returns right user when session is nil" do
    assert_equal @user, current_user
    assert is_logged_in?
  end

  test "current_user returns nil when remember digest is wrong" do
    @user.update_attribute(:remember_digest, User.digest(User.new_token))
    assert_nil current_user
  end
end

Note that we’ve added a second test, which checks that the current user is nil if the user’s remember digest doesn’t correspond correctly to the remember token, thereby testing the authenticated? expression in the nested if statement:

if user && user.authenticated?(cookies[:remember_token])

Incidentally, in Listing 8.55 we could write

assert_equal current_user, @user

instead, and it would work just the same, but (as mentioned briefly in Section 5.6) the conventional order for the arguments to assert_equal is expected, actual:

assert_equal <expected>, <actual>

which in the case of Listing 8.55 gives

assert_equal @user, current_user

With the code as in Listing 8.55, the test is red as required:

Listing 8.56: red
$ bundle exec rake test TEST=test/helpers/sessions_helper_test.rb

We can get the tests in Listing 8.55 to pass by removing the raise and restoring the original current_user method, as shown in Listing 8.57. (You can also verify by removing the authenticated? expression in Listing 8.57 that the second test in Listing 8.55 fails, which confirms that it tests the right thing.)

Listing 8.57: Removing the raised exception. green app/helpers/sessions_helper.rb
module SessionsHelper
  .
  .
  .
  # Returns the user corresponding to the remember token cookie.
  def current_user
    if (user_id = session[:user_id])
      @current_user ||= User.find_by(id: user_id)
    elsif (user_id = cookies.signed[:user_id])
      user = User.find_by(id: user_id)
      if user && user.authenticated?(cookies[:remember_token])
        log_in user
        @current_user = user
      end
    end
  end
  .
  .
  .
end

At this point, the test suite should be green:

Listing 8.58: green
$ bundle exec rake test

Now that the “remember” branch of current_user is tested, we can be confident of catching regressions without having to check by hand.

8.5 Conclusion

We’ve covered a lot of ground in the last two chapters, transforming our promising but unformed application into a site capable of the full suite of signup and login behaviors. All that is needed to complete the authentication functionality is to restrict access to pages based on login status and user identity. We’ll accomplish this task en route to giving users the ability to edit their information, which is the main goal of Chapter 9.

Before moving on, merge your changes back into the master branch:

$ bundle exec rake test
$ git add -A
$ git commit -m "Finish log in/log out"
$ git checkout master
$ git merge log-in-log-out

Then push up to the remote repository:

$ bundle exec rake test
$ git push

Before deploying to Heroku, it’s worth noting that the application will briefly be in an invalid state after pushing but before the migration is finished. On a production site with significant traffic, it’s a good idea to turn maintenance mode on before making the changes:

$ heroku maintenance:on
$ git push heroku
$ heroku run rake db:migrate
$ heroku maintenance:off

This arranges to show a standard error page during the deployment and migration. (We won’t bother with this step again, but it’s good to see it at least once.) For more information, see the Heroku documentation on error pages.

8.5.1 What we learned in this chapter

8.6 Exercises

For a suggestion on how to avoid conflicts between exercises and the main tutorial, see the note on exercise topic branches in Section 3.6.

  1. In Listing 8.32, we defined the new token and digest class methods by explicitly prefixing them with User. This works fine and, because they are actually called using User.new_token and User.digest, it is probably the clearest way to define them. But there are two perhaps more idiomatically correct ways to define class methods, one slightly confusing and one extremely confusing. By running the test suite, verify that the implementations in Listing 8.59 (slightly confusing) and Listing 8.60 (extremely confusing) are correct. (Note that, in the context of Listing 8.59 and Listing 8.60, self is the User class, whereas the other uses of self in the User model refer to a user object instance. This is part of what makes them confusing.)
  2. As indicated in Section 8.4.6, as the application is currently designed there’s no way to access the virtual remember_token attribute in the integration test in Listing 8.51. It is possible, though, using a special test method called assigns. Inside a test, you can access instance variables defined in the controller by using assigns with the corresponding symbol. For example, if the create action defines an @user variable, we can access it in the test using assigns(:user). Right now, the Sessions controller create action defines a normal (non-instance) variable called user, but if we change it to an instance variable we can test that cookies correctly contains the user’s remember token. By filling in the missing elements in Listing 8.61 and Listing 8.62 (indicated with question marks ? and FILL_IN), complete this improved test of the “remember me” checkbox.
Listing 8.59: Defining the new token and digest methods using self. green app/models/user.rb
class User < ActiveRecord::Base
  .
  .
  .
  # Returns the hash digest of the given string.
  def self.digest(string)
    cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
                                                  BCrypt::Engine.cost
    BCrypt::Password.create(string, cost: cost)
  end

  # Returns a random token.
  def self.new_token
    SecureRandom.urlsafe_base64
  end
  .
  .
  .
end
Listing 8.60: Defining the new token and digest methods using class << self. green app/models/user.rb
class User < ActiveRecord::Base
  .
  .
  .
  class << self
    # Returns the hash digest of the given string.
    def digest(string)
      cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
                                                    BCrypt::Engine.cost
      BCrypt::Password.create(string, cost: cost)
    end

    # Returns a random token.
    def new_token
      SecureRandom.urlsafe_base64
    end
  end
  .
  .
  .
Listing 8.61: A template for using an instance variable in the create action. app/controllers/sessions_controller.rb
class SessionsController < ApplicationController

  def new
  end

  def create
    ?user = User.find_by(email: params[:session][:email].downcase)
    if ?user && ?user.authenticate(params[:session][:password])
      log_in ?user
      params[:session][:remember_me] == '1' ? remember(?user) : forget(?user)
      redirect_to ?user
    else
      flash.now[:danger] = 'Invalid email/password combination'
      render 'new'
    end
  end

  def destroy
    log_out if logged_in?
    redirect_to root_url
  end
end
Listing 8.62: A template for an improved “remember me” test. green test/integration/users_login_test.rb
require 'test_helper'

class UsersLoginTest < ActionDispatch::IntegrationTest

  def setup
    @user = users(:michael)
  end
  .
  .
  .
  test "login with remembering" do
    log_in_as(@user, remember_me: '1')
    assert_equal FILL_IN, assigns(:user).FILL_IN
  end

  test "login without remembering" do
    log_in_as(@user, remember_me: '0')
    assert_nil cookies['remember_token']
  end
  .
  .
  .
end
  1. Another common model is to expire the session after a certain amount of time. This is especially appropriate on sites containing sensitive information, such as banking and financial trading accounts. 
  2. Some browsers offer an option to restore such sessions via a “continue where you left off” feature, but of course Rails has no control over this behavior. 
  3. A second option is to use form_tag in place of form_for, which might be even more idiomatically correct Rails, but it has less in common with the signup form, and at this stage I want to emphasize the parallel structure. 
  4. The log_in method is available in the Sessions controller because of the module inclusion in Listing 8.11
  5. This practice of remembering variable assignments from one method invocation to the next is known as memoization. (Note that this is a technical term; in particular, it’s not a misspelling of “memorization”.) 
  6. Image from http://www.flickr.com/photos/hermanusbackpackers/3343254977/
  7. Web browsers can’t actually issue DELETE requests; Rails fakes it with JavaScript. 
  8. See the Bootstrap components page for more information. 
  9. If you’re using the cloud IDE, I recommend using a different browser to test the login behavior so that you don’t have to close down the browser running the IDE. 
  10. It’s worth noting that indentation in fixture files must take the form of spaces, not tabs, so take care when copying code like that shown in Listing 8.19
  11. As with the Sessions controller, the log_in method is available in the Users controller because of the module inclusion in Listing 8.11
  12. For example, I once had a test suite that was green even after accidentally deleting the main log_in method in the Sessions helper. The reason is that the tests were happily using a test helper with the same name, thereby passing even though the application was completely broken. As with is_logged_in?, we’ll avoid this issue by defining the test helper log_in_as in Listing 8.50
  13. Some browsers offer a “remember where I left off” feature, which restores the session automatically, so be sure to disable any such feature before trying to log out. 
  14. Setting @current_user to nil would only matter if @current_user were created before the destroy action (which it isn’t) and if we didn’t issue an immediate redirect (which we do). This is an unlikely combination of events, and with the application as presently constructed it isn’t necessary, but because it’s security-related I include it for completeness. 
  15. Session hijacking was widely publicized by the Firesheep application, which showed that remember tokens at many high-profile sites were visible when connected to public Wi-Fi networks. 
  16. This choice is based on the RailsCast on remember me
  17. Indeed, it had better be OK, because with bcrypt’s salted hashes there’s no way for us to tell if two users’ passwords match. 
  18. With unique remember tokens, an attacker always needs both the user id and the remember token cookies to hijack the session. 
  19. As a general rule, if a method doesn’t need an instance of an object, it should be a class method. Indeed, this decision will prove important in Section 10.1.2
  20. As noted in Section 6.3.1, “unencrypted password” is a misnomer, as the secure password is hashed, not encrypted. 
  21. I generally use the convention of putting such assignments in parentheses, which is a visual reminder that it’s not a comparison. 
  22. Google “<your browser name> inspect cookies” to learn how to inspect the cookies on your system. 
  23. Thanks to reader Paulo Célio Júnior for pointing this out. 
  24. Thanks to reader Niels de Ron for pointing this out. 
  25. Note that this means unchecking the box will log out the user on all browsers on all computers. The alternate design of remembering user login sessions on each browser independently is potentially more convenient for users, but it’s less secure, and is also more complicated to implement. Ambitious readers are invited to try their hand at implementing it. 
  26. Before we wrote remember user without parentheses, but when used with the ternary operator omitting them results in a syntax error.