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:
- First, the client sends the server an email and password.
- The server verifies the email address and password and, if correct, generates a JWT token and sends it back to the client.
- 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.
- 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.