Implementing JSON Web Token in Ruby on Rails
An obligatory theme in the world of back-end web development is user authentication. In the context of a Ruby on Rails project, the most common way to authenticate users is through the use of sessions. However, there are other ways to authenticate, such as JSON Web Token (JWT). I’m currently working on a project that uses JWT for authentication in some web frontend and Android apps. I’ve been studying this to understand some parts of the legacy code, reading articles, docs, and practicing in one of my personal projects, Alljobs. There are many articles and tutorials about JWT, and I’ll share the most useful ones that I’ve found and summarize the main points of my implementation in this post.
Let’s start
To begin, it’s useful to know the difference between authorization and authentication. Authentication is the process of verifying who a user is. On the other hand, authorization is the process of verifying what they have access to. JWT is a way to authenticate users, not to authorize them. It’s a standard for creating and consuming tokens in a secure way. It’s a compact, URL-safe means of representing claims (data) used to authenticate users on a website or app.
JWT works like a ping-pong game between the client and the server. The client sends a request to the server with the user’s credentials. The server validates the credentials, generates a token, and sends it to the client. The client stores the token and sends it in the header of each request. The server validates the token and sends the response. Diagram of the JWT flow request-response from Bluebash:
What’s the ball of the game? The token. It’s a string that contains three parts: header, payload, and signature. The token is composed of three parts separated by dots. The first part is the header, the second part is the payload, and the third part is the signature. The header and payload are JSON objects that are base64 encoded. The signature is the encoded header, encoded payload, a secret, the algorithm specified in the header, and the signing method. Example of a JWT token:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Image with JWT token parts from Nordic APIs:
Show me the code
I’ve implemented JWT in Alljobs. It’s a job opening website built using Ruby on Rails. The project is open-source, and you can check all the code in the repository. I’ll show the main parts of the implementation here.
First, I’ve added the jwt gem
to the Gemfile and installed it. It’s a Ruby implementation of the JSON Web Token (JWT) standard. It’s a simple library to encode and decode JWT tokens with useful instructions on how to use and examples in the Readme.
Now, with the power to handle JWT tokens, I’ve created the JsonWebToken
class in lib for encoding and decoding the JWT tokens. The code is shown below, pay attention to the encode method that adds an expiration time - “exp” - to the token and the use of the Rails secret key base to encode and decode the token. In this implementation, you do not hard-code the key or store it in an environment variable; you put it in a more secure place, the Rails credentials. This is a minimal explanation of how and why to do that. The key guarantees the security of the token; it’s used to encode and decode it, so it’s important to keep it safe.
class JsonWebToken
class << self
def encode(payload, exp = 24.hours.from_now)
payload[:exp] = exp.to_i
JWT.encode(payload, Rails.application.credentials.secret_key_base)
end
def decode(token)
JWT.decode(token, Rails.application.credentials.secret_key_base)
rescue StandardError
nil
end
end
end
In the decode method, I’ve used a rescue block to return nil if the token is invalid. We do that to handle the error in another part of the code. The token can be invalid for many reasons, such as the token is expired, the token is tampered with, the token is not signed with the correct key, etc. Now that we have the encode method, we can use it to generate the token for the user. I’ve created an Authenticate
class to handle the authentication.
It has an initialize method that receives the email and password of the user. The user method checks if the user exists and if the password is correct. If the user exists and the password is correct, the authenticate method is called and generates the call_user; if not, it raises an error. In the Alljobs implementation, there are User and also Headhunter, but the logic is the same. The code is shown below:
class Authenticate
def initialize(email, password, account_type)
@email = email
@password = password
@account_type = account_type
end
def call
authenticate!
JsonWebToken.encode(@payload)
end
private
attr_reader :email, :password
def authenticate!
@account = @account_type.constantize.find_by(email: email)
raise ActiveRecord::RecordNotFound unless @account&.valid_password?(password)
@account_type == "User" ? user_payload : headhunter_payload
end
def user_payload
@payload = { user_id: @account.id }
end
# Other method to headhunter_payload
end
The “authenticate!” method is responsible for checking if the user exists and if the password is correct. If the user exists and the password is correct, the payload is generated. The payload is a hash that contains the user or headhunter id; it’s used to generate the token. Note that the payload does not contain the password; it’s a secure way to ensure that the system does not have a method to access the password directly. The Authenticate
class is used in the TokensController
, the controller that is responsible for generating the token for the user. Depending on the user type, the controller calls the Authenticate
class with the correct user type. The code is shown below:
class TokensController < Api::V1::ApiController
def auth_user
authenticate("User")
end
def auth_headhunter
authenticate("Headhunter")
end
private
def authenticate(user_type)
auth = Authenticate.new(params[:email], params[:password], user_type).call
auth_user_type(user_type, auth)
rescue ActiveRecord::RecordNotFound
log_and_return_error(user_type)
end
# Other methods to log and return errors
end
When the user sends the email and password to the server, the controller calls the Authenticate
class with the correct user type. If the user exists and the password is correct, the token is generated and returned to the user; if not, an error is returned. The user must store the token and send it in the header of each request. The server must validate the token in each request. I’ve created an Authenticable
module with methods to validate the token and check if it is not expired (remember that we put a 24-hour period). The code is shown below:
module Authenticable
def authenticate_with_token
@token ||= request.headers["Authorization"]
render_unauthorized unless valid_token?
end
def valid_token?
body = JsonWebToken.decode(@token)
body[0]["exp"] > Time.now.to_i if body
end
# Other method to render unauthorized
end
The authenticate_with_token
method is called in the controller that needs to validate the token and check if it is expired. The Authenticable
module is included in the ApiController
to be used in all controllers that need to validate the token.
Conclusion
We’ve seen that JWT is a powerful tool to authenticate users in a secure way. We learned how to encode and decode the token, how to handle the authentication, how to generate the token, and how to validate the token. You can check the full implementation in the Alljobs repository. I hope this post helps you understand how to use JWT in a Ruby on Rails project. If you have any questions or suggestions, please let me know.