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:
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.
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.
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:
Setting Up the Application
To begin, create a new Rails API application using the following command:
rails new jwt_rails --api
Dependencies
In your
Gemfile
, add the necessary gems:jwt
andbcrypt
.gem 'jwt', '~> 2.7' gem 'bcrypt', '~> 3.1.7'
Then, run
bundle install
to install the gems.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
.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
User Model
In the
User
model (app/models/user.rb
), add thehas_secure_password
method and validation for username.class User < ApplicationRecord has_secure_password validates :username, presence: true, uniqueness: true end
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.
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.
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
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
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:
- 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.
- 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!