RELS Part 3: User Authentication with Ruby on Rails, RSpec and Capybara

Welcome to part 3 of our tutorial series: Build your own Real Estate Listing Service with Ruby on Rails. In this part we take care about the different users that can use the service and the behavior driven development of user authentication.

The other parts of this tutorial series can be found here.

You can use it as standalone tutorial, too. Make sure that your development system is configured like described in Part1 of this series.

Additional Gems

For this tutorial we need some additional gems. Please uncomment this line in our Gemfile:

1
  # gem 'bcrypt-ruby', '~> 3.0.0'

and add to the test group the email_spec:

1
2
3
4
5
  group :test do
    ...
    gem 'email_spec'
    ...
  end

We need bcrypt to store encrypted passwords for our users and email_spec to test email delivery. We use bundle install to make them available for our application. Additionally, for the email_spec gem we have to add this lines to our spec_helper.rb file:

1
  require 'email_spec'

on the top of our spec_helper.rb file and

1
2
3
4
5
6
  RSpec.configure do |config|
    ...
    config.include(EmailSpec::Helpers)
    config.include(EmailSpec::Matchers)
    ...
  end

inside of our RSpec.configure block.

Thinking About User Roles

For a real estate listing service we need different types of users. In our last parts we focused on the visitor-user accessing public functions like search. For this and upcomming tutorials we need two more roles so that we have a set of roles like this:

  • Admin
  • Visitor
  • Real Estate Agent

The admin is the super user and owner of our RELS. He has access to a admin dashboard, can manage all users and properties. The visitor is the regular user. He wants to search for properties. The real estate agent is a user who can submit and manage his own properties.

User Authentication

We need a user authentication system to implement different functions for the given user roles. Only the visitor role doesn’t need any authentication because this role has only access to public functions. The user data for the admin we can create manually but for the agent we need a sign up process. In summary we need following functions:

  • Real estate agent sign up
  • Admin and Agent log in and log out

The sign up functions should validate the entered user data and send a activation email to the given email address. The agent can’t log in until he activated his account.

The admin and the agent can log in to their accounts by clicking a link on the start page. If they try to access a protected site they will be redirected to the log in page.

User Authorization

The admin and agent users are not allowed to use protected functions equally. Additionally to the public functions the agent user can do these actions:

  • Edit his own user data
  • Create, update or delete his own properties

The admin user has access to all data of the site. Until now he has access to these functions:

  • List, create, update or delete users
  • List, create, update or delete properties
  • Has access to the admin dashboard

To implement this access structure we use a role based authorization system making controller actions only accessible for certain roles.

User Management Features with RSpec and Capybara

  • As the site owner
  • I want to provide an user management
  • so that I can protect functions and grant access based on roles

This is the high level description of our user management system. We use Capybara scenarios to write our acceptance tests for this feature. For this we create following file:

1
  /spec/features/users_spec.rb

In this file we add our capybara specification:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
  require 'spec_helper'

  feature 'User Management', %q{
    As the site owner
    I want to provide an user management
    so that I can protect functions and grant access based on roles
  } do

    background do
      @agent = create(:user,:agent)
    end

    scenario 'Real estate agent sign up' do
      visit root_path
      click_link 'Sign Up'
      fill_in 'user[name]', with: 'Jane Doe'
      fill_in 'user[email]', with: 'newagent@example.com'
      fill_in 'user[password]', with: 'newpassword'
      fill_in 'user[password_confirmation]', with: 'newpassword'
      click_button 'Submit'
      expect(page).to have_content "Sign up complete. Please check your inbox for activation email."
    end

    scenario 'Real estate agent activation' do
      activate(@agent)
      expect(page).to have_content "Activation complete. Welcome #{@agent.name}"
    end

    scenario 'User log in' do
      activate(@agent)
      login(@agent)
      expect(page).to have_content "Successfully logged in."
    end

    scenario 'User log out' do
      activate(@agent)
      login(@agent)
      logout(@agent)
      expect(page).to have_content "Successfully logged out."
    end

    scenario 'Inactive User are not able to login' do
      login(@agent)
      expect(page).to have_content "Account inactive. Please activate your account."
    end

    scenario 'Agents have access to their office page' do
      activate(@agent)
      login(@agent)
      visit office_path
      expect(page).to have_content "Welcome to your office, #{@agent.name}"
    end

    scenario 'Visitors dont have access to any office' do
      visit office_path
      expect(page).to have_content "This site is protected. Please log in first."
    end

    scenario 'Users with no agent role dont have office access' do
      @agent.destroy
      @noagent = create(:user,:no_agent)
      activate(@noagent)
      login(@noagent)
      visit office_path
      expect(page).to have_content "Access denied. Insufficent rights."
    end

  end

