Securing Ruby on Rails Web Apps with JSON Web Tokens (JWTs)

Securing Ruby on Rails Web Apps with JSON Web Tokens (JWTs)

As I reflect on my recent experience at the API Festival in Nairobi, Kenya, I am excited to share my journey of learning about JSON Web Tokens (JWTs) and how I decided to implement this authentication method in a Ruby on Rails web application.

Introduction

Ensuring the security of user data is a fundamental aspect of any web application that deals with user interactions. There are various ways to achieve this, and in this article, we'll delve into the realm of authentication using JSON Web Tokens (JWTs) within the context of the Ruby on Rails framework.

What are JSON Web Tokens (JWTs)?

JSON Web Tokens (JWTs) are a streamlined way of transmitting information securely between parties as a JSON object. They consist of three essential parts, each playing a specific role in the token's structure:

  1. Header: The header typically contains two parts: the type of token (JWT) and the signing algorithm used, such as HMAC SHA256 or RSA. For example:

     {
       "alg": "HS256",
       "typ": "JWT"
     }
    

    This header is then Base64Url encoded to form the first part of the JWT.

  2. Payload: The second part of the token is the payload, which contains the claims. Claims are statements about an entity (usually the user) and additional data. There are three types of claims: registered, public, and private claims.

    • Registered claims: These are predefined claims that are not mandatory but recommended for interoperability. Examples include "iss" (issuer), "exp" (expiration time), "sub" (subject), and more. Note that claim names are kept short for compactness.

    • Public claims: These are custom claims defined by users of JWTs. To avoid clashes, they should be either defined in the IANA JSON Web Token Registry or as URIs with a collision-resistant namespace.

    • Private claims: These are custom claims that parties agree upon for sharing information. They aren't registered or public claims.

An example payload could look like this:

    {
      "sub": "1234567890",
      "name": "John Doe",
      "admin": true
    }

Like the header, the payload is Base64Url encoded to form the second part of the JWT.

Important: While claims are protected against tampering, they are readable by anyone. Sensitive information should not be placed in the payload or header without encryption.

  1. Signature: The third part of the JWT is the signature, which is used to verify that the sender of the JWT is who they claim to be and to ensure that the message wasn't altered in transit. The signature is created by hashing the encoded header, the encoded payload, a secret, and the algorithm specified in the header. For example, using HMAC SHA256:

     HMACSHA256(
       base64UrlEncode(header) + "." +
       base64UrlEncode(payload),
       secret)
    

    The output is a string of characters that serves as the signature.

When combined, the three parts are Base64Url encoded and separated by dots:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

JWTs are compact, secure, and widely used for various purposes, such as authentication and information exchange. They offer a concise way to convey information while maintaining security and integrity.

Below is a visual representation of an authentication flow with JWT:

Implementing JWT Authentication in Ruby on Rails

