DEV Community

Cover image for How !(not) to store passwords
anes
anes

Posted on • Updated on

How !(not) to store passwords

This article is the second part to how not to store passwords, which I advise you to read before reading this one. There we talk about the basics of password storage.
The source code of the first approach is here
The source code of the second approach is here

Why this now?

The last article about passwords and their storage was merely to explain how it can be done. This article is about making the knowledge from before useful.
I will show you two different approaches on how you can handle users. The first approach will be doing it by hand and the second one makes use of the devise gem, which is the proper way to do it.

Approach one - doing it by hand

This approach is the first one that someone might think of. Even though it works, it is by far the hardest and most insecure way. We will still do it this way, to better understand what happens when we actually do it with the devise gem.

Creating the project

Step one is to create our project using

rails new rails-auth-by-hand
Enter fullscreen mode Exit fullscreen mode

and jump in it with cd rails-auth-by-hand. There we generate our home_controller without the routes using

rails generate controller home index --skip-routes
Enter fullscreen mode Exit fullscreen mode

Then we go into our routes.rb file, where we define that root is home#index. Our routes file should look like this:

Rails.application.routes.draw do
  root 'home#index'
end
Enter fullscreen mode Exit fullscreen mode

Now we can start our server using rails server. Our root page should just display Home#index and a bit of text below it.

Importing bootstrap

To style this project we will use bootstrap. I like keeping bootstrap open while I design. This tutorial will not explain bootstrap or responsiveness as it is about security and not design.
Now to import bootstrap we take the <link> tag and put it in the header of our layouts/application.html.erb view file. The <script> tag we put into the last line before the <body> tag closes.
In the end the file should look as follows:

<!DOCTYPE html>
<html>
  <head>
    <title>RailsAuthByHand</title>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

    <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
    <%= javascript_importmap_tags %>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-iYQeCzEYFbKjA/T2uDLTpkwGzCiq6soy8tYaI1GyVh/UjpbCx/TYkiZhlZB6+fzT" crossorigin="anonymous">
  </head>

  <body>
    <%= yield %>
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.1/dist/js/bootstrap.bundle.min.js" integrity="sha384-u1OknCvxWvY5kfmNBILK2hRnQC3Pr17a+RTT6rIHI7NnikvbZlHgTPOOmMi466C8" crossorigin="anonymous"></script>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

Creating the models

Next we need to generate our user and a token model, which we will use as a session token. Now the user needs a username, a password and a password confirmation. For now we aren't going further than that. The token only needs a key and a user_id.
The user we generate by typing

rails generate model User username:string password:string confirmation:string
Enter fullscreen mode Exit fullscreen mode

And then the token:

rails generate model ManualSessionToken key:string user:references
Enter fullscreen mode Exit fullscreen mode

Now to get all of that into our database, we rails db:migrate. It is possible that your database doesn't exist yet. In that case just type rails db:create beforehand.

Creating controllers

Next we need to generate our controllers. The user controller we create by entering

rails g controller user show new create --skip-routes
Enter fullscreen mode Exit fullscreen mode

And the same for the session token controller

rails g controller sessionToken new create destroy --skip-routes
Enter fullscreen mode Exit fullscreen mode

What both of these controllers will be doing is quite intuitive: The user controller handles viewing an account and also creating one, the session token controller handles login and logout because login is the creation of a session token and logout is a deletion of the session token.

Then we add those routes in our routes.rb:

Rails.application.routes.draw do
  root 'home#index'

  get '/account', to: 'user#show'
  get '/register', to: 'user#new'
  post '/register', to: 'user#create'

  get '/login', to: 'session_token#new'
  delete '/logout', to: 'session_token#destroy'
  post '/login', to: 'session_token#create'
end
Enter fullscreen mode Exit fullscreen mode

Working on the frontend

First, we will work on our homepage. There we will have a card in the center of our screen with a sign up and either a login or logout button. For now we will show both a login and a logout button, but later we will give it a conditional render to appropriately display what it needs. The styling is actually quite easy with the use of bootstrap. My code for it looks as follows:

<div class="d-flex align-items-center justify-content-center vw-100 vh-100">
    <div class="card text-center home">
        <div class="card-header">
            <h3 class="card-title">Welcome!</h3>
        </div>
        <div class="card-body d-flex flex-column gap-2">
            <p>What would you like to do?</p>
            <a>
                <button type="button" class="btn btn-primary w-100">
                    Register
                </button>
            </a>
            <a>
                <button type="button" class="btn btn-primary w-100">
                    Log in
                </button>
            </a>
            <a>
                <button type="button" class="btn btn-danger w-100">
                    Log out
                </button>
            </a>
        </div>
    </div>
