Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save efrenfuentes/4061704 to your computer and use it in GitHub Desktop.
Save efrenfuentes/4061704 to your computer and use it in GitHub Desktop.
Authentication from Scratch with Rails 3.2.5

Authentication from Scratch with Rails 3.2.5

Rails 3.1 gives us a really easy way to authenticate users in our app: http_basic_authenticate_with. Using it, all we need to do is create a model for the user (certainly, User model :P) with an attribute called password_digest and some views feature for login and register users. After all, let's relax and let Rails do the hard work.

What we'll need

Gemfile

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

Model

First at all, an User model which we can generate by following: rails g model user email:string password_digest:string and then add the following methodo call to generated class: has_secure_password We certainly want validate presence for email and password fields when creating new users. In order to accomplish this task, let's deal with the validators in the User model class:

validates_uniqueness_of :email
validates_presence_of :email
validates_presence_of :password, :on => :create

After all, we may need some attr_accessible in order to access user's attributes. Adding this, our final user model may look like this:

class User < ActiveRecord::Base
  attr_accessible :email, :password, :password_confirmation

  has_secure_password

  validates_uniqueness_of :email
  validates_presence_of :email
  validates_presence_of :password, :on => :create
end

Controllers

UsersController

We'll need a controller that allows users creation and manipulation at all. rails g controller users Our new controller may look like this:

class UsersController < ApplicationController
  def new
    @user = User.new
  end

  def create
    @user = User.new(params[:user])
    if @user.save
      redirect_to root_url, :notice => "Signed up!"
    else
      render "new"
    end
  end
end

SessionsController

Later we'll need a controller for willing users authenticate (login and logout). Authentications are handled by Sessions controller. rails g controller sessions At a first glance, our sessions controller can look just like that:

class SessionsController < ApplicationController
  def new
  end

  def create
    user = User.find_by_email(params[:email])
    if user && user.authenticate(params[:password])
      session[:user_id] = user.id
      redirect_to root_url, :notice => "Logged in!"
    else
      flash.now.alert = "Invalid email or password"
      render "new"
    end
  end

  def destroy
    session[:user_id] = nil
    redirect_to root_url, :notice => "Logged out!"
  end
end

It's in sessions controller that magic happens. The has_secure_password method that we've added in users model earlier gives us the power to call authenticate method from an user object. We've got the power! authenticate method will ensure that the given password matches the password_digest for that user and return true if it does - false otherwise.

ApplicationController

In order to fetch the session's user anywhere in our application, we can make use of ApplicationController and append to it the following lines of code:

  private

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

  helper_method :current_user

The call to helper_method passing :current_user as parameter does so that current_user method can also be called from views - nice!

Views

SignUp Users

Located at app/views/users/new.html.erb

<h1>Sign Up</h1>

<%= form_for @user do |f| %>
  <% if @user.errors.any? %>
    <div class="error_messages">
      <h2>Form is invalid</h2>
      <ul>
        <% for message in @user.errors.full_messages %>
          <li><%= message %></li>
        <% end %>
      </ul>
    </div>
  <% end %>
  <div class="field">
    <%= f.label :email %>
    <%= f.text_field :email %>
  </div>
  <div class="field">
    <%= f.label :password %>
    <%= f.password_field :password %>
  </div>
  <div class="field">
    <%= f.label :password_confirmation %>
    <%= f.password_field :password_confirmation %>
  </div>
  <div class="actions"><%= f.submit %></div>
<% end %>

Login Users

Located at app/views/sessions/new.html.erb

<h1>Log in</h1>

<%= form_tag sessions_path do %>
  <div class="field">
    <%= label_tag :email %>
    <%= text_field_tag :email, params[:email] %>
  </div>
  <div class="field">
    <%= label_tag :password %>
    <%= password_field_tag :password %>
  </div>
  <div class="actions"><%= submit_tag "Log in" %></div>
<% end %>

Does it really do what it should?

