Skip to main content

Ruby (Rails / Sinatra)

Setup

# Gemfile
gem 'jwt'
gem 'faraday'

Rails example

# app/controllers/application_controller.rb
class ApplicationController < ActionController::API
AUTH_URL = ENV.fetch('AUTH_URL', 'http://auth:4000')

before_action :authenticate!

private

def authenticate!
token = cookies[:identsphere_at] || request.headers['Authorization']&.sub(/^Bearer /, '')
return render json: { error: 'unauthenticated' }, status: 401 unless token

jwks_response = Rails.cache.fetch('identsphere_jwks', expires_in: 5.minutes) do
Faraday.get("#{AUTH_URL}/.well-known/jwks.json").body
end
jwks = JSON.parse(jwks_response)
decoded, _header = JWT.decode(token, nil, true, algorithms: ['RS256'], jwks: { keys: jwks['keys'] })
@current_user = decoded
rescue JWT::DecodeError
render json: { error: 'invalid token' }, status: 401
end
end

Proxy login through Rails

# app/controllers/auth_controller.rb
class AuthController < ApplicationController
skip_before_action :authenticate!, only: [:login, :register]

def login
res = Faraday.post("#{AUTH_URL}/v1/auth/login") do |req|
req.headers['Content-Type'] = 'application/json'
req.body = request.raw_post
end

# forward Set-Cookie headers
res.headers.each do |k, v|
response.set_header('Set-Cookie', v) if k.downcase == 'set-cookie'
end
render plain: res.body, status: res.status
end
end

Sinatra

Same idea, leaner:

require 'sinatra'
require 'jwt'
require 'faraday'

AUTH_URL = ENV.fetch('AUTH_URL')

before do
next if request.path == '/auth/login'
token = request.cookies['identsphere_at']
halt 401 unless token
jwks_doc = JSON.parse(Faraday.get("#{AUTH_URL}/.well-known/jwks.json").body)
payload, _ = JWT.decode(token, nil, true, algorithms: ['RS256'], jwks: { keys: jwks_doc['keys'] })
@user = payload
rescue JWT::DecodeError
halt 401
end

get '/api/me' do
json user_id: @user['sub'], org_id: @user['org']
end

Verify tokens in your backend

This is Path 2 — per-request authorization: the RS256 access token is validated locally against the cached JWKS, no network hop per request. Use the jwt gem. Fetch and cache the JWKS, then JWT.decode with jwks: and algorithms: ['RS256']; pass iss: + verify_iss: true to check the issuer. Expiration (exp) is verified by default.

# Gemfile: gem 'jwt'; gem 'faraday'
require 'jwt'
require 'faraday'
require 'json'

IDENTSPHERE_URL = ENV.fetch('IDENTSPHERE_URL')
ISSUER = ENV.fetch('IDENTSPHERE_ISSUER', 'IdentSphere')
AUDIENCE = ENV['IDENTSPHERE_TOKEN_AUDIENCE'] # nil unless configured

def verify_token(token)
jwks_raw = Rails.cache.fetch('identsphere_jwks', expires_in: 5.minutes) do
Faraday.get("#{IDENTSPHERE_URL}/.well-known/jwks.json").body
end
jwks = JWT::JWK::Set.new(JSON.parse(jwks_raw))
jwks.filter! { |key| key[:use] == 'sig' } # signing keys only

options = {
algorithms: ['RS256'], # never accept 'none' / HS256
jwks: jwks,
iss: ISSUER,
verify_iss: true, # check the issuer
verify_expiration: true, # reject expired tokens (default true)
}
if AUDIENCE
options[:aud] = AUDIENCE
options[:verify_aud] = true
end

payload, _header = JWT.decode(token, nil, true, options)
payload # { 'sub' => ..., 'org' => ..., 'email' => ..., 'aal' => ..., ... }
end

Wire it into the before_action (or Sinatra before block) shown above, replacing the inline JWT.decode call:

def authenticate!
token = cookies[:identsphere_at] ||
request.headers['Authorization']&.sub(/^Bearer /, '')
return render(json: { error: 'unauthenticated' }, status: 401) unless token

@current_user = verify_token(token)
rescue JWT::DecodeError # covers JWT::ExpiredSignature, JWT::InvalidIssuerError, etc.
render json: { error: 'invalid token' }, status: 401
end

JWT::ExpiredSignature, JWT::InvalidIssuerError, and JWT::InvalidAudError are all subclasses of JWT::DecodeError, so the single rescue fails closed.

Caveats

Same as Pattern 2 elsewhere: revocation lag ~ JWKS cache TTL + access-token TTL.