</div>

<style>
.home {
    width: fit-content;
    height: fit-content;
}
</style>
Enter fullscreen mode Exit fullscreen mode

Registration

Next, we will design our register form. Here I also used Bootstrap to my advantage, so that the design was done quicker. The form looks something like this:

<div class="d-flex align-items-center justify-content-center vw-100 vh-100">
    <%= form_with model: @user, url: "/register", method: :post, class: "rounded-2 border border-primary p-3" do |f| %>
        <div class="mb-3">
            <%= f.label :username, "Username", class: "form-label" %>
            <%= f.text_field :username, class: 'form-control' %>
            <div class="form-text">has to be unique</div>
        </div>
        <div class="mb-3">
            <%= f.label :password, "Password", class: "form-label" %>
            <%= f.password_field :password, class: 'form-control' %>
        </div>
        <div class="mb-3">
            <%= f.label :confirmation, "Password confirmation", class: "form-label" %>
            <%= f.password_field :confirmation, class: 'form-control' %>
        </div>
        <%= f.submit "Register", class: "btn btn-primary" %>
    <% end %>
</div>
Enter fullscreen mode Exit fullscreen mode

With the magic of bootstrap it looks like this out of the box:
Screenshot of register form
And the most impressing part is, that the div is centered!!
Then we will also want to connect the register button on our homepage with this page, so we add the URI, which is /register:

<a href='/register'>
    <button type="button" class="btn btn-primary w-100">
        Register
    </button>
</a>
Enter fullscreen mode Exit fullscreen mode

And finally we will add divs to conditionally display our flash messages in our application.html.erb. This code comes right below the opening <body> tag:

<% if flash[:error] %>
  <div class="alert alert-danger position-absolute top-0 start-50 translate-middle mt-5" role="alert" >
    <%= flash[:error] %>
  </div>
<% end %>
<% if flash[:success] %>
  <div class="alert alert-success position-absolute top-0 start-50 translate-middle mt-5" role="alert" >
    <%= flash[:success] %>
  </div>
<% end %>
Enter fullscreen mode Exit fullscreen mode

Then we write our controller. For that we go into our user_controller.rb. The new function will just make a new, empty, user instance, which is passed to the frontend:

def new
  @user = User.new
end
Enter fullscreen mode Exit fullscreen mode

Now our create function is the place where we actually save the user instance. If you try to print the params with puts params inside the create function it gives you something similar to this back:

{
"authenticity_token"=>"WdWaPB8Idxow55WaK43QfwZFc4irsRm1Fme6RQug96ezIulgDUokoHMx6-HpQ3O1_q5goLiuPU8dmnJwynrelQ", 
"username"=>"anes",
"password"=>"12345678",
"confirmation"=>"12345678",
"commit"=>"Register",
"controller"=>"user",
"action"=>"create"
}
Enter fullscreen mode Exit fullscreen mode

Now let us quickly go through what we can see. First we have the authenticity token. That is an auto-generated token by Rails which is responsible for our security. With that we can catch fake forms being sent in. Next we have the username we put in. Then comes our password and it's confirmation. The last three attributes specify where the request has been called from.
For us the username, password and its confirmation are important. In this tutorial we wont go over validation on the frontend, but a tutorial on how to do that is already in the making.
Next, we will try to validate that in our backend. Step one is creating strong parameters, so that the user can only submit the stuff we really want:

private

def user_params
  params.require(:user).permit(:username, :password, 
                               :confirmation)
end
Enter fullscreen mode Exit fullscreen mode

This blocks everything else that comes in when we get a form submission. Now let us start working on our validations and saving of the user. We will split up all of our validations into private methods as not to complicate our create function too much.
Step one is that the password has at least 6 characters. That is very simple to implement:

private
# ...
def invalid_length?
  return false if user_params[:password].length >= 6

  flash[:error] = 'Password must be at least 6 characters'
  redirect_to '/register'
end
Enter fullscreen mode Exit fullscreen mode

Now we want to check if the password matches with our validation:

private
#...
def password_not_matches?
  return false if user_params[:password] == user_params[:confirmation]

  flash[:error] = 'Password and confirmation do not match'
  redirect_to '/register'
end
Enter fullscreen mode Exit fullscreen mode

Then we want to check if the username is already in the database. For that we just query our database and if it exists we give an error:

def non_unique_username?
  return false unless User.find_by(username: user_params[:username])

  flash[:error] = 'Username already exists'
  redirect_to '/register'
end
Enter fullscreen mode Exit fullscreen mode