Ok. Now let's make some tests. Note that it's not at all a TDD approach, once we're not covering our models neither our controllers with unitary and functional tests.

Configuring Gemfile

We'll need the following gems:

  gem "rspec-rails"
  gem "factory_girl_rails"
  gem "capybara"
  gem "guard-rspec"

Short way, rspec stands for the test framework, factory girl will help us as an alternative for rails built-in fixtures mechanism, cabypara is the test framework for integration tests and guard-spec will run tests for us after changes occur.

Preparing the test framework

rails g rspec:install
mkdir spec/support spec/models spec/routing
guard init
bundle exec guard

At this point, if it's all right, guard should be running all tests - I mean... guard must not have much work to do right now - besides anything else than run all specs and get no errors because there's no test. Enable capybara by adding the following line to spec/spec_helper.rb:

require 'capybara/rspec'

You may also want to remove the configuration line for config.fixture_path, once we're not going to use fixtures.

Creating the test

We can generate an integration test by running rails g integration_test login, which will create the spec/requests/logins_spec.rb file. Now, we need an user. Let's make use of our factory just by creating a spec/factories.rb file with the following lines:

Factory.define :user do |f|
  f.sequence(:email) { |n| "foo#{n}@example.com" }
  f.password "secret"
end

Now we have an user factory to play around, lets take a look at what could be the code for our login_spec.rb file:

require 'spec_helper'

describe "Logins" do
  it "log in valid users" do
    user = Factory(:user)
    visit login_path
    fill_in "Email", :with => user.email
    fill_in "Password", :with => user.password
    click_button "Login"
    page.should have_content("successfully")
  end
end

Thats a pretty simple integration test but it is one! After saving this, guard should run tests and warn about the failure. Let's see what fails:

  1) Logins log in valid users
     Failure/Error: user = Factory(:user)
     ActiveRecord::StatementInvalid:
       Could not find table 'users'

That's because I haven't run rake db:migrate in tests environment neither the rake db:test:prepare command. Ok, now that I did it, lets click enter in terminal which is running guard to force it running our tests. Another failure:

  1) Logins log in valid users
     Failure/Error: visit login_path
     NameError:
       undefined local variable or method `login_path'

The get "login" => "sessions#new", :as => "login" line in config/routes.rb should solve the problem. Now we get another failure message related to routes:

  1) Logins log in valid users
     Failure/Error: visit login_path
     ActionView::Template::Error:
       undefined local variable or method `sessions_path'

That's pretty obvious. We did not created any route to our sessions controller. Let's do it now. resources :sessions should handle this. Next failure:

  1) Logins log in valid users
     Failure/Error: click_button "Login"
     Capybara::ElementNotFound:
       no button with value or id or text 'Login' found

Great! Some broken test which failure is not related to routes. The complaint we received is about our log in view. There's no such login button. Inspecting our earlier views/sessions/new.html.erb, I found that submit_tag is named "Log in" and not "Login" as we stated in our integration test. Let's change it and fix this problem. Next guard complaint:

  1) Logins log in valid users
     Failure/Error: click_button "Login"
     NameError:
       undefined local variable or method `root_url'

This is an easy one! We haven't configured a root route in our routes.rb. A simple root :to => 'sessions#new' line in our routes.rb should solve this problem. Next complaint:

  1) Logins log in valid users
     Failure/Error: page.should have_content("successfully")

Uh. We'll need give a feedback message when user logs in. Let's take some lines into views/layouts/application.html.erb in order to show our flash message:

<% if flash[:error] %>
  <p class='error'><%=h flash[:error] %></p>
<% end %>
<% if flash[:notice] %>
  <p class='notice'><%=h flash[:notice] %></p>
<% end %>

Next... for sure! Let's modify the :notice message in our sessions controller to "Logged in successfully!". Do not forget to remove public/index.html file if your rails project is a brand new one. And here we are! Our first integration test run successfully.

References

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment