devise is one of popular authentication gem for rails. But, we are not using any gem here for user authentication here so that we can understand it from the base. We will be familiarized with working mechanism. Once we get basic knowledge, we can use them since they are for us.
So, lets start creating model and controllers for it.
Let's generate controller for users
rails g controller home index
Output:
create app/controllers/home_controller.rb
route get 'home/index'
invoke erb
create app/views/home
create app/views/home/index.html.erb
invoke test_unit
create test/controllers/home_controller_test.rb
invoke helper
create app/helpers/home_helper.rb
invoke test_unit
The rails generate command above seem new for us. The syntax is rails g controller controller_name method_name(s). As seen in the output, the command generated view file for index method inside app/views/home/index.html.erb.
Why do we store a password digest instead of the password?
Storing a user's actual password in the database is a serious security risk. If the database is ever leaked, anyone with access to it would immediately know every user's password.
Instead, Rails stores a password digest, which is a one-way encrypted representation of the password. A digest cannot be converted back into the original password.
When a user logs in:
- They enter their password.
- Rails creates a digest of the entered password.
- It compares that digest with the one stored in the database.
- If they match, the user is authenticated.
This means the application never needs to know or store the user's original password after it has been set.
Rails provides the has_secure_password feature for handling password hashing, and it uses the bcrypt gem internally.
Let's add the gem to the Gemfile and install it.
gem 'bcrypt', '~> 3.1', '>= 3.1.20'
bundle install
What is bcrypt?
bcryptis a password hashing library specifically designed for storing passwords securely.Unlike general encryption, password hashing is a one-way process. The original password cannot be recovered from its digest.
bcryptis intentionally slow compared to normal hashing algorithms, making brute-force attacks significantly more difficult. It also automatically generates a unique salt for every password, ensuring that two users with the same password will still have different password digests stored in the database.
We will be creating a scaffold of User.
rails g scaffold user email:uniq password:digest
Scaffolding
A scaffold is a set of automatically generated files which forms the basic structure of a Rails project. These files include:
- A controller
- A model
- Views for every standard controller action (index, edit, show, new)
- A new route
- And a migration to prepare your database.
We can add additional fields to the migration file according to our requirements. We need to migrate the database after creating new migration file by:
rails db:migrate
The email:uniq stores the email address and also creates the unique database index. Incase of password_digest, the generator creates a password_digest field which asks for an additional password_confimation in the form.
Lets add some validations to user model.
class User < ApplicationRecord
has_secure_password
validates :email, presence: true, uniqueness: true
end
Active Record Integration
Adding
has_secure_passwordautomatically handles password hashing, adds validation for password presence and confirmation, and provides the.authenticatemethod for user login verification. Note that your database table must have a column namedpassword_digestfor this to work.
This makes sure that we have a unique email address.
We can start the rails server and create the new user and don't forget to add the routes for users.
Rails.application.routes.draw do
resources :users
end
A session is created when a user logs in and is destroyed when same user logs out.
Session
"Session" is the term used to refer to a user's time browsing a web site. It's meant to represent the time between their first arrival at a page in the site until the time they stop using the site. In practice, it's impossible to know when the user is done with the site.
Lets create a sessions controller with actions:
rails g controller sessions new create destroy
and in sessions_controller we have
class SessionsController < ApplicationController
def new
end
def create
end
def destroy
end
end
Let's find a user using their email and if the user is found from it's email and the password they have given is correct, create a new session for the user and redirect the user to root path.
# code above here remain unchanged
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] = "Email or password is invalid"
render :new, status: :unprocessable_entity
end
end
# code below here remain unchanged
To destroy the user session, we simply set the session for user to nil and redirect to root path.
def destroy
session[:user_id] = nil
redirect_to root_url, notice: "Logged out!"
end
and the new form for sessions should look like this:
<% if alert %>
<div class="p-4 mb-4 text-sm text-red-800 rounded-lg bg-red-50 dark:bg-gray-800 dark:text-red-400" role="alert">
<span class="font-medium"><%= alert %></span>
</div>
<% end %>
<h4>Login</h4>
<%= form_tag sessions_path do |form| %>
<div>
<%= label_tag :email %>
<%= text_field_tag :email %>
</div>
<div>
<%= label_tag :password %>
<%= password_field_tag :password %>
</div>
<div>
<%= submit_tag "Login" %>
</div>
<% end %>
If you want a login page using flowbite design then add following code in sessions/new.html.erb
<% if alert %>
<div class="p-4 mb-4 text-sm text-red-800 rounded-lg bg-red-50 dark:text-red-400" role="alert">
<span class="font-medium"><%= alert %></span>
</div>
<% end %>
<section class="bg-gray-50">
<div class="flex flex-col items-center justify-center px-6 py-8 mx-auto md:h-screen lg:py-0">
<div class="flex items-center mb-6 text-2xl font-semibold text-gray-900"> Login page </div>
<div class="w-full bg-white rounded-lg shadow dark:border md:mt-0 sm:max-w-md xl:p-0 dark:bg-gray-800 dark:border-gray-700">
<div class="p-6 space-y-4 md:space-y-6 sm:p-8">
<h1 class="text-xl font-bold leading-tight tracking-tight text-gray-900 md:text-2xl dark:text-white">
Sign in to your account
</h1>
<form class="space-y-4 md:space-y-6" action="<%= sessions_path %>" method="post">
<div>
<label for="email" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Your email</label>
<input type="email" name="email" id="email" class="bg-gray-50 border border-gray-300 text-gray-900 rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="name@example.com" required="">
</div>
<div>
<label for="password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Password</label>
<input type="password" name="password" id="password" placeholder="••••••••" class="bg-gray-50 border border-gray-300 text-gray-900 rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" required="">
</div>
<button type="submit" class="w-full text-white bg-primary-600 hover:bg-primary-700 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800">Sign in</button>
<p class="text-sm font-light text-gray-500 dark:text-gray-400">
Don’t have an account yet? <a href="#" class="font-medium text-primary-600 hover:underline dark:text-primary-500">Sign up</a>
</p>
</form>
</div>
</div>
</div>
</section>
You may change the design as your preference. now lets add the routes for sessions as well N
Rails.application.routes.draw do
root 'home#index'
resources :users
resources :sessions, only: [:new, :create, :destroy]
get 'signup', to: 'users#new', as: 'signup'
get 'login', to: 'sessions#new', as: 'login'
delete 'logout', to: 'sessions#destroy', as: 'logout'
end
The user can use http://localhost:3000/login to access login page.
current_user
current_user is a helper method that is used to get the logged in user in most rails applications.
We can use this method by adding it in the application_controller.
#app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
helper_method :current_user
def current_user
if session[:user_id]
@current_user ||= User.find(session[:user_id])
else
@current_user = nil
end
end
end
We will be using this method in our app.
Open rails console by rails c in terminal and create a user
User.create(email: "test@example.com", password: "password123", password_confirmation: "password123")
After this you can login with the credential given above in login page.