DEV Community

Jason Park
Jason Park

Posted on

A Beginner’s Guide to Authentication and Authorization in Rails

Contents

Introduction

As we expand our app development knowledge, let's dive into the world of authentication and authorization in Ruby on Rails. In this guide, we'll cover the essential concepts and tools you need to build secure web applications. Authentication and authorization are fundamental to ensuring the right users access the right resources. We'll explore topics like cookies and sessions, BCrypt for secure password handling, user sign-up and logIn processes, auto-login mechanisms, and user logout procedures with examples. Plus, we'll delve into the art of authorizing actions to secure the backend server from unwanted requests. Let's get started on this crucial aspect of web app development!

Authentication vs. Authorization

First we need to define the differences between authentication and authorization.

Authentication is the process of verifying the identity of the user. In other words, we are checking to make sure the users are who they say they are.

Authorization is the process of allowing certain users to gain access to certain features in the app.

Basically, authentication answers the question of “who are you?” while authorization determines “what are you allowed to do?” The security of any web application lies in its ability to authenticate and authorize users effectively, safeguard sensitive data, prevent unauthorized access, and continuously adapt to emerging threats.

Cookies and Sessions

While there are many ways to authenticate users, we will be exploring how to achieve this via cookies and sessions. Cookies and sessions are fundamental tools in web development for maintaining user state and enhancing security. They allow us to keep track of user data and interactions throughout their visit to our application.

Cookies are small pieces of data that a web server sends to a user’s browser (server to client). These data packages are domain-specific and stored on the user’s device after the user has visited a website or a web application so that on subsequent visits, the server quickly knows who the client is.

Cookies serve various purposes including storing information such as user authentication tokens, shopping cart contents, or user preferences. They are stored on the client-side in the browser and sent back to the server with each HTTP request, allowing the server to recognize and remember the user. In the context of authentication, cookies often store user session information to keep a user logged in across multiple interactions with a web application.

Sessions are a server-side mechanism for maintaining user state. Unlike cookies, which are stored on the user’s device, sessions are stored on the server.

When a user interacts with a web application, a unique session identifier is typically stored in a cookie on the user's device. This session identifier allows the server to associate subsequent requests from the same user with their session data stored on the server. In the context of authentication and authorization, user session data can include information like the user's ID, which are used to determine what actions the user is allowed to perform within the application.

Here is how this works:

  1. User visits a website/application and logs in.
  2. Server generates a session on the server-side and sends a cookie containing a session identifier to the client's browser.
  3. The client's browser stores this cookie.
  4. On subsequent requests, the client's browser automatically sends the stored cookie with the session identifier back to the server.
  5. The server uses this session identifier to retrieve the user's data from its session storage and identifies the user.

In order to enable cookies and sessions in our Rails application, there are some things we need to set up.

In our config/application.rb:

module MyApp
    class Application < Rails::Application
        # Add cookies and session middleware
        config.middleware.use ActionDispatch::Cookies
        config.middleware.use ActionDispatch::Session::CookieStore

        # Use SameSite=Strict for all cookies to help protect against CSRF
        config.action_dispatch.cookies_same_site_protection = :strict

        # Initialize configuration defaults for originally generated Rails version.
        config.load_defaults 6.1

        # Only loads a smaller set of middleware suitable for API only apps.
        # Middleware like session, flash, cookies can be added back manually.
        # Skip views, helpers and assets when generating a new resource.
        config.api_only = true
    end
end
Enter fullscreen mode Exit fullscreen mode

In our app/controllers/application_controller.rb:

class ApplicationController < ActionController::API
    include ActionController::Cookies
end
Enter fullscreen mode Exit fullscreen mode

Once these files are properly set up, we can use cookies and sessions.

BCrypt

When a user first signs up, the server must store that information in their databases. However, one very important concept to keep in mind is that passwords are never saved as a plain text in any databases due to security risks. A standard method of storing passwords is through encryption via salting & hashing.

Salting is the process used to enhance the security of password storage. This process involves a random and unique piece of data called “salt”, which is generated and combined with every password before applying the hash function. This ensures that two users with the same password will have different values due to the unique salts.

Hashing is a one-way function that transforms the password into a fixed-length string of characters called “hash”. After a password has been salted, it undergoes the hashing process.

The primary characteristic of a good cryptographic hash function is that it's irreversible, meaning you can't reverse the process to retrieve the original password. Even a small change in the input data should produce a significantly different hash. In the context of password security, the user's password is salted and hashed and then stored in the database. When a user attempts to log in, their entered password is hashed again, and the resulting hash is compared to the stored hash in the database. If they match, the login attempt is successful.