All this scenarios will fail. The login, activate and logout functions are helper functions. To keep this spec more readable I did some refactoring in advance. We create the helper file

1
  /spec/support/users_helper.rb

and add following code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
module  UserHelper
 
  def login(a)
    visit root_path
    click_link 'Log In'
    fill_in 'session[email]', with: a.email
    fill_in 'session[password]', with: a.password
    click_button 'Log In'
  end
 
  def logout(a)
    visit root_path
    click_link 'Log Out'
  end
 
  def activate(a)
    visit activate_path(:code => a.activation_code)
  end
 
end

To make them available we have to add this line to our spec_helper.rb inside the Rspec.configure block:

1
2
3
4
5
RSpec.configure do |config|
  ...
  config.include(UserHelper)
  ...
end

Let’s go through the feature spec.

In the background block we create an @agent user. This will fail because there is no user model.

In the ‘Real estate agent sign up’ scenario we describe the needed steps for creating a user. We use only a small set of user attributes but there is no problem to add more attributes later. With clicking the submit button the user gets a notice that he got an email for activation. This scenario fails because there are no links for registration.

In the ‘Real estate agent activation’ scenario we describe the activation steps. For this we use the activate function in our helper file. This function visits the activation url with the activation code as argurment. This test fails too, because there is no path for activation.

In the ‘User log in’ and ‘User log out’ scenarios we check the authentication. Here we have to take care that we have to activate our @agent we created in the background block. Only active agents are allowed to log in. In the next scenario, ‘Inactive User are not able to login’, we check this.

Every agent has it’s own ‘office’ page. On this page the agent can manage his properties. This page we have to restrict in access to agents only. The next scenarios take care of it.

In the scenario ‘Agents have access to their office page’ we describe the way agents access to their page. Visitors are not allowed to access this page testing it in the next scenario ‘Visitors dont have access to any office’.

In the scenario ‘Users with no agent role dont have office access’ the test that only agent roles can access an office pages.

User Model Specs

We start making all this scenarios pass by adding a user model to our application. For this we use the rails generator:

1
  rails g model User email:string password_digest:string name:string is_admin:boolean role:string activation_code:string

and preparing the databases

1
2
  bundle exec rake db:migrate
  bundle exec rake db:test:clone

Because we have created a new model we need to modify our cleanup process in the spec/spec_helper.rb file. Add the following line after Property.delete_all:

1
  User.delete_all

With the line above we delete our user stubs from factory girl after each test. Now we step down to our model spec in spec/models/user_spec.rb and describe the wanted behavior like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
require 'spec_helper'

describe User do
  it "is valid with name, email and password confirmation" do
    u = create(:user, :agent)
    expect(u).to be_valid
  end
 
  it "sends a confirmation email after sign up" do
    u = create(:user, :agent)
    open_last_email.should be_delivered_to u.email
  end
 
  it "is invalid without email" do
    u = build(:user, :agent, :email=> nil)
    expect(u).to have(1).errors_on(:email)
  end
 
  it "is invalid without name" do
    u = build(:user, :agent, :name=> nil)
    expect(u).to have(1).errors_on(:name)
  end
 
  it "is invalid if passwords don't match" do
    u = build(:user, :agent, :password => "pw1", :password_confirmation => "wp1")
    expect(u).to have(1).errors_on(:password)
  end
 
  it "is invalid if no activation code is generated" do
    u = create(:user, :agent)
    expect(u.activation_code).to_not be_nil
  end
 
