Skip to content

Simple Password Authentication

Dustin Scarberry edited this page Oct 17, 2022 · 82 revisions

In this tutorial we will add Sorcery as the registration/authentication engine to Rails 4 app. At the end of the tutorial we will be able to register a user, and then login and logout with a username and a password.

Let's start from adding the sorcery gem into the Gemfile, and 'bundle install'. In the app's Gemfile:

# Gemfile
gem 'sorcery'

We'll start building the app by running this generator added by sorcery:

rails g sorcery:install

It will create the initializer file, the User model, unit test stubs, and the default migration, which we want to run now:

rake db:migrate

To get some views and controllers fast, we'll run rails scaffold generator, and skip the files we already created (we'll need to delete the new migration manually though):

rails g scaffold_controller user email:string crypted_password:string salt:string

We don't want users to edit/view their crypted password or salt, so we'll remove these from all templates in app/views/users/.

We also need to allow UsersController receive form attributes, and to remove crypted password and salt from user_params:

class UsersController < ApplicationController
  # ...
  private

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

We'll need to add a password 'virtual' field instead, that will hold the password before it is encrypted into the database:

# app/views/users/_form.html.erb
<div class="field">
   <%= form.label :password %><br />
   <%= form.password_field :password %>
</div>
<div class="field">
   <%= form.label :password_confirmation %><br />
   <%= form.password_field :password_confirmation %>
</div>

The virtual attributes will be added via validates :password, confirmation: true as seen below. You may also want to allow the user to change only some of his attributes. Also we need to let the User model know it is using sorcery. We do that like this:

# app/models/user.rb
class User < ActiveRecord::Base
  authenticates_with_sorcery!

  validates :password, length: { minimum: 3 }, if: -> { new_record? || changes[:crypted_password] }
  validates :password, confirmation: true, if: -> { new_record? || changes[:crypted_password] }
  validates :password_confirmation, presence: true, if: -> { new_record? || changes[:crypted_password] }

  validates :email, uniqueness: true
end

And we need to define routes.

# config/routes.rb
root :to => 'users#index'
resources :users

Now run the app and create a new user at http://0.0.0.0:3000/users. Voila! The password was automatically encrypted, and a salt was also auto-created! By default the encryption algorithm used is BCrypt (using the bcrypt-ruby gem) but that can be configured, as well as the salt, and the database field names.

Now we need a way to login after registering, so we'll generate a controller for that:

rails g controller UserSessions new create destroy

Make it look like this:

# app/controllers/user_sessions_controller.rb
class UserSessionsController < ApplicationController
  def create
    @user = login(params[:email], params[:password])

    if @user
      redirect_back_or_to(:users, notice: 'Login successful')
    else
      flash.now[:alert] = 'Login failed'
      render action: 'new'
    end
  end

  def destroy
    logout
    redirect_to(:users, notice: 'Logged out!')
  end
end

'login' takes care of login, 'logout' takes care of logout. 'redirect_back_or_to' takes care of redirecting the user to the page he asked for before reaching the login form, if such a page exists.

Let's create the login form:

# app/views/user_sessions/new.html.erb
<h1>Login</h1>

<%= render 'form' %>

<%= link_to 'Back', users_path %>
# app/views/user_sessions/_form.html.erb
<%= form_with url: login_path, method: :post do |f| %>
  <div class="field">
    <%= f.label :email %><br />
    <%= f.text_field :email %>
  </div>
  <div class="field">
    <%= f.label :password %><br />
    <%= f.password_field :password %>
  </div>
  <div class="actions">
    <%= f.submit "Login" %>
  </div>
<% end %>

And define some routes. At this point make sure your routes.rb file includes these routes and remove others:

# config/routes.rb
root :to => 'users#index'
resources :users

get 'login' => 'user_sessions#new', :as => :login
post 'login' => "user_sessions#create"
post 'logout' => 'user_sessions#destroy', :as => :logout

One Last thing, we need some navigation links, and a way to display flash messages:

# app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
  <head>
    <title>Tutorial</title>
      <%= stylesheet_link_tag    "application" %>
      <%= javascript_include_tag "application" %>
      <%= csrf_meta_tags %>
  </head>
  <body>

  <div id="nav">
    <% if current_user %>
      <%= link_to "Edit Profile", edit_user_path(current_user.id) %>
      <%= link_to "Logout", :logout, method: :post %>
    <% else %>
      <%= link_to "Register", new_user_path %> |
      <%= link_to "Login", :login %>
    <% end %>
  </div>
  <div>
    <p id="notice"><%= flash[:notice] %></p>
    <p id="alert"><%= flash[:alert] %></p>
  </div>
  <%= yield %>

  </body>
</html>

Now you can click on the "Login" link, enter your username and password, and get redirected to the users list with a nice flash message. Try the "Logout" link as well.

OK, that's nice, but our logged-out users can do anything a logged-in user can, like editing users for example. Let's fix that by protecting our sensitive controller actions:

# app/controllers/application_controller.rb
before_action :require_login

# app/controllers/users_controller.rb
skip_before_action :require_login, only: [:index, :new, :create] # this should only be used if you are allowing users to register themselves. 

# app/controllers/user_sessions_controller.rb
skip_before_action :require_login, only: [:new, :create]

The default controller code redirects you to "show" action after "create" completes. This means when you register a user, you will be redirected to a protected action, so let's fix it by redirecting to the users index action:

# app/controllers/users_controller.rb
...
if @user.save
  redirect_to(:users, notice: 'User was successfully created')
  ...
# app/controllers/application_controller.rb
private
def not_authenticated
  redirect_to login_path, alert: "Please login first"
end

Now some parts of the application are protected from logged-out users. Also, if you try to edit a user while logged out, get denied and then login at the login form, you'll see you are automatically taken to the edit page. If 'not_authenticated' is a problematic name for you, it is possible to supply a different method at configuration time, that will be called when a user is denied due to being logged-out.