Here is how it works:

  1. We have two users John and Jane with the same password, “Password123”.
  2. When they sign up, they are given two unique salts: John gets “sd9f6” and Jane gets “3x41p”.
  3. The salting process turns John’s password into “Password123sd9f6” and Jane’s into “Password1233x41p”.
  4. The hashing process via a secure hash function turns John’s salted password into “d3f4e5g6h7i8j9k0” and Jane’s into “m1n2o3p4q5r6s7t8”.
  5. The server stores John’s salt “sd9f6” and hashed password “d3f4e5g6h7i8j9k0” and Jane’s salt “3x41p” and hashed password “m1n2o3p4q5r6s7t8”.
  6. When John or Jane logs in, the salt and hash function converts their password input into the hashed version. If the hashed passwords match, they are successfully logged in.

To simplify this process as web developers, we can use a Ruby gem, BCrypt. BCrypt provides the has_secure_password method for the User model. This method:

  • Automatically salts and hashes user passwords
  • Automatically adds a password_confirmation attribute to the model (used for signing up and updating passwords).
  • Automatically adds validations for password-related inputs, such as its presence and any other specified requirements (minimum length, required characters, etc)
  • Provides the authenticate method for the Sessions controller.

All these features can be utilized after including the has_secure_password method in the User model.

class User < ApplicationRecord
    has_secure_password
end
Enter fullscreen mode Exit fullscreen mode

One last thing we need to do before we move on is to add the password_digest attribute as a string to our User model. This is where our salted and hashed passwords are stored. Once we include this attribute, BCrypt will automatically manage the passwords in a secure way.

Signing Up

To set up the sign-up logic in Rails, we first need to create the appropriate route in config/routes.rb.

Rails.application.routes.draw do
    # signing up
    post '/signup', to: 'users#create'
end
Enter fullscreen mode Exit fullscreen mode

Since we are creating a new User instance, we need to have the route point to the users#create controller action.

class UsersController < ApplicationController
    # post '/signup'
    def create
        render json: User.create!(user_params), status: :created
    rescue ActiveRecord::RecordInvalid => e
        render json: { errors: e.record.errors.full_messages }, status: :unprocessable_entity
end

    private

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

In our users#create action, we:

  1. Create a new instance of the User model and save it to our database using the sister method create!(). This allows for Rails to catch any exceptions raised by our rescue clause.
  2. Utilize strong parameters, which are defined under the private methods. We make sure to include both the :password and :password_confirmation attributes in the permitted params.
  3. Make sure to include the status: :created to let the browser know that the User was successfully created.
  4. Include the rescue ActiveRecord::RecordInvalid => e to catch any invalid records. We render this error message in a hash and set the status: :unprocessable_entity.

If we want to have the user automatically log in after creating a new account, we need to set up the login mechanism in our backend and create a new session after the frontend fetch post request.

Logging In

As with the sign up process, we first need to make a new route for this controller action.

Rails.application.routes.draw do
    # signing up
    post '/signup', to: 'users#create'
    # logging in
    post '/login', to: 'sessions#create'
end
Enter fullscreen mode Exit fullscreen mode

This time, we point to our sessions controller. Remember, that when users log in, they must request for the server to create a new session that they can store server-side and refer to when they receive the cookie with the session identifier. For this reason, our controller action will be sessions#create.

class SessionsController < ApplicationController
    # post '/login'
    def create
        user = User.find_by(username: params[:username])

        if user&.authenticate(params[:password])
            session[:user_id] = user.id
            render json: user, status: :created
        else
            render json: { error: 'Invalid username or password' }, status: :unauthorized
        end
    end
end
Enter fullscreen mode Exit fullscreen mode

In our sessions#create action, we:

  1. Use the .find_by() method on our User model to find the appropriate user by the username.
  2. Set up a conditional using the user&.authenticate(params[:password]) method. The &. is a shorthand way of saying “if the user object exists, invoke the authenticate(params[:password]) method on it. Otherwise, return nil.”
  3. Create a new session hash with the key :user_id and value set to the actual user id (user.id) if the conditional is true. This hash typically represents a session token or identifier, which is essential for subsequent authenticated requests.
  4. Render a response with the status: :created if the conditional is true. This response usually contains the session-related information.
  5. Render an error message “Invalid username or password” with status: :unauthorized if the conditional is false.

Once this controller action is set up, we can make a fetch post request from our frontend whenever we need to create a session (log in). If we have our sign-up logic set up, it is typical for the frontend to automatically log in, so the frontend can utilize this fetch request at the end of the sign-up process as well.

Auto Login

When the user reloads a page or closes and reopens the browser, it typically starts a new browsing session. This means the session cookie from the previous session is no longer available, and the server can't associate the new request with the old session. Instead of having the user enter their credentials every time, this feature allows for the user to maintain their logged in status through page reloads.

Like before, we start by creating a new route for a new controller action.

Rails.application.routes.draw do
    # signing up
    post '/signup', to: 'users#create'
    # logging in
    post '/login', to: 'sessions#create'
    # auto login
    get '/me', to: 'users#show'
end
Enter fullscreen mode Exit fullscreen mode