end

In this spec we describe the model validation. We want name, email and password with confirmation as required fields. Im the last block we test the activation code generation. This spec fails and before we can make it pass we modify our user factory in order to get testable stubs for our suite.

We find the file already prepared in spec/factories/users.rb and modify it like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# Read about factories at https://github.com/thoughtbot/factory_girl

FactoryGirl.define do
  factory :user do
    email "john@example.com"
    password "secret"
    name "John Doe"
   
    trait :no_agent do
      role "no_agent"
    end
   
    trait :agent do
      role "agent"
    end
   
    trait :admin do
      is_admin true
    end
   
  end
end

With the traits we are able to generate different user roles efficiently. With this we can start to make our model spec pass. We open our user model and add modify it like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class User < ActiveRecord::Base
 
  has_secure_password
 
  attr_accessible :email, :is_admin, :name, :role, :activation_code, :password, :password_confirmation
 
  validates :name, :presence => true
  validates :email, :presence => true
 
  before_create :create_activation_code
  after_create :send_confirmation_email
 
  def send_confirmation_email
    UserMailer.confirmation_email(self).deliver
  end
 
  def create_activation_code
    self.activation_code = random_string(length=10)
  end
 
  def random_string(length=10)
    (1..length).collect {alphanumeric_characters.sample}.join
  end
 
  def alphanumeric_characters
    ("a".."z").to_a + ("0".."9").to_a
  end

  def is_active?
    self.activation_code == nil
  end
     
end

This code makes some validation tests pass. The has_secure_password entry is a helper from rails handling password and password_confirmation validation functions. For the activation code we generate a random string with alphanumeric characters.

Besides some tests pass the main blocker is the UserMailer. To resolve this we have to take care about the email generation.

User Activation Mailer

We create the user mailer with the following command:

1
  rails g mailer UserMailer

The generator creates all needed files. So we can open the spec/mailers/user_mailer_spec.rb and write tests like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
require "spec_helper"

describe UserMailer do
  include Rails.application.routes.url_helpers
 
  before(:all) do
    @user = create(:user,:agent)
    @email = UserMailer.confirmation_email(@user)
  end
 
  it "should be sent to the user email" do
    @email.should deliver_to(@user.email)
  end
 
  it "should contain the activation link inside" do
    pending("Uncomment after activation function works")
    # @email.should have_body_text(/#{activate_path(:code => @user.activation_code)}/)
  end
 
end

In this spec we create an agent user and an email first. We test only two things. At first we test that the email will be send to the correct email address. At second we test that the activation link is inside. Because we can the second test only make pass if we activation function works we set this test on pending.

The first test fails because there is no confirmation_email. To make this test pass we open the mailers/user_mailer.rb and change the code that it looks like this:

1
2
3
4
5
6
7
8
9
class UserMailer < ActionMailer::Base
  default from: "byorels@example.com"
 
  def confirmation_email(user)
    @user = user
    mail(:to => user.email, :subject => "Please Activate Your Account")
  end
 
end

With this the first mailer test should pass. And if we rerun our user model spec this spec should pass to.

Back to our feature spec the background block should be working now. We start now making the sign up process pass.

User Signup and Activation

We create our user controller with the following command:

1
  rails g controller users new

In the config/routes.rb we add:

1
  resources :users

Now we can add a “sign up” link to our homepage with new users path. We add the following code to our home/index.html.erb file:

1
  <%= link_to "Sign Up", new_user_path %>

Next we write our sign up formular. For this we open the views/users/new.html.erb file and modify the code that it looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<div class="page-header">
  <h1>Sign Up</h1>
