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
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
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
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>
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
And then the token:
rails generate model ManualSessionToken key:string user:references
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
And the same for the session token controller
rails g controller sessionToken new create destroy --skip-routes
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
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>
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>
With the magic of bootstrap it looks like this out of the box:
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>
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 %>
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
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"
}
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
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
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
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
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
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 :/'
Now we can see that our validation renders an 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
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
Now if we check our users in the rails console we can see that one important thing is missing:
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'
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:
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>
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>
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
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
Now if a hacker gets access to our db and is able to see all our ManualSessionToken
s 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
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
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
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>
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
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
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" %>
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
And our application_helper.rb
:
module ApplicationHelper
def parse_session_token
#...
end
def logged?
#...
end
def send_to_login
#...
end
end
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 %>
Which looks like this if you are logged in:
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
routes.rb
:
Rails.application.routes.draw do
root 'home#index'
end
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>
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>
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
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>
Now before we do step four, we need to generate our model:
rails generate devise user
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" %>
By clicking on those links we can see that the forms already all exist:
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 %>
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
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)