Now, let's explore how we can implement JWT authentication in a Ruby on Rails application. Here's a step-by-step guide:

  1. Setting Up the Application

    To begin, create a new Rails API application using the following command:

     rails new jwt_rails --api
    
  2. Dependencies

    In your Gemfile, add the necessary gems: jwt and bcrypt.

     gem 'jwt', '~> 2.7'
     gem 'bcrypt', '~> 3.1.7'
    

    Then, run bundle install to install the gems.

  3. Generate Models

    Generate the User and Product models using the following commands:

     rails generate model User username:string password_digest:string
     rails generate model Product name:string description:text
    

    Run the migrations with rails db:migrate.

  4. Create JSON Web Token Wrapper

    Create a JSON Web Token wrapper class in app/lib/json_web_token.rb to handle token encoding and decoding.

     class JsonWebToken
       JWT_SECRET = Rails.application.secrets.secret_key_base
    
       def self.encode(payload, exp = 12.hours.from_now)
         payload[:exp] = exp.to_i
         JWT.encode(payload, JWT_SECRET)
       end
    
       def self.decode(token)
         body = JWT.decode(token, JWT_SECRET)[0]
         HashWithIndifferentAccess.new(body)
       end
     end
    
  5. User Model

    In the User model (app/models/user.rb), add the has_secure_password method and validation for username.

     class User < ApplicationRecord
       has_secure_password
       validates :username, presence: true, uniqueness: true
     end
    
  6. Implementing JWT Security in Rails Controllers

    Let's delve into incorporating JWT security within our Rails controllers. We'll start with the ApplicationController located at app/controllers/application_controller.rb:

     class ApplicationController < ActionController::API
       before_action :authenticate
    
       rescue_from JWT::VerificationError, with: :invalid_token
       rescue_from JWT::DecodeError, with: :decode_error
    
       private
    
       def authenticate
         auth_header = request.headers['Authorization']
         token = auth_header.split(" ").last if auth_header
         decoded_token = JsonWebToken.decode(token)
    
         User.find(decoded_token[:user_id])
       end
    
       def invalid_token
         render json: { message: 'Invalid token' }
       end
    
       def decode_error
         render json: { message: 'Token decoding error' }
       end
     end
    

    In this scenario, we construct an "authenticate" method responsible for decoding the JSON Web tokens users send us. Upon successful token verification, we provide the User object representing the requester. Our focus is primarily on the positive outcome, bypassing many checks.

    By defining the "authenticate" method within the ApplicationController and establishing it as a "before_action," we secure any controller that inherits from it. Thus, for a request to access any other controller, a valid JWT is mandatory, given that each controller inherits this central controller.

  7. Authentication Controller

    The next step involves creating an AuthenticationController for users to send requests and receive signed JSON Web Tokens from our server. Locate this controller at app/controllers/authentication_controller.rb, and it could resemble the following:

     class AuthenticationController < ApplicationController
       skip_before_action :authenticate
    
       def login
         user = User.find_by(username: params[:username])
         authenticated_user = user&.authenticate(params[:password])
    
         if authenticated_user
           token = JsonWebToken.encode(user_id: user.id)
           expires_at = JsonWebToken.decode(token)[:exp]
    
           render json: { token:, expires_at: }, status: :ok
         else
           render json: { error: 'Unauthorized' }, status: :unauthorized
         end
       end
     end
    

    When a request reaches this controller, we intentionally avoid initial user authentication since users request tokens. This controller's purpose is to provide a token that users can utilize to access other resources on our server. This rationale necessitates the "skip_before_action :authenticate" directive on the second line.

    Within the "login" action (where users request tokens), we extract the username and password from the request parameters. If the user's authentication is successful – i.e., their provided credentials match our stored data – we furnish them with a signed token. This token is accompanied by information concerning its expiration.

    Later on, we'll employ "curl" to execute these steps and verify their functionality.

  8. Routes

    Define the routes in config/routes.rb for login and product access.

     Rails.application.routes.draw do
       post 'login', to: 'authentication#login'
       get 'products', to: 'products#index'
     end
    
  9. Products Controller

    Create a products controller (app/controllers/products_controller.rb) to protect the product resource with JWT authentication.

     class ProductsController < ApplicationController
       before_action :authenticate
    
       def index
         @products = Product.all
         render json: @products
       end
     end
    
  10. Testing Authentication with cURL

    Let's simulate the authentication process using cURL. Start by creating a new user in the console:

    User.create(username: "test1", password: "password")
    

    Now, use cURL to test the authentication:

    1. Request a token for the valid user:
    curl -H "Content-Type: application/json" -X POST -d '{"username":"test1","password":"password"}' http://localhost:3000/login

This should provide you with a signed JSON web token. Make sure to copy this token for the next step.

  1. Access protected products using the obtained token:

Replace <YOUR_TOKEN> with the token you received in the previous step:

    curl -H "Authorization: Bearer <YOUR_TOKEN>" http://localhost:3000/products

This command will allow you to access the protected products using the valid token. You should receive a response containing the product information.

By following these steps and using cURL, you can test the authentication process and ensure that your JSON web token implementation is working correctly.

Conclusion

In this article, I've shared my journey of discovering JSON Web Tokens (JWTs) during the recent API Festival in Nairobi, Kenya. I've demonstrated how to implement JWT authentication in a Ruby on Rails application, ensuring the security of user data while utilizing this stateless authentication method.

As you embark on your projects, consider the advantages of JWTs, such as their simplicity, efficiency, and strong security guarantees. By following the steps outlined in this article, you can implement JWT authentication in your Ruby on Rails applications, providing a robust and secure user authentication experience.

Happy coding, and may your applications remain safe and reliable!