</div>
<%= form_for @user, :html => { :class => 'form-horizontal' } do |f| %>

  <% if @user.errors.any? %>
  <div class="alert alert-error">
    <a class="close" data-dismiss="alert">×</a>
    <% for message in @user.errors.full_messages %>
      <%= message %><br />
    <% end %>
  </div>
  <% end %>

  <div class="control-group">
    <%= f.label :name, :class => 'control-label' %>
    <div class="controls">
      <%= f.text_field :name, :class => 'text_field' %>
    </div>
  </div>
  <div class="control-group">
    <%= f.label :email, :class => 'control-label' %>
    <div class="controls">
      <%= f.text_field :email, :class => 'text_field' %>
    </div>
  </div>
  <div class="control-group">
    <%= f.label :password, :class => 'control-label' %>
    <div class="controls">
      <%= f.password_field :password, :class => 'text_field' %>
    </div>
  </div>
  <div class="control-group">
    <%= f.label :password_confirmation, :class => 'control-label' %>
    <div class="controls">
      <%= f.password_field :password_confirmation, :class => 'text_field' %>
    </div>
  </div>

  <div class="form-actions">
    <%= f.hidden_field :role, :value => "agent" %>
    <%= f.submit 'Submit', :class => 'btn btn-primary' %>
  </div>
<% end %>

Because the behavior is covered by the feature spec we don’t write a view spec here. Next we open the users_controller. At first we add the function:

1
2
3
def new
  @user = User.new(:role => "agent")
end

to create the user for the sign up form. Next we add the create function to execute the sign up in our users controller:

1
2
3
4
5
6
7
8
def create
  @user = User.new(params[:user])
  if @user.save
    redirect_to root_path, notice: 'Sign up complete. Please check your inbox for activation email.'
  else
    render action: "new"
  end
end

After user creation we redirect to the homepage with a flash notice. Our registration process works now and the matching scenario should pass. After the user sign up we have to make our activation functions pass. For this we add the following function to our users_controller:

1
2
3
4
5
6
7
8
9
def activate
  @user = User.find_by_activation_code(params[:code])
  @user.activation_code = nil
  if @user.save
    redirect_to root_path, notice: "Activation complete. Welcome #{@user.name}"
  else
    render action: "new"
  end
end

This function searches for a user with the matching activation code. For activating we set the activation code to nil. After saving the user we redirect to our homepage with a flash notice. To use this activate function we have to add following to our routes.rb:

1
  get "activate/:code" => "users#activate", :as => "activate"

To get our flash message output displayed we open our application.html.erb and change the code that it looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<!DOCTYPE html>
<html>
<head>
  <title>Tutorial</title>
  <%= stylesheet_link_tag    "application", :media => "all" %>
  <%= javascript_include_tag "application" %>
  <%= csrf_meta_tags %>
</head>
<body>
  <div class="container">
       
    <% if flash[:notice] -%>
      <div class = "alert alert-success">
        <a class="close" data-dismiss="alert">×</a>
        <%= flash[:notice] %>
      </div>
    <% end %>
    <% if flash[:error] -%>
      <div class = "alert alert-error">
        <a class="close" data-dismiss="alert">×</a>
        <%= flash[:error] %>
      </div>
    <% end %>
    <% if flash[:warning] -%>
      <div class = "alert alert-warning">
        <a class="close" data-dismiss="alert">×</a>
        <%= flash[:warning] %>
      </div>
    <% end %>
    <%= yield %>
  </div>

</body>
</html>

Now the user feature scenario ‘Real estate agent activation’ should pass. Go back to the mailer spec and uncomment the pending test and create the email view views/user_mailer/ confirmation_email.text.erb. Insert the following code to the view:

1
2
3
4
5
<%= @user.name %>, please activate your account.

Activation link: <%= "http://www.example.com/"+activate_path(:code => @user.activation_code) %>

Thanks for signing up!