This time, we point to the users#show action since we are looking for an existing user instance.

class UsersController < ApplicationController
    # post '/signup'
    def create
        render json: User.create!(user_params), status: :created
    rescue ActiveRecord::RecordInvalid => e
        render json: { errors: e.record.errors.full_messages }, status: :unprocessable_entity
    end

    # get '/me'
    def show
        render json: User.find_by!(id: session[:user_id])
    rescue ActiveRecord::RecordNotFound
        render json: { error: 'Not authorized' }, status: :unauthorized
    end

    private

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

In our users#show action, we:

  1. Use the find_by!(id: session[:user_id]) method to find the existing user by its id.
  2. Render the error message with status: :unauthorized if the user is not authenticated (if the session has expired or the user is not logged in).

With this set up, we make sure our frontend triggers a fetch get request to this route on start-up. On React, this can be done using useEffect(() => {}, []).

Once we are able to retrieve the user information from the backend, we can properly set up our frontend to save our user state. On React, this can be done using useContext() to persist this data throughout our components.

Logging Out

Since we created a new session when logging in, we need to delete that session[:user_id] for logging out.

Rails.application.routes.draw do
    # signing up
    post '/signup', to: 'users#create'
    # logging in
    post '/login', to: 'sessions#create'
    # auto login
    get '/me', to: 'users#show'
    # logging out
    delete '/logout', to: 'sessions#destroy'
end
Enter fullscreen mode Exit fullscreen mode
class SessionsController < ApplicationController
    # post '/login'
    def create
        user = User.find_by(username: params[:username])

        if user&.authenticate(params[:password])
            session[:user_id] = user.id
            render json: user, status: :created
        else
            render json: { error: 'Invalid username or password' }, status: :unauthorized
        end
    end

    # delete '/logout'
    def destroy
        session.delete :user_id
        head :no_content
    end
end
Enter fullscreen mode Exit fullscreen mode

In our sessions#destroy action, we:

  1. Use the .delete(:user_id) method to delete the session[:user_id].
  2. Send an empty head with :no_content.

Authorizing

To add another layer of backend routing security, we want to restrict access to most of our controller actions to those who are authorized. To restrict all controller actions, we must create a custom method in the application controller using before_action.

class ApplicationController < ActionController::API
    include ActionController::Cookies
    before_action :authorize

    def authorize
        return render json: { error: "Not authorized" }, status: :unauthorized unless session.include? :user_id
    end
end

Enter fullscreen mode Exit fullscreen mode

This custom method authorize sends an error message “Not authorized” whenever the user tries to access a controller action before logging in. However, there are some actions that must be exempt from this.

If we think about it, we need to allow the user to be able to sign up and log in without this method stopping them. So we need to create exemptions for these actions. Here is what our final users and sessions controllers should look like.

class UsersController < ApplicationController
    skip_before_action :authorize, only: [:create]

    # post '/signup'
    def create
        render json: User.create!(user_params), status: :created
    rescue ActiveRecord::RecordInvalid => e
        render json: { errors: e.record.errors.full_messages }, status: :unprocessable_entity
    end

    # get '/me'
    def show
        render json: User.find_by!(id: session[:user_id])
    rescue ActiveRecord::RecordNotFound
        render json: { error: 'Not authorized' }, status: :unauthorized
    end

    private

    def user_params
        params.require(:user).permit(:username, :password, :password_confirmation)
    end
end
Enter fullscreen mode Exit fullscreen mode
class SessionsController < ApplicationController
    skip_before_action :authorize, only: [:create]

    # post '/login'
    def create
        user = User.find_by(username: params[:username])

        if user&.authenticate(params[:password])
            session[:user_id] = user.id
            render json: user, status: :created
        else
            render json: { error: 'Invalid username or password' }, status: :unauthorized
        end
    end

    # delete '/logout'
    def destroy
        session.delete(:user_id)
        head(:no_content)
    end
end
Enter fullscreen mode Exit fullscreen mode

The skip_before_action allows us to exempt the sign up and log in actions from our custom authorize method.

Conclusion

We’ve explored critical concepts of authentication and authorization in the context of web application security. We've established the importance of distinguishing between these two processes, ensuring that only authorized users can access protected resources while allowing for efficient authentication processes. Cookies and sessions were examined as tools for managing user sessions securely. Additionally, the use of BCrypt for password encryption was discussed, enhancing application security. By implementing these practices and understanding their roles, developers can build robust and secure web applications, safeguarding user data and privacy.

This guide is meant to serve as a learning tool for people who are just starting to learn authentication and authorization mechanisms in Rails. Please read the official documentation linked below for more information. Thank you for reading and if you have any questions or concerns about some of the material mentioned in this guide, please do not hesitate to contact me at jjpark987@gmail.com.

Resources

Rails Documentation on Sessions and Security
SameSite Documentation
BCrypt Documentation

Top comments (0)