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!