The mailer test with activation link testing should pass now. After sign up and activation tests pass can take care about the authentication processes.

User Authentication

For user authentication we already wrote scenarios in our user features specs. Now we have to make them pass. For this we need a new controller:

1
 rails g controller sessions new

Next we go to our routes.rb configuration file and add this:

1
  resource :session

It generates the RESTful paths automatically. Now we can add the log in link to our homepage by adding

1
  <%= link_to "Log In", new_session_path %>

We open our view/sessions/new.html.erb file and add the following code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<div class="page-header">
  <h1>Log In</h1>
</div>

<%= form_for :session, :url => session_path, :html => { :class => 'form-horizontal' } do |f| %>

  <div class="control-group">
    <%= f.label :email, :class => 'control-label' %>
    <div class="controls">
      <%= f.text_field :email, :class => 'text_field' %>
    </div>
  </div>
  <div class="control-group">
    <%= f.label :password, :class => 'control-label' %>
    <div class="controls">
      <%= f.password_field :password, :class => 'text_field' %>
    </div>
  </div>

  <div class="form-actions">
    <%= f.submit 'Log In', :class => 'btn btn-primary' %>
  </div>
<% end %>

This form asks for the email and password for authentication. Next we open our sessions_controller and add a create function like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
def create
  user = User.find_by_email(params[:session][:email]).try(:authenticate, params[:session][:password])
  if user
    if user.is_active?
      session[:user_id] = user.id
      redirect_to (session[:target_url] || root_path), :notice => "Successfully logged in."
    else
      redirect_to new_session_path, :flash => { :error => "Account inactive. Please activate your account." }
    end
  else
    redirect_to new_session_path, :flash => { :error => "Invalid username or password." }
  end
end

It handles multiple functions. At first we use the rails helper functions to find a a user with the entered email and password
combination. If this user exists we check for activation_code via is_active? function. If the user is active we store the user_id in the session container and redirect to the target_url or the root_path. The target_url is used if the user has tried to call a protected site directly. After authorization we want to sent the use back to his called site.

With this function the log in scenario passes. Now we take care about the log out test. For this we add to our sessions controller following function:

1
2
3
4
def destroy
  session[:user_id] = nil
  redirect_to root_path, :notice => "Successfully logged out."
end

This function resets the stored session id. Now we add a log out link to our homepage like this:

1
  <%= link_to "Log Out", "/session", :method => "DELETE" %>

Our log out scenario should pass now. It’s time to take care about the authorization functions.

User Authorization

At first we create an office controller. The office page is a kind of dashboard for the real estate agent to manage his properties. We create the controller with:

1
  rails g controller offices show

and add the path configuration to our routes.rb file:

1
  resource :office

This page we want to protect. To do this we open the application controller and add following functions:

1
2
3
def current_user  
  @current_user ||= User.find(session[:user_id]) if session[:user_id]
end

The current_user function is a helper function it returns the user with the help of the session stored user_id. It returns the user if the user is logged in or returns nil if no session user_id exists. To use this function sitewide we add

1
2
3
4
5
class ApplicationController < ActionController::Base
  ...
  helper_method :current_user
  ...
end

to our application controller.

1
2
3
4
5
6
def auth_required
  unless current_user
    session[:target_url] = request.fullpath
    redirect_to new_session_path, :flash => { :error => "This site is protected. Please log in first." }
  end
end

This function above we use if we want to protect a controller or single actions of a controller. This function asks for a logged user. If no user is logged in it redirects to our login form with a flash message. It’s the function for authentication.

1
2
3
4
5
6
7
8
def access_only_with_roles(*roles)
  if current_user
    unless roles.include? current_user.role
      session[:target_url] = request.fullpath
      redirect_to new_session_path, :flash => { :warning => "Access denied. Insufficent rights." }
    end
  end
end

This function we use to limit the access to certain roles. In each action we want to protect we can use the following syntax to limit the access:

1
  access_only_with_roles("agent","office")