Now we reference all of those functions in our create method and return if they are false as not to trigger the rest of the function:

def create
  return if password_not_matches?
  return if invalid_length?
  return if non_unique_username?
end
Enter fullscreen mode Exit fullscreen mode

And then we obviously have to also try to store the user:

def create
  #...
  @user = User.new(user_params)
  if @user.save
    redirect_to root_path
  else
    flash[:error] = 'There was an unexpected error :/'
Enter fullscreen mode Exit fullscreen mode

Now we can see that our validation renders an flash message:
Image of our flash message
If we successfully register a user we get redirected to the root_path with a success message. To check if the user was created we go into the rails console by typing rails console. There we enter User.last where we should see the user. If you were attentive in my last tutorial you can see a major flaw: The password is being stored in plain text! We need to fix that ASAP.

Encrypting the password

Step one is creating a new column in our user table to save the salt in. For that we type in the command rails generate migration addSaltToUser salt:string which automatically creates following migration:

class AddSaltToUser < ActiveRecord::Migration[7.0]
  def change
    add_column :users, :salt, :string
  end
end
Enter fullscreen mode Exit fullscreen mode

And then we obviously need to migrate our database with rails db:migrate. If we now take a look at our user model we should see that salt has been created.
Now we actually need to use the salt. Step one is going into our user model. There we need to create an before_create callback, which only triggers before the user is created for the first time. Then we need to call a function to generate our salt and also make a constant that is our pepper:

class User < ApplicationRecord
  before_create :encrypt_password
  PEPPER = 'FollowAnesHodzaOnDev.toAndHodzaAnesOnTwitterIfYouEnjoy'.freeze

  private

  def encrypt_password
    self.salt = gen_salt
    self.password = Digest::SHA256.hexdigest(password + salt + PEPPER)
  end

  def gen_salt
    SecureRandom.hex(64)
  end
end
Enter fullscreen mode Exit fullscreen mode

Now if we check our users in the rails console we can see that one important thing is missing:
Screenshot of active model object
The confirmation shows the password. That is a major security flaw. We will just fix it by changing the confirmation hardcoded:

self.confirmation = 'follow @hodzaanes on twitter'
Enter fullscreen mode Exit fullscreen mode

Now checking the db should not give you any information about the password except for the salt, which is alright.

Login

Let's start off by going to the login page:
Screenshot of our login page
The function that is (as seen on the screenshot) SessionToken#new handles those. We first want to create the login form. We can just copy the register form and tweak it a bit: The destination has to be changed, the confirmation field is not needed anymore and the button text needs to be changed to 'Log in':

<div class="d-flex align-items-center justify-content-center vw-100 vh-100">
    <%= form_with model: @user, url: "/register", method: :post, class: "rounded-2 border border-primary p-3" do |f| %>
        <div class="mb-3">
            <%= f.label :username, "Username", class: "form-label" %>
            <%= f.text_field :username, class: 'form-control' %>
        </div>
        <div class="mb-3">
            <%= f.label :password, "Password", class: "form-label" %>
            <%= f.password_field :password, class: 'form-control' %>
        </div>
        <%= f.submit "Log in", class: "btn btn-primary" %>
    <% end %>
</div>
Enter fullscreen mode Exit fullscreen mode

Now in our home/index.html.erb we can link this site too:

<a href='/login'>
    <button type="button" class="btn btn-primary w-100">
        Log in
    </button>
</a>
Enter fullscreen mode Exit fullscreen mode

Then we need to code the login. That will consist of 2 parts: Checking if the login is correct and creating the session token.
We check if the login is correct by first getting a user that matches our username and then we check if the password matches the hash stored in the db:

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

  def create
    @user = User.find_by(username: user_params[:username])
    return if invalid_login?

    flash[:success] = 'Login successful'
    redirect_to '/account'
  end

  def destroy; end

  private

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

  def invalid_login?
    if @user && @user.password != Digest::SHA256.hexdigest(user_params[:password] + @user.salt + User::PEPPER)
      return false
    end

    flash[:error] = 'Invalid login :/'
    redirect_to '/login'
  end
end
Enter fullscreen mode Exit fullscreen mode

Don't forget the strong parameters!
The second part is creating the session token. That will be stored in the cookies. Now to validate if that session token is correct we now need to take our ManualSessionToken class and create a new one when a user logs in. First we start off by generating a random session token and storing it in the users cookies:

class SessionTokenController < ApplicationController
  #...
  def create
    #...
    cookies[:session_token] = generate_session_token
    ManualSessionToken.create(user_id: user.id, key: cookies[:session_token])
  #...
  private
  #...
  def generate_session_token
    SecureRandom.hex(64)
  end
  #...
