Authenticate Your Rails API with JWT from Scratch

Share this article

Authenticate Your Rails API with JWT from Scratch

Authentication is one of the vital parts of any web application. There are innumerable libraries and frameworks that provide various options to perform authentication one way or another. These libraries take away much of the groundwork required to setup an authentication system, providing “magic” with what’s happening behind the scenes. For Rails, we have a number of authentication systems, the prominent one being Devise.

Devise is an authentication engine that runs as part of our application and does all the heavy lifting when it comes to authentication. However, often times we don’t need many of the parts it provides. For example, Devise doesn’t work very well with API-based systems, which is why we have the devisetokenauth gem. devisetokenauth is a library that does what Devise does, but with tokens instead of sessions.

Today we’re going to explore building our own custom JWT-based authentication system from scratch. Let’s get started.

NOTE: This Tutorial is aimed for API-based authentication.

Why JWT

JWT (JSON Web Token, pronounced “jot”) is a self-contained authentication standard designed for securely exchanging data between systems. Since it’s self-contained, it doesn’t need any backing storage to work. Also, the JWT approach is very reliable and flexible, allowing it to be used with any client. It doesn’t have any overhead to get started and almost all languages have libraries that make working with JWTs a breeze.

We already have an excellent tutorial about JWT and how to use it with Rails. If you’re new to JWT I’d suggest you to read it first to get an idea of what we’re going to create.

Model

We’ll begin by building a model for our app. The application will authenticae users, so let’s create a User model.

rails g model user

This command will create a migration file under db/migrate called XXXcreateusers, where XXX is the current date. Add the following code to this file, which will add the columns we need:

create_table :users do |t|
  t.string :email,              null: false
  t.string :password_digest,    null: false
  t.string   :confirmation_token
  t.datetime :confirmed_at
  t.datetime :confirmation_sent_at

  t.timestamps
end

Run rake db:migrate to run the migration.

We’re adding email and password_digest columns here, which are the basic columns required to register or authenticate a user. Of course, you can add more columns that makes sense, if you like. The confirmation_token, confirmed_at, and confirmation_sent_at columns are required for user confirmation. You can skip this if you don’t wish to confirm the emails.

Validations

Let’s add our validations to models/user.rb:

validates_presence_of :email
validates_uniqueness_of :email, case_sensitive: false
validates_format_of :email, with: /@/

We’re validating the uniqueness of email without case sensitivity and doing a simple format check of the email ensuring there is a @ in the email string. This might not be a solid validation, but there is no standard one and that’s why we verify the email upon registration.

Callbacks

We are going to make use of Rails’s hassecurepassword for handling the password hashing.

To start, add the gem bcrypt (it’s probably there, but commented out) to your Gemfile and run bundle install. Once the gem is installed, add the following line inside the User class in the file models/user.rb:

class User < ApplicationRecord
  has_secure_password
  ...

Let’s also add callbacks that we want to perform before user creation. First, we’d want to lowercase the email and strip any spaces. Second, we’d have to generate a confirmation token that will be sent in the email to the user.

Add these before callbacks to the models/user.rb file:

before_save :downcase_email
before_create :generate_confirmation_instructions

We downcase the email before saving it to the DB. The confirmation instructions should be generated only during the creation of the user record. Let’s add the methods:

def downcase_email
  self.email = self.email.delete(' ').downcase
end

def generate_confirmation_instructions
  self.confirmation_token = SecureRandom.hex(10)
  self.confirmation_sent_at = Time.now.utc
end

If you plan to skip the confirmation, you can omit the above method and the corresponding callback.

Registration

Create

We now have the model setup, so let’s add an endpoint for user creation. Create our users controller:

rails g controller users

Add the below line to config/routes.rb:

resources :users, only: :create

We now have generated our UsersController. Head over to the controller (app/controllers/users_controller.rb) and add the following lines:

def create
  user = User.new(user_params)

  if user.save
    render json: {status: 'User created successfully'}, status: :created
  else
    render json: { errors: user.errors.full_messages }, status: :bad_request
  end
end

private

