Main page background image

Ruby on Rails token-based authentication system with React.js frontend, PART I


Ihor T.
RoR Developer

In this article, I want to show how we can create a simple authentication system for an application that only uses the Rails API. In addition, we will add an external application written in React.js to show you how it all can work together.

We’ll use the knock gem so we don’t have to create everything ourselves. But to be honest, a JWT-based system is relatively easy to build, even from scratch. Let me know in the comments if you want me to add this topic to future articles.

The code from the article can be found here.

Step 0. What Is JWT?

JWT is a JSON web token that is used for authorization. The server encodes and serializes the token and signs it with its private key. The user information is already included in this token, meaning the server does not need to store it in the database. The server only needs to deserialize the token to access the user information with its private key.

So the overall flow of JWT tokens is quite simple:

  1. First, the client sends the server an email and password.
  2. The server verifies the email address and password and, if correct, generates a JWT token and sends it back to the client.
  3. Now, the client can request a private resource from the server using this token. By including the JWT token in the headers, the client requests the server for the private resource.
  4. The server then gets the token from the headers, reads and deserializes it, and gets the user’s information. Finally, the server decides if that user can access a particular resource and sends back an appropriate response.

Step 1. Create a new rails application.

First, let’s create a new Ruby on Rails application. We will use Rails in API mode, Ruby version 2.6.3, and the Postgres database.

rvm use 2.6.3
rails new rails_jwt_auth --api -T -d postgresql
cd ./rails_jwt_auth

Step 2. Add dependencies to the Gemfile.

We need a knock gem that manages all JWT generation functions.

And the rack-cors allow us to specify from where we can and cannot make requests to our server from.

Open Gemfile and paste:

# Gemfile
gem 'rack-cors'
gem 'knock'

And install them:

bundle

Step 3. Add User table.

We need to create a user table to store our users, allowing them to authenticate.

You can use any table name you want. Ensure it has a password_digest column, so passwords are appropriately managed.

And if you are using a table name other than a User, you may need to apply additional configuration for the Knock gem.

rails g model User email:string password_digest:string

rails db:create
rails db:migrate

For the User table to work with passwords, we need to add the following line toapp/models/user.rb

# app/models/user.rb
class User < ApplicationRecord
  has_secure_password
end

With the User table created, we can add some test users.

Open the rails console.

rails c

Then

User.create(email: 'test1@test.com', password: 'test1@test.com')

Step 4. Set up the knock gem.

Before we start, let’s add the following two lines to config/application.rb

# config/application.rb
module RailsJwtAuth
  class Application < Rails::Application
    # don't forget to comment the line below
    # config.load_defaults 6.1
    config.load_defaults 6.0
    config.autoloader = :classic
    ...
  end
end

I don’t want to go into too much detail, but this fixes a bug caused by the Rails 6 autoloader known as Zeitwerk. The error may have been fixed by the knock gem at the time of this writing. So make sure you follow it.

Now we can start setting up the knock.

rails g knock:install
# creates config/initializers/knock.rb

Open config/initializers/knock.rb and paste:

# config/initializers/knock.rb
Knock.setup do |config|
  ## Expiration claim
  config.token_lifetime = 1.day

  ## Signature algorithm
  config.token_signature_algorithm = 'HS256'

  ## Signature key
  config.token_secret_signature_key = -> { Rails.application.credentials.secret_key_base }

  ## Exception Class
  config.not_found_exception_class_name = 'ActiveRecord::RecordNotFound'
end

The next step is adding a controller so the client can request a token. Create the following file app/controllers/user_token_controller.rb and paste:

# app/controllers/user_token_controller.rb
class UserTokenController < Knock::AuthTokenController
  skip_before_action :verify_authenticity_token, raise: false
end

And finally, add the route to the config/routes.rb

Rails.application.routes.draw do
  post :user_token, controller: :user_token, action: :create
end

Step 5. Set up CORS

Cross-origin requests will be blocked if the client domain or port differs from the server domain or port. This is purely a browser feature, so if we want to make requests from our future React.js application, we need to set up CORS.

NOTE: We can make requests from postman or similar tools even without CORS configured just because they are not browsers, it means we can do whatever we want, and there is no such security feature.

For simplicity, let’s allow requests from any origin to any resource.

Open config/initializers/cors.rb and paste:

# config/initializers/cors.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins '*'
    resource '*', headers: :any, methods: %i[get post options]
  end
end

Step 6. Test the JWT token through postman.

Starting the rails server:

rails s

Now we can request a JWT token.

POST http://localhost:3000/user_token
{
 "auth": {
  "email": "test1@test.com",
  "password": "test1@test.com"
  }
}

In response, you should see:

{
    "jwt": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2NzIyNDU2NTksInN1YiI6MX0.cJm9cNbR9fWd4-79oxdqLSWXvQ71NQz_Z9Cx5QQ1648"
}

Step 7. Add a private API endpoint.

Open app/controllers/application_controller.rb and paste:

# app/controllers/application_controller.rb
class ApplicationController < ActionController::API
  include Knock::Authenticable

  # With the below line, we have current_user available in all actions
  before_action :authenticate_user

  # Since this is an API-only application, we want to skip the CSRF protection.
  skip_before_action :verify_authenticity_token, raise: false
end

Now let’s create the most straightforward API endpoint.

Create the file app/controllers/heart_controller.rb and paste:

# app/controllers/heart_controller.rb
class HeartController < ApplicationController
  def beat
    render json: 'yes', status: 200
  end
end

Open config/routes.rb and paste:

# config/routes.rb
Rails.application.routes.draw do
  get 'heart/beat', controller: :heart, action: :beat
  # rest of the routes ...
end

Step 8. Test the private API through postman.

Starting the rails server:

rails s

Now we can request a private endpoint.

GET http://localhost:3000/heart/beat
HEADERS = {
  Authorization: 'your_token_here'
}

In response, you should see:

yes

If you don’t put the JWT in the “Authorization” header, you will see “401 Unauthorized”.

Step 9. Create a new React js app.

I moved this part to a separate article. You can find it here - “Ruby on Rails token-based authentication system with React.js frontend, PART II”

The code for the Rails part of the article can be found here.