end
Enter fullscreen mode Exit fullscreen mode

Now if a hacker gets access to our db and is able to see all our ManualSessionTokens it's the same as if he had every users password, because he can just query requests through the token. That's why we also need to salt and pepper this.
This is the same process as when we did it to the user:

rails generate migration addSaltToManualSessionToken salt:string
rails db:migrate
Enter fullscreen mode Exit fullscreen mode
class ManualSessionToken < ApplicationRecord
  belongs_to :user
  before_create :encrypt_session_key
  PEPPER = 'ThisPepperIsSoHotItWillMakeYouSweatAndStopTheHackerFromAccessingOurPasswords'.freeze

  private

  def encrypt_session_key
    self.salt = gen_salt
    self.key = Digest::SHA256.hexdigest(key + salt + PEPPER)
  end

  def gen_salt
    SecureRandom.hex(64)
  end
end
Enter fullscreen mode Exit fullscreen mode

Now we also need to think of other situations: what if the user logs in a second time?

Expanding the session key logic

Now we want to check if a SessionKey already exists when the user logs in. If that is the case, the user already has a SessionKey and instead of creating a new one every time we want to update that one:

def create
  #...
  cookies[:session_token] = generate_session_token
  manage_session_token
  #...
end
#...
private
#...
manage_session_token
  session_token = ManualSessionToken.where(user_id: @user.id).first
  if session_token
    session_token.update(key: cookies[:session_token])
  else
    ManualSessionToken.create(user_id: @user.id, key: cookies[:session_token])
  end
end
Enter fullscreen mode Exit fullscreen mode

Protecting a route

Now we want to implement the user account details view. We want to configure it in such a way, that the user can only see his account details if he is logged in.
First we need to create a view in html. That could look as following:

<div class="d-flex align-items-center justify-content-center vw-100 vh-100">
    <div class="d-flex align-items-center justify-content-center flex-column">
        <h1>Welcome <b><%= @user.username %></b></h1>
        <p>
            Here we would have some random data, if you even had any data
        </p>
    </div>
</div>
Enter fullscreen mode Exit fullscreen mode

The next step is to make any way for our application to know what user the token belongs to. We already have the user_id in our db next to the session token, so we can just use that. For that we just need to expand our stored cookie:

def create
  #...
  cookies[:session_token] = "#{@user.id};#{generate_session_token}"
  #...
end
Enter fullscreen mode Exit fullscreen mode

As you shouldn't store objects in the cookies we just concat both to a string which we can then later parse back to an object.
Now in our protected function user#show we have to first check if the cookie exists, if the server has a cookie and if the both match. If that is the case, the user gets access to the user with that id:

def show
  return unless logged?

  @user = User.find(parse_session_token[:id])
end
#...
private
#...
def parse_session_token
  token = cookies[:session_token]
  return { id: token.split(';')[0], key: token.split(';')[1] } if token

  false
  end


def logged?
  return false unless parse_session_token

  session_token = ManualSessionToken.find_by(user_id: parse_session_token[:id])
  return false unless session_token

  encrypt = Digest::SHA256.hexdigest(cookies[:session_token] + session_token.salt + ManualSessionToken::PEPPER)
  if encrypt == session_token.key
    true
  else
    send_to_login
    false
  end
end
Enter fullscreen mode Exit fullscreen mode

Logout

Now we want to create our logout. That logout exists of two functions: Destroying the server side ManualSessionToken and destroying the client-side cookie.
Let us first create the button. For that we can just use the rails button_to:

<%= button_to 'Log out', '/logout', method: :delete, class: "btn btn-danger w-100" %>
Enter fullscreen mode Exit fullscreen mode

Now if we want to make the buttons render conditionally we will have to reuse our logged? function from before. To reuse it here we need to extract it into a helper method. For that we delete our functions parse session token, logged? and send_to_login. We paste them into our application_helper.rb which makes it possible for us to access them from anywhere.
Caution: You need to reference the methods as helpers.my_function if you are in a controller, which means our user_controller#show looks like this:

def show
  return unless helpers.logged?

  @user = User.find(helpers.parse_session_token[:id])
end
Enter fullscreen mode Exit fullscreen mode

And our application_helper.rb:

module ApplicationHelper
  def parse_session_token
    #...
  end

  def logged?
    #...
  end

  def send_to_login
    #...
  end
end
Enter fullscreen mode Exit fullscreen mode

Now we can use the logged? in our home/index.html.erb to conditionally show our buttons:

<% if logged? %>
    <a href='/login'>
        <button type="button" class="btn btn-primary w-100">
            Log in
        </button>
    </a>
