Building a simple authentication in Rails 7 from scratch
Published: 2023-06-04 19:18:26

## Introduction
Ruby on Rails doesn't provide an official authentication solution despite that Rails is a highly opinionated web framework. It somehow shows how tricky authentication can be. Nonetheless, with many other features provided by Rails, it's not difficult to build our own authentication mechanism from scratch.

This article will show you how to **store user credentials**, let users **sign up**, **log in** and **log out**.

## Table of Contents
{%- # TOC start (generated with https://github.com/derlin/bitdowntoc) -%}

- [Introduction](#introduction)
- [Store user credentials with has_secure_password](#store-user-credentials-with-hassecurepassword)
- [Login](#login)
- [Log out](#log-out)
- [Other](#other)
- [Conclusion](#conclusion)
- [Code Repository](#code-repository)
- [References](#references)

{%- # TOC end -%}

## Store user credentials with has_secure_password

If you want to build an authentication system, the first thing you should think of is how to store users' passwords. In the past, many systems just stored users' passwords in plain text format. (Facebook was one of them...) Sounds ridiculous. Whatever, we don't do that anymore.

The best practice for storing passwords will be storing the **hash digest** of the password instead of the password itself. Hash Digest? What's that? Don't worry about that at this moment. The important thing now is how do we do that. Fortunately, Rails has a built-in method called `has_secure_password` that can handle the task for us.

But first, let us create a new rails project:
```bash
$ rails new auth_from_scratch
$ cd auth_from_scratch
```
### install bcrypt

`has_secure_password` will use `bcrypt` to calculate the hash of passwords so we need to add the gem `bcrypt` in the Gemfile
```ruby
# Gemfile
gem 'bcrypt', '~> 3.1.7'
```
then do
```
$ bundle install
```

### add User model

On the table that you want to store the users' credentials, `has_secure_password` will utilize the column `XXX_digest` to store the passwords' hash digest. We then make a `User` model with 3 columns:

- name
- password_digest
- password_confirmation

by:
```bash
$ rails g model user name password_digest password_confirmation
$ rails db:migrate
```

`password_confirmation` is optional but it let the system have the ability to ask users to input a password twice when registering a new user, just like many websites do.

We then call `has_secure_password` in the `User` model:
```ruby
# app/models/user.rb
class User < ApplicationRecord
  has_secure_password :password, validations: true
  validates :name, presence: true, uniqueness: true
end
```

We can look closer at the line having `has_secure_password`. `:password` means we want to use the `password_digest` column to store passwords. `validations: true` means we want users to compare `password` and `password_confirmation` when storing passwords. In fact, these 2 options are the default options, therefore, we can write it as this:

```ruby
# app/models/user.rb
class User < ApplicationRecord
  has_secure_password
  validates :name, presence: true, uniqueness: true
end
```

### Make a Signup page
Let's make a signup page so users can register themselves.
```bash
$ rails g controller users index new create
```
This creates a `UsersController` with `index`, `new` and `create` actions with the following functionalities:
- `index`: show all users in the system so we know we sign a user up successfully.
- `new`: show a signup form
- `create`: create a new user according to the `name` and `password` filled by the user

We then add the following codes:
```ruby
# config/routes.rb
Rails.application.routes.draw do
  resources :users, only: [:index, :new, :create]
end
```

```ruby
# app/controllers/users_controller.rb
class UsersController < ApplicationController
  def index
    @users = User.all
  end

  def new
    @user = User.new
  end

  def create
    @user = User.new(user_params)

    if @user.save
      flash[:notice] = "User created successfully"
      redirect_to users_path
    else
      flash[:alert] = "User not created"
      render :new, status: :unprocessable_entity
    end
  end

  private

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

```

```html
<!-- app/views/users/index.html.erb -->
<h1>Users#index</h1>

<%= link_to 'New User', new_user_path %>

<table>
  <thead>
    <tr>
      <th>id</th>
      <th>name</th>
    </tr>
  </thead>
  <tbody>
    <% @users.each do |user| %>
      <tr>
        <td><%= user.id %></td>
        <td><%= user.name %></td>
      </tr>
    <% end %>
  </tbody>
</table>
```

```html
<!-- app/views/users/new.html.erb -->
<h1>Users#new</h1>
<%= form_with model: @user do |f| %>
  <% if @user.errors.any? %>
    <div>
      <ul>
        <% @user.errors.full_messages.each do |message| %>
          <li><%= message %></li>
        <% end %>
      </ul>
    </div>
  <% end %>
  <div>
    <%= f.label :name %><br>
    <%= f.text_field :name %>
  </div>
  <div>
    <%= f.label :password %><br>
    <%= f.password_field :password %>
  </div>
  <div>
    <%= f.label :password_confirmation %><br>
    <%= f.password_field :password_confirmation %>
  </div>
  <p>
    <%= f.submit %>
  </p>
<% end %>
```

Modify the layout application.html.erb so it can display **flash messages**
```html
<!-- app/views/layouts/application.html.erb  -->
<body>
  <% flash.each do |type, msg| %>
    <div>
      <%= msg %>
    </div>
  <% end %>

  <%= yield %>
</body>
```

### Testing
1. We can go to the `/users` page and there's no user.
  - ![User index page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hx61vh0u3hsj8gcvav7q.png)

2. Click the link **New User** and go to the `/users/new` page. Then enter the username and password you want in the form.
  - ![New User page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yr8wwaomp5hns8rf7n3v.png)

3. After submitting the form, you should be redirected to the `/users` page. At this moment, you can see there's the user you just created and it also shows the flash message **"User created successfully"** 

  - ![User created](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/t28com56n9bebruzt6zo.png)

4. That's nice! We then want to know if it can really validate the password we enter. We go back to the `/users/new` page and enter password fields differently on purpose.
  - ![invalid user](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/77o3w7f9ye4zbleo9tgf.png)

5. It won't create the user. it shows the error message `Password confirmation doesn't match Password`. 
  - ![invalid user rejected](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8jidqdcdna02d88nd7ti.png)

### Checkpoint 1 - database
We can check how the user credential is stored. Open `rails console` and execute `User.first` you will get this:
```ruby
#<User:0x0000000109e013c0
 id: 1,
 name: "Kevin",
 password_digest: "[FILTERED]",
 password_confirmation: nil,
 created_at: Sat, 03 Jun 2023 16:14:15.155800000 UTC +00:00,
 updated_at: Sat, 03 Jun 2023 16:14:15.155800000 UTC +00:00>
```
The username, `Kevin`, is stored and the `password_digest` shows `[FILTERED]`.

We can also go to the database to check it out.
```sql
-- $ sqlite3 db/development.sqlite3
SELECT name, password_digest FROM users;
name   password_digest                                             
-----  ------------------------------------------------------------
Kevin  $2a$12$eIDbEN3j5T4pS//ra.QaN.8lQGvXu8aElqn9ypmDxZgA6Nz9IUatW
```
You'll find the `password_digest` stores a gibberish string `$2a$12$eIDbEN3j5T4pS//ra.QaN.8lQGvXu8aElqn9ypmDxZgA6Nz9IUatW` which is actually the digest of `bcrypt`.

## Login
The most important thing in an authentication system is to let users sign in; otherwise, it's kind of meaningless...

### Build a Login page
We first create a `UserSessionsController` by:
```bash
$ rails g controller user_sessions new create
```

It will have 2 actions:
- `new`: show the login page with the login form
- `create`: use the entered `username` and `password` to log in the user

```ruby
# app/controllers/application_controller.rb
class UserSessionsController < ApplicationController
  def new
    @user = User.new
  end

  def create
    @user = User.find_by(name: params[:user][:name])

    if @user && @user.authenticate(params[:user][:password])
      session[:user_id] = @user.id
      redirect_to root_path
    else
      flash[:alert] = "Login failed"
      redirect_to new_user_session_path
    end
  end
end
```

We should take a closer look at `create` because it's the critical part.

#### @user.authenticate
`User.find_by(name: params[:user][:name])` finds the user by the username entered. Then we call `authenticate` method on that `user` object to verify the password entered.
`User#authenticate` is also a method provided by `has_secure_password`. If the hash digest of the password entered matches the password digest stored in the database, it returns `true`; otherwise, it returns `false`.

We can utilize this method to verify whether the credential is correct or not.

#### session[:user_id] = @user.id
How does the system **memorize** a user who has logged in? Rails provides a simple but effective storage mechanism called `session`, it defaults to use **cookies** to store information. We can store the logging-in user's id as `user_id` so the next time when a controller checks the session, it knows it's a logging-in user.

We'll discuss more `session` later in this article. Let's continue adding code.

### Add Login page view
```html
<!-- app/views/user_sessions/new.html.erb -->
<h1>Login page</h1>
<%= form_with model: @user, url: user_sessions_path do |f| %>
  <div>
    <%= f.label :name %><br>
    <%= f.text_field :name %>
  </div>
  <div>
    <%= f.label :password %><br>
    <%= f.password_field :password %>
  </div>
  <p>
    <%= f.submit 'Login' %>
  </p>
<% end %>
```

### Add a helper method :current_user
Let's add a helper method `current_user` so we can get the current logged-in user easily. A `helper_method` declared in controllers can be accessed in the views, too.
```ruby
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  helper_method :current_user

  def current_user
    # If session[:user_id] is nil, set it to nil, otherwise find the user by id.
    @current_user ||= session[:user_id] && User.find_by(id: session[:user_id])
  end
end
```

### Add a public page and a restricted page
We need some pages to let us see the difference between the user before and after logging in. We add 2 pages here.

```ruby
# app/controllers/pages_controller.rb
class PagesController < ApplicationController
  # index is a public page
  def index
  end

  # secret is a private page, only logged-in user can enter
  def secret
    if current_user.blank?
      render plain: '401 Unauthorized', status: :unauthorized
    end
  end
end
```

```html
<!-- app/views/pages/index.html.erb -->
<% if current_user %>
  <h1>Welcome, <%= current_user.name %></h1>
<% else %>
  <h1>This is the index page</h1>
  <%= link_to 'Login', new_user_session_path %><br>
<% end %>
<%= link_to 'Secret page', '/pages/secret' %>
```

```html
<!-- app/views/pages/secret.html.erb -->
<h1>This is the secret page</h1>
```

### Add routes
Don't forget to add routes. Here we set the root page to `pages#index`
```ruby
Rails.application.routes.draw do
  root 'pages#index'
  get 'pages/secret'
  resources :user_sessions, only: [:new, :create]
  resources :users, only: [:index, :new, :create]
end
```

### Test
1. Go to the root page. You can enter it because it's a public page
  - ![Root page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ngzypknfkxjaz57q4ids.png)

2. Click the **Secret page** link, and you will get `401 Unauthorized`
  - ![401](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lcqniiqi56trd8s6b3f5.png)
3. Go back and click the **Login** link. Enter the credential and submit the form.
  - ![Login page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2o3jy7khdoipk0q0a2d7.png)
4. You will log in to the system. The index page will welcome you with your user's name.
  - ![Logged in](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/w9rma2zektvv5w7o7rrd.png)
5. Click the **Secret page** link. You can enter the secret page now!
  - ![Secret page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/04pwld40pqddrmbk6ae6.png)

### Checkpoint 2 - Session
We use `session` to store the information of logged-in users. However, it still seems a little vague. And most importantly, is it secure? We can check what the session looks like now.

First, we add `binding.break` to stop the execution. `binding.break` is from the gem `debug`.
```ruby
def index
  binding.break
end
```
Go to the root page, http://localhost:3000, when it stops.  We can go to the terminal and check how the session stores the information.
```ruby
session.to_hash
{
  "session_id"=>"09c4f96fdf7b2659c769aa4041e5d1a0",
  "_csrf_token"=>"e52CWauVPEQTi2mgPAx91wtwO81uVnIBeiDar5_K0r8", 
  "user_id"=>1
}
```
You'll find that `session` is just a `Hash`-like structure. You can see `"user_id" => 1` is stored in `session`. I hope it makes more sense when you do something like
```ruby
session['something'] = 'some value'
session['something']
```

We mentioned in [session[:user_id]](#session[:user_id] = @user.id) that session is stored in cookies. Does that mean we can see the information in the browser? If you're using Chrome, open the **Application** tab and click **Storage > Cookies > http://localhost:3000**, you will see there's a key called `_auth_from_scratch_session` in the cookie, the `session` object is stored there. However, if you check its value, you'll find it's not a key-value object as we expect, instead, it's a gibberish string.

![Session in cookie](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/up076xmyr62pqapb3sgx.png)

In fact, `session` is **encrypted** so *it cannot be read by browsers*. It uses the `secret_key_base` to do the encryption (You can get the secret by `Rails.application.credentials[:secret_key_base]`). Therefore, generally speaking, storing data in `session` is pretty secure. It's worth mentioning that the `session` is also **signed** so it's **tamperproof**.

The name `session` is not intuitive for the beginners, in my opinion, it should be called **encrypted_cookie** 😁

#### What if Javascript code wants to read the session data?
You can use embeded ruby like the code below to share information in the browser. However, be very careful and very picky about what information you want to reveal.
```javascript
var userId = <%= session[:user_id] %>
```

## Log out
The last thing is to provide a way for the users to log out. It's very simple. All we need to do is to empty `session[:user_id]`. 

### Add destroy action for logging out
We can add a `destroy` action in 
```ruby
def destroy
  session[:user_id] = nil
  redirect_to root_path
end
```

Remember to add the corresponding routes
```ruby
# config/routes.rb
resources :user_sessions, only: [:new, :create, :destroy]
```

### Add logout link in the view
The `destroy` action only accepts `DELETE` HTTP action. We're using `turbo-rails` by default so we just need to add `data: { turbo_method: :delete }` in the `link_to`
```html
<!-- app/views/pages/index.html.erb -->
<% if current_user %>
  <h1>Welcome, <%= current_user.name %></h1>
  <%= link_to 'Log out', user_session_path(current_user), data: { turbo_method: :delete } %><br>
<% else %>
  <h1>This is the index page</h1>
  <%= link_to 'Login', new_user_session_path %><br>
<% end %>
<%= link_to 'Secret page', '/pages/secret' %>
```

### Test
1. Go to the root page and click **Log out** link. You should log out successfully.

![Logout link](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ofpb29y7utocy0vupltc.png)

## Other

### Parameters filtering
All the passwords filled in by users will be sent in plaintext. If you're careless, the log will record the password input by users. 

Fortunately, `has_secure_password` handles that for us, too. Let's take a look at the HTTP payloads for registering a new user and logging in:
#### HTTP payload for registering a new user
```
Started POST "/users" for ::1 at 2023-06-03 12:14:14 -0400
Processing by UsersController#create as TURBO_STREAM
  Parameters: {"authenticity_token"=>"[FILTERED]", "user"=>{"name"=>"Kevin", "password"=>"[FILTERED]", "password_confirmation"=>"[FILTERED]"}, "commit"=>"Create User"}
  TRANSACTION (0.0ms)  begin transaction
  ↳ app/controllers/users_controller.rb:13:in `create'
  User Create (2.0ms)  INSERT INTO "users" ("name", "password_digest", "password_confirmation", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?)  [["name", "Kevin"], ["password_digest", "[FILTERED]"], ["password_confirmation", "[FILTERED]"], ["created_at", "2023-06-03 16:14:15.155800"], ["updated_at", "2023-06-03 16:14:15.155800"]]
  ↳ app/controllers/users_controller.rb:13:in `create'
  TRANSACTION (0.7ms)  commit transaction
  ↳ app/controllers/users_controller.rb:13:in `create'
Redirected to http://localhost:3000/users
Completed 302 Found in 255ms (ActiveRecord: 2.7ms | Allocations: 4113)
```

#### HTTP payload of logging in
```
Started POST "/user_sessions" for ::1 at 2023-06-03 16:00:13 -0400
Processing by UserSessionsController#create as TURBO_STREAM
  Parameters: {"authenticity_token"=>"[FILTERED]", "user"=>{"name"=>"Kevin", "password"=>"[FILTERED]"}, "commit"=>"Create User"}
  User Load (1.7ms)  SELECT "users".* FROM "users" WHERE "users"."name" = ? LIMIT ?  [["name", "Kevin"], ["LIMIT", 1]]
  ↳ app/controllers/user_sessions_controller.rb:7:in `create'
Redirected to http://localhost:3000/
Completed 302 Found in 268ms (ActiveRecord: 1.7ms | Allocations: 1290)
```

The `password` and `password_confirmation` are both masked with `[FILTERED]`.

## Conclusion
You now know how to build a very basic authentication mechanism. Storing password hash digests and memorizing and sending signed-in users' information with signed messages are common practices. You can extend this idea to see if you can add more features like resetting the user's password, sending confirmation letters, etc. 

To be honest, in practice, it is still recommended to use an existing gem, such as `devise` to conduct authentication. What? You say you just finished this article by following every step and then I tell you just to use existing gems? Yes 😆 Although you can build it from scratch, it doesn't mean you should do that. There's no reason for you to reinvent the wheel. 
{% embed https://dev.to/kevinluo201/how-to-setup-very-basic-devise-in-rails-7-55ia %}

However, making authentication is a very good practice that makes you understand the functionalities behind the scenes. The lesson you learn through building authentication will be invaluable and not limited to only Ruby on Rails but have a general idea for authentication and authorization of web applications.

If you think this article is helpful, you can buy me a coffee to encourage me 😉
[![Buy Me A Coffee](https://cdn.buymeacoffee.com/buttons/default-black.png)](https://www.buymeacoffee.com/kevinluo)

## Code Repository
https://github.com/kevinluo201/auth_from_scratch

## References
https://api.rubyonrails.org/classes/ActiveModel/SecurePassword/ClassMethods.html
https://guides.rubyonrails.org/security.html#sessions