def user_params
  params.require(:user).permit(:email, :password, :password_confirmation)
end

We now have an API endpoint to create a user! You can try it by starting the server and sending a POST request with the user details as JSON in the body. Here’s an example of what you could post to http://localhost:3000/users:

{
  user: {
    email: 'test@example.com',
    password: 'anewpassword',
    password_confirmation: 'anewpassword'
  }
}

You should receive the User created successfully message as the response. Subsequent requests with the same data should respond with error messages. That’s our validations in play. For those who are skipping the confirmation part, you can skip the following and head to the Login section.

Confirmation

One thing that is pending is the user confirmation. We are going to send an email confirmation to the user before creation and create an endpoint that validates the token to confirm the user.

To start, we have to send an email to the user when the record is successfully created. We wouldn’t be going through how to send emails since there are already good tutorials that cover how to do it. We should add the line to send email right after user.save in users_controller.

...
if user.save
  #Invoke send email method here
...

Just make sure that you include the user.confirmation_token in your email. Ideally, the URL should lead to an endpoint that fetches the token and posts it to our API. Let’s create that post API endpoint.

Adda route to our config/routes.rb file for the confirm endpoint:

resources :users, only: :create do
  collection do
    post 'confirm'
  end
end

Now, create the confirm action in UsersController:

def confirm
  token = params[:token].to_s

  user = User.find_by(confirmation_token: token)

  if user.present? && user.confirmation_token_valid?
    user.mark_as_confirmed!
    render json: {status: 'User confirmed successfully'}, status: :ok
  else
    render json: {status: 'Invalid token'}, status: :not_found
  end
end

Let’s see what we’re doing here. First, we are fetching the token from params, calling to_s to handle the case where the token is not sent in the request. Next, fetch the corresponding user based on the confirmation token.

If the user is present and the confirmation isn’t expired, call the model method mark_as_confirmed! and respond with a success message. We have to add the confirmation_token_valid? and mark_as_confirmed! methods to our User model:

def confirmation_token_valid?
  (self.confirmation_sent_at + 30.days) > Time.now.utc
end

def mark_as_confirmed!
  self.confirmation_token = nil
  self.confirmed_at = Time.now.utc
  save
end

The confirmation_token_valid? method checks if the confirmation was sent in the last 30 days and, thus, is not expired. You can change it to any value you wish.

mark_as_confirmed! saves the confirmed time and nullifies the confirmation token so that the same confirmation email can’t be used to confirm the user again.

We now have the endpoint to confirm a user. You can test it by sending a post request to the endpoint users/confirm?token=<CONFIRMATION_TOKEN> and check the confirmed_at and confirmation_token value for the user. You should get User confirmed successfully. Subsequent requests with the same token should return Invalid token.

We now have a working registration as part of our app. Let’s create the final piece: the login endpoint.

Login

For login, we are going to create an endpoint to log the user in by sending the credentials of the user and responding with a JWT token. We also will add a helper method that can be used to secure endpoints that we want to be exposed only to authenticated users.

Controller

Let’s add the login route to our config/routes.rb file under users resource:

resources :users, only: :create do
    collection do
      post 'confirm'
      post 'login'
    end
end

We have created a users/login route now. The controllers require some modification to pull in the json_web_token code and create the corresponding action in the UsersController.

In controllers/application_controller.rb, add the below line right after the class definition:

require 'json_web_token'

In controllers/users_controller.rb add the following snippet:

def login
  user = User.find_by(email: params[:email].to_s.downcase)

  if user && user.authenticate(params[:password])
      auth_token = JsonWebToken.encode({user_id: user.id})
      render json: {auth_token: auth_token}, status: :ok
  else
    render json: {error: 'Invalid username / password'}, status: :unauthorized
  end
end

Let’s go over this method step by step. First, we’re fetching the user from the email and, if present, call the authenticate method passing the password that is supplied. The authenticate method is provided by the has_secure_password helper.

Once we verify the email and password, encode the user’s id into a JWT token via our encode method from JsonWebToken lib which we have yet to create. Then, return the token.