<% else %>
    <%= button_to 'Log out', '/logout', method: :delete, class: "btn btn-danger w-100" %>
<% end %>
Enter fullscreen mode Exit fullscreen mode

Which looks like this if you are logged in:
Screenshot of the homepage displaying a log out button

Approach two - devise gem

It may have been cool and important for our understanding to that by hand, but in the real world no one does it like that. When we need to set up authentication we just use the devise gem. You will soon see how much quicker it is.

Creating the project

This step is almost the same as in approach one:

rails new rails-auth-with-devise
cd rails-auth-with-devise
bundle add devise
bundle install
rails generate controller home index --skip-routes
Enter fullscreen mode Exit fullscreen mode

routes.rb:

Rails.application.routes.draw do
  root 'home#index'
end
Enter fullscreen mode Exit fullscreen mode

Add bootstrap to your application.html.erb again:

<!DOCTYPE html>
<html>
  <head>
    <title>RailsAuthWithDevise</title>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
    <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
    <%= javascript_importmap_tags %>
  </head>

  <body>
    <%= yield %>
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

Now we also make our home/index.html.erb look the same as it did before:

<div class="d-flex align-items-center justify-content-center vw-100 vh-100">
    <div class="card text-center home">
        <div class="card-header">
            <h3 class="card-title">Welcome!</h3>
        </div>
        <div class="card-body d-flex flex-column gap-2">
            <p>What would you like to do?</p>
            <a>
                <button type="button" class="btn btn-primary w-100">
                    Register
                </button>
            </a>
            <a>
                <button type="button" class="btn btn-primary w-100">
                    Log in
                </button>
            </a>
            <a>
                <button type="button" class="btn btn-danger w-100">
                    Log out
                </button>
            </a>
        </div>
    </div>
</div>

<style>
.home {
    width: fit-content;
    height: fit-content;
}
</style>
Enter fullscreen mode Exit fullscreen mode

Adding devise

Now that we set up the basics we can start with devise. It is actually quite easy to set it up. We start off by typing rails generate devise:install. Now we get four instructions, which we will follow. First we go into our development.rb and add following code:

Rails.application.configure do
  #...
  config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }
end
Enter fullscreen mode Exit fullscreen mode

Step two we already did before. For step three we will do the same as we did in the first tutorial for our flash messages but with a small tweak:

<body>
  <% if alert %>
    <div class="alert alert-danger position-absolute top-0 start-50 translate-middle mt-5" role="alert" >
      <%= alert %>
    </div>
  <% end %>
  <% if notice %>
    <div class="alert alert-success position-absolute top-0 start-50 translate-middle mt-5" role="alert" >
      <%= notice %>
    </div>
  <% end %>
  <!-- ... -->
</body>
Enter fullscreen mode Exit fullscreen mode

Now before we do step four, we need to generate our model:

rails generate devise user
Enter fullscreen mode Exit fullscreen mode

That generated a few things. Important are: the migration, the model and the new route. We run the migration with rails db:migrate to update our schema and add the user model in our database. The devise_for created all the routes used by devise. Those routes still need views, which we can create by typing rails generate devise:views.
By typing rails routes -g user we can see, that devise created everything for us. We can just link it to the home/index.html.erb like we did before:

<a href="/users/sign_up">
  <button type="button" class="btn btn-primary w-100">
    Register
  </button>
</a>
<a href="/users/sign">
  <button type="button" class="btn btn-primary w-100">
    Log in
  </button>
</a>
<%= button_to "Log out", '/users/sign_out', method: :delete, class: "btn btn-danger w-100" %>
Enter fullscreen mode Exit fullscreen mode

By clicking on those links we can see that the forms already all exist:
Screenshot of sign up form
If you want to, you can also style these forms, but I won't go over that again.
For our conditional rendering of the login or logout button we used a helper we manually created. Devise also offers us that:

<% unless user_signed? %>
  <a href="/users/sign">
    <button type="button" class="btn btn-primary w-100">
      Log in
    </button>
  </a>
<% else %>
  <%= button_to "Log out", '/users/sign_out', method: :delete, class: "btn btn-danger w-100" %>
<% end %>
Enter fullscreen mode Exit fullscreen mode

When you try to register there may be the issue undefined method user_url. If that is the case add this line to initializers/devise.rb:

Devise.setup do |config|
  #...
  config.navigational_formats = ['*/*', :html, :turbo_stream]
  #...
end
Enter fullscreen mode Exit fullscreen mode

And that's already it! Now you can officially claim that you know how to handle users, logins, sessions and much more in Ruby on Rails.

Top comments (0)