Skip to content

Integrating with Ruby on Rails API

This guide explains how to implement JWT authentication in your Ruby on Rails API using AuthAction’s JWKS (JSON Web Key Set) endpoint.

Example Repository: For a complete working example, check out our example repository.

Before you begin, ensure you have:

  1. Ruby 3.2+ and Bundler: Install via rbenv or RVM
  2. AuthAction Account: You’ll need your AuthAction tenant domain and API identifier

Add to your Gemfile:

gem "rails", "~> 7.2"
gem "jwt", "~> 2.9"
gem "dotenv-rails", groups: [:development, :test]

Then run:

Terminal window
bundle install

Create a .env file in your project root:

Terminal window
AUTHACTION_DOMAIN=your-authaction-tenant-domain
AUTHACTION_AUDIENCE=your-authaction-api-identifier
SECRET_KEY_BASE=$(rails secret)

Create lib/jwt_validator.rb:

require "jwt"
require "net/http"
require "json"
class JwtValidator
DOMAIN = ENV["AUTHACTION_DOMAIN"]
AUDIENCE = ENV["AUTHACTION_AUDIENCE"]
ISSUER = "https://#{DOMAIN}"
JWKS_URI = "https://#{DOMAIN}/.well-known/jwks.json"
CACHE_KEY = "authaction_jwks"
CACHE_TTL = 1.hour
def self.verify(token)
payload, _header = JWT.decode(
token, nil, true,
algorithms: ["RS256"],
iss: ISSUER,
verify_iss: true,
aud: AUDIENCE,
verify_aud: true,
jwks: jwks_loader
)
payload
end
def self.jwks_loader
# The jwt gem calls this lambda with kid_not_found: true when the signing
# key is absent from the cached set, enabling a single cache-bust on key rotation.
lambda do |options|
Rails.cache.delete(CACHE_KEY) if options[:kid_not_found]
Rails.cache.fetch(CACHE_KEY, expires_in: CACHE_TTL) do
response = Net::HTTP.get(URI(JWKS_URI))
JSON.parse(response, symbolize_names: true)
end
end
end
end

Create app/controllers/concerns/jwt_authenticatable.rb:

require "jwt_validator"
module JwtAuthenticatable
extend ActiveSupport::Concern
private
def authenticate_request!
token = extract_bearer_token
@current_payload = JwtValidator.verify(token)
rescue JWT::ExpiredSignature
render json: { error: "Token has expired" }, status: :unauthorized
rescue JWT::DecodeError => e
render json: { error: e.message }, status: :unauthorized
end
def extract_bearer_token
header = request.headers["Authorization"]
raise JWT::DecodeError, "Missing token" unless header&.start_with?("Bearer ")
header.split(" ", 2).last.strip
end
end

Include the concern in ApplicationController:

class ApplicationController < ActionController::API
include JwtAuthenticatable
end
class MessagesController < ApplicationController
before_action :authenticate_request!, only: [:protected_message]
def public_message
render json: { message: "This is a public message!" }
end
def protected_message
render json: {
message: "This is a protected message!",
sub: @current_payload["sub"]
}
end
end

In config/routes.rb:

Rails.application.routes.draw do
get "/public", to: "messages#public_message"
get "/protected", to: "messages#protected_message"
end
Terminal window
rails server

The API will be available at http://localhost:3000.

  1. Obtain an Access Token:
Terminal window
curl --request POST \
--url https://your-authaction-tenant-domain/oauth2/m2m/token \
--header 'content-type: application/json' \
--data '{
"client_id": "your-authaction-m2m-app-clientid",
"client_secret": "your-authaction-m2m-app-client-secret",
"audience": "your-authaction-api-identifier",
"grant_type": "client_credentials"
}'
  1. Call the Public Endpoint:
Terminal window
curl http://localhost:3000/public
  1. Call the Protected Endpoint:
Terminal window
curl --request GET \
--url http://localhost:3000/protected \
--header 'Authorization: Bearer YOUR_ACCESS_TOKEN'
  • Verify AUTHACTION_DOMAIN and AUTHACTION_AUDIENCE match your AuthAction dashboard exactly
  • Ensure the token is signed with the RS256 algorithm
  • Verify your application can reach https://your-authaction-tenant-domain/.well-known/jwks.json
  • Ensure the Authorization: Bearer <token> header is present and the token is not expired
  • Confirm the token’s audience matches your API identifier