menu
What is a JSON Web Token?

A JSON Web Token is an internet standard defined by the Internet Engineering Task Force (IETF) as a: "compact, URL-safe means of representing claims to be transferred between two parties" so, go ahead with the configuration.

Build the Rails app:
rails new jwt_rails_api --api


Add the gems:
gem 'jwt', '~> 2.7'
gem "bcrypt", "~> 3.1.7"

Generate User and Product Models
rails g model User username:string password:string
rails g model Product name:string description:text

After running our migration with rails db:migrate, our setup with models is complete, and our schema, found in db/schema.rb, should now look similar to this:

ActiveRecord::Schema[7.0].define(version: 2024_03_26_224534) do
  create_table "products", force: :cascade do |t|
    t.string "name"
    t.text "description"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end

  create_table "users", force: :cascade do |t|
    t.string "username"
    t.string "password_digest"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end
end


Build a jwt Gem Wrapper

Our wrapper class is found in app/lib/json_web_token.rb and looks like this:

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

At this point, you can already test this class in your Rails console:

data = {"name"=>"Juanequex"}

JsonWebToken.encode(data)
# => "eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiQXBwU2lnbmFsIiwiZXhwIjoxNjg1NDI0MjI5fQ.zWJyFHH8Pa6phBOU99XgtRntyfZQSOTX4TdwOxFY9gY"

JsonWebToken.decode(JsonWebToken.encode(data))
# => {"name"=>"Juanequex", "exp"=>1685424262}


Back to the User Model
Now will be a good time to visit our User model at app/models/user.rb. All we need to do here is add the has_secure_password class method:

class User < ApplicationRecord
  has_secure_password
end

Creating a Sample User and Product in Rails is possible through the Rails console or configuring your seeds.
User.create(username: "juanequex", password: "password")
Product.create(name: "Rad Ruby", description: "A book collection of Ruby tips")



Using JWTs in Rails Controllers

 A good place to start is the ApplicationController 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
    authorization_header = request.headers['Authorization']
    token = authorization_header.split(" ").last if authorization_header
    decoded_token = JsonWebToken.decode(token)

    User.find(decoded_token[:user_id])
  end

  def invalid_token
    render json: { invalid_token: 'invalid token' }
  end

  def decode_error
    render json: { decode_error: 'decode error' }
  end
end

Next, we need an AuthenticationController to which users can send requests and get a signed JSON Web Token from our server. This controller should be placed at app/controllers/authentication_controller.rb and may look like this:

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


Testing Our Ruby Application with a Protected Resource
Let's create a controller with rails g controller Product index so we have something like:
class ProductsController < ApplicationController
  before_action :authenticate

  def index
    @products = Product.all

    render json: @products
  end
end

Of course, we need a route to access these controllers. Our config/routes.rb should look something like:
Rails.application.routes.draw do
  post 'login', to: "authentication#login"
  get 'products', to: "products#index"
end


Let's try getting a JWT with a user that doesn't exist:
curl -H "Content-Type: application/json" -X POST -d '{"username":"manny","password":"password"}' http://localhost:3000/login


We should get the following response:
{"error":"unauthorized"}


Now try the same with a user that we created earlier:
curl -H "Content-Type: application/json" -X POST -d '{"username":"juanequex","password":"password"}' http://localhost:3000/login


This should give us a signed JSON web token that could look like this:
{"token":"eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE2ODU0NTEyMTR9.1UEYAbmFOSF93yp9pJqNEzkdHr3rVqutPNZWRIPDYkY","expires_at":1685432077}

Let's keep this token for a second and try accessing a product resource with a bad token (I changed a random character in the token):
curl -H "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE2ODU0NTEyMTR9.1UEYAbmFOSF93yp9pJqNEzkdHr3rVqutPNZWRIPZYkY" http://localhost:3000/products

output:
{"decode_error":"decode error"}



However, if we make the same request to access the product resource with the valid token we got from the server previously, We're granted access to the product resource:

[{"id":1,"name":"Rad Ruby","description":"A book collection of Ruby tips","created_at":"2024-03-26T19:33:30.826Z","updated_at":"2024-03-26T19:33:30.826Z"}]

That's it! We've successfully secured our Ruby application with a JSON web token!