Next we use this functions in our office controller. Now the controller should look like this:

1
2
3
4
5
6
7
8
class OfficesController < ApplicationController
 
  def show
    auth_required
    access_only_with_roles("agent")
  end

end

After we secured the office controller our controller spec starts failing. In this case just remove the spec because for now the behavior is covered by the feature specs. Next we open our views/offices/show.html.erb and modify the code that it looks like this:

1
2
3
<div class="page-header">
  <h1>Welcome to your office, <%= current_user.name %></h1>
</div>

It simply adds a headline to show us that we are in the private office area. If we save this file an rerun our test suite all tests should pass. Your console output should look like this:

1
2
3
4
5
6
.............................

Finished in 5.43 seconds
29 examples, 0 failures

Randomized with seed 29763

Summary

In this tutorial we built a basic user authentication and authorization system from scratch. We used the behavior driven development approach to develop the functions for this system. Some functions are still missing like password recovery and remember me cookies.

In the next tutorials we are going to extend the property model(pictures, texts, geodata) and we going to build the CRUD functions for the real estate agents.

If you like this tutorial please share it on your social networks. If you have tips for improvement or any other information please leave a comment.

16 Responses to “RELS Part 3: User Authentication with Ruby on Rails, RSpec and Capybara”

    • Jeff

      Sry hit Enter to early.

      As I was saying. Great view into BDD with Rails.

      I tried to follow along the series and had no trouble with the first two installments. However with this one I have three failing test.

      1. The email body is empty.
      2. The “Users with no agent role dont have office access” doesn’t contain the flash message. Yes, I added to the view.
      3. The “Account inactive. Please activate your account.” test also doesnẗ contain the flash message but I’m not sure if that is due to number 2.

      A chance you make your code accessable?

      Thanks. Keep t up!

      Reply
      • Lars

        Hi Jeff, thanks for your comment.

        You are right and I updated the tutorial. I forgot to transfer the two code blocks from my app to the tutorial(confirmation.text.erb email view and update of application.html.erb)

        I’m not sure with the idea to make the source code public accessible because I’m afraid of a lot support requests from copy&paste users with problems to get it running or with problems to get it deployed to provider XYZ.

        Kind regards,
        Lars

        Reply
  1. Jeff

    Thanks Lars, the adjustments you made fixed the issues. But now I have three new ones.

    I’ve put the output of rspec up on pastebin. If you have time, I’d appreciate it, if you could point me in the right direction for solving them.

    http://pastebin.com/QXSJVsmP

    Reply
    • Lars

      Hi Jeff, can you make a

      1
      bundle exec rake db:test:clone

      and add a

      1
      User.delete_all

      in the spec_helper.rb after the Property.delete_all line. If you re-run the tests again they should pass. I’m going to update the tutorial in the evening.

      Reply
      • Jeff

        Hey Lars,

        thanks for your fast response. I appended the two lines but when I run rspec it responds with a syntax error. (Once again the message on pastebin.)

        However when I uncomment the bundle line all tests pass.

        Reply
        • Jeff

          > However when I uncomment the bundle line all tests pass.
          Should have read
          However when I comment out the bundle line all tests pass.
          Sorry.

          Reply
          • Lars

            I’m sorry. I didn’t wrote it correctly. I mean you should execute the bundle line in the console in the application folder and add only the User.delete_all line to the spec helper.

  2. Morgan

    Hi Lars, thanks for the best rails tutorial I have seen! looking forward to part 4.

    Reply
    • Lars

      Hi Emily, I’m currently working on part 4, but at the moment there is a lot of client work. I have to meet some deadlines before summer vacations. I will do my best do finish part 4 in the next two weeks.

      Reply
    • Lars

      Hi Morgan, I have been working hard on projects for clients for last months, so I couldn’t finish part 4 yet. I’m going to update the blog as soon as possible.

      Reply

Leave a Reply

  • (will not be published)

XHTML: You can use these tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>