For those who are confirming emails, we have to disallow users who aren’t confirmed. Modify the controller action to include one more conditional:

...
if user && user.authenticate(params[:password])
  if user.confirmed_at?
    auth_token = JsonWebToken.encode({user_id: user.id})
    render json: {auth_token: auth_token}, status: :ok
  else
    render json: {error: 'Email not verified' }, status: :unauthorized
  end
else
...

Checking if the confirmed_at field is not empty does the job, meaning user has been confirmed before allowing them to login.

JWT Library

Now, let’s add the JWT library. Begin by adding the following gem to your Gemfile and do bundle install:

gem 'jwt'

Once done, create a file called jsonwebtoken.rb under lib/ and add these lines:

require 'jwt'

class JsonWebToken
  # Encodes and signs JWT Payload with expiration
  def self.encode(payload)
    payload.reverse_merge!(meta)
    JWT.encode(payload, Rails.application.secrets.secret_key_base)
  end

  # Decodes the JWT with the signed secret
  def self.decode(token)
    JWT.decode(token, Rails.application.secrets.secret_key_base)
  end

  # Validates the payload hash for expiration and meta claims
  def self.valid_payload(payload)
    if expired(payload) || payload['iss'] != meta[:iss] || payload['aud'] != meta[:aud]
      return false
    else
      return true
    end
  end

  # Default options to be encoded in the token
  def self.meta
    {
      exp: 7.days.from_now.to_i,
      iss: 'issuer_name',
      aud: 'client',
    }
  end

  # Validates if the token is expired by exp parameter
  def self.expired(payload)
    Time.at(payload['exp']) < Time.now
  end
end

Let’s go through the code. First, in the encode method, merge the payload which is the user id with meta information such as expiry, issuer, and audience. You can learn about these “meta” claims from the JWT tutorial linked at the beginning of this post. Once merged, encode the payload using the JWT.encode method along with the secret key from our server. It is important that this key is kept secure since this is the master key for all the tokens our app issues.

Then, the decode method, which uses the decode method from the jwt gem to (you guessed it) decode the payload using the secret key. We have a couple of other helper methods, one is valid_payload which validates if the payload has been tampered with, and the expired method which validates whether the token has expired. The default expiry is set in the meta method which is 7 days, but you are free to change it as per your requirement.

Now we have a functioning login endpoint that we can use to login the user. Try calling the endpoint users/login with the below formatted data in the request body:

{
  "email": "test@example.com",
  "password": "anewpassword"
}

You should see a response similar to this,

{
  "auth_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE0NzUzMTM5OTQsImlzcyI6Imlzc3Vlcl9uYW1lIiwiYXVkIjoiY2xpZW50In0.5P3qJKelCdbTixnLyIrsLKSVnRLCv2lvHFpXqVKdPOs"
}

There it is. Our authentication token for the user. We can now use this token to validate each request for the user.

Authentication Helper

Alright, we are going to create a helper method that gets the token from the header, validates the payload, and fetches the corresponding user from the DB. Open up /app/controllers/application_controller.rb and add the following:

protected
# Validates the token and user and sets the @current_user scope
def authenticate_request!
  if !payload || !JsonWebToken.valid_payload(payload.first)
    return invalid_authentication
  end

  load_current_user!
  invalid_authentication unless @current_user
end

# Returns 401 response. To handle malformed / invalid requests.
def invalid_authentication
  render json: {error: 'Invalid Request'}, status: :unauthorized
end

private
# Deconstructs the Authorization header and decodes the JWT token.
def payload
  auth_header = request.headers['Authorization']
  token = auth_header.split(' ').last
  JsonWebToken.decode(token)
rescue
  nil
end

# Sets the @current_user with the user_id from payload
def load_current_user!
  @current_user = User.find_by(id: payload[0]['user_id'])
end

Here, authenticate_request! is the helper method that we are going to use to authenticate controller actions. It fetches the payload from the Authorization header of the request, then validates the payload using the valid_payload method, which we’ve seen before. Once confirmed valid, it fetches the user using the user_id in the payload, loading the user record into the scope.

We can now add the authenticate_request method as a before_filter to any controller action that we want to be authenticated for the user.

Conclusion

With that we have come to the conclusion of this tutorial. We covered the meaty aspects of user authentication – registration, confirmation, and login – all through an API using JWT. You can take this forward as is or make any sort of modifications to parts of it, maybe adding separate components like password reset, email update, locking accounts, and much more.

The example app is hosted on github. Feel free to fork and mess with it.

I thank you for reading this tutorial and I hope it serves your purposes. Happy learning.

Frequently Asked Questions (FAQs) about JWT Authentication in Rails API

How can I refresh JWT tokens in Rails API?

Refreshing JWT tokens in Rails API involves creating a new token and invalidating the old one. This can be done by storing the token in a blacklist upon logout or expiration. You can use gems like ‘devise-jwt’ which provides a revocation strategy that allows you to blacklist tokens. When a request is made with a blacklisted token, it will be rejected, thus ensuring the token is effectively “refreshed”.

How can I handle JWT expiration in Rails API?

JWT tokens have an expiration time, after which they are no longer valid. You can handle this in Rails API by checking the ‘exp’ claim of the token. If the current time is beyond the ‘exp’ time, the token is expired. You can then either prompt the user to log in again or automatically issue a new token, depending on your application’s requirements.

How can I secure JWT tokens in Rails API?

Securing JWT tokens involves ensuring they are not intercepted or tampered with. This can be done by using HTTPS for all communications, storing tokens securely on the client side (e.g., in HttpOnly cookies), and using a strong signing key for the tokens. Additionally, you can use libraries like ‘ruby-jwt’ which provide features for validating and decoding tokens, ensuring they have not been tamely altered.

How can I use JWT with Devise in Rails API?

Devise is a popular authentication solution for Rails, and it can be used with JWT by using the ‘devise-jwt’ gem. This gem provides a JWT user authentication strategy for Devise, allowing you to issue and validate JWT tokens for users. You can configure Devise to use this strategy in your Devise initializer.

How can I test JWT authentication in Rails API?

Testing JWT authentication involves making requests with valid and invalid tokens and checking the responses. You can use libraries like ‘rspec’ for this. For example, you can create a valid token for a user, make a request with this token, and expect a successful response. Then, you can make a request with an invalid or expired token and expect an error response.

How can I handle JWT errors in Rails API?

JWT errors can occur for various reasons, such as an invalid token, an expired token, or a token with invalid claims. You can handle these errors by rescuing them in your application and returning an appropriate error response. For example, if a ‘JWT::DecodeError’ is raised, you can return a ‘401 Unauthorized’ response.

How can I decode JWT tokens in Rails API?

Decoding JWT tokens involves verifying the signature and decoding the payload. You can use the ‘ruby-jwt’ library for this. This library provides a ‘decode’ method that takes a token and a key, verifies the signature of the token using the key, and returns the decoded payload.

How can I issue JWT tokens in Rails API?

Issuing JWT tokens involves creating a payload with the user’s information and encoding it into a token. You can use the ‘ruby-jwt’ library for this. This library provides an ‘encode’ method that takes a payload and a key, and returns a token.

How can I use JWT for authorization in Rails API?

JWT can be used for authorization by including claims in the token that specify the user’s roles or permissions. When a request is made, the token can be decoded and the claims checked to determine if the user is authorized to perform the requested action.

How can I implement JWT authentication from scratch in Rails API?

Implementing JWT authentication from scratch involves several steps. First, you need to create a method to issue tokens, which involves creating a payload with the user’s information and encoding it into a token. Then, you need to create a method to authenticate requests, which involves decoding the token from the request, verifying the signature, and checking the claims. Finally, you need to handle token expiration and errors, and secure the tokens.

Vinoth is a Server Administrator turned Full stack web developer. He loves to try his hands on multiple programming languages but his primary programming language of choice is Ruby. He is currently a Software Engineer @ Intelllex building the server side of things. You can find more about him at avinoth.com.

authenticationGlennGJWTRuby on Rails
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week