verikloak-rails
Rails integration for Verikloak.
Purpose
Provide drop-in, token-based authentication for Rails APIs via Verikloak (OIDC discovery and JWKS verification). It installs middleware and a controller concern to authenticate Bearer tokens, exposes helpers for claims and subject, and returns standardized JSON error responses (401/403/503) with WWW-Authenticate
on 401. Defaults prioritize security while keeping configuration minimal.
Features
- Auto-wiring via Railtie (
config.verikloak.*
) - Controller concern with
before_action :authenticate_user!
- Helpers:
current_user_claims
,current_subject
,current_token
,authenticated?
- Exceptions → standardized JSON (401/403/503) with
WWW-Authenticate
on 401 - Log tagging (
request_id
,sub
) - Installer generator:
rails g verikloak:install
Compatibility
- Ruby: >= 3.1
- Rails: 6.1 – 8.x
- verikloak: >= 0.2.0, < 1.0.0
Quick Start
bundle add verikloak verikloak-rails
rails g verikloak:install
Then configure config/initializers/verikloak.rb
.
Controller Helpers
Available Methods
Method | Purpose | Returns | On failure |
---|---|---|---|
authenticate_user! |
Use as a before_action to require a valid Bearer token |
void |
Renders standardized 401 JSON and sets WWW-Authenticate: Bearer when token is absent/invalid |
authenticated? |
Whether verified user claims are present | Boolean |
— |
current_user_claims |
Verified JWT claims (string keys) | Hash or nil |
— |
current_subject |
Convenience accessor for sub claim |
String or nil |
— |
current_token |
Raw Bearer token from the request | String or nil |
— |
with_required_audience!(*aud) |
Enforce that aud includes all required entries |
void |
Raises Verikloak::Error('forbidden') so the concern renders standardized 403 JSON and halts the action |
Data Sources
Value | Rack env keys | Fallback (RequestStore) | Notes |
---|---|---|---|
current_user_claims |
verikloak.user |
:verikloak_user |
Uses RequestStore only when available |
current_token |
verikloak.token |
:verikloak_token |
Uses RequestStore only when available |
Example Controller
class ApiController < ApplicationController
# Auto-included by default; if disabled, add explicitly:
# include Verikloak::Rails::Controller
def me
render json: { sub: current_subject, claims: current_user_claims }
end
def must_have_aud
with_required_audience!('my-api')
render json: { ok: true }
end
end
Manual Include (disable auto-include)
If you disable auto-inclusion of the controller concern, add it manually:
# config/initializers/verikloak.rb
Rails.application.configure do
config.verikloak.auto_include_controller = false
end
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
include Verikloak::Rails::Controller
end
Middleware
Inserted Middleware
Component | Inserted relative to | Purpose |
---|---|---|
Verikloak::Bff::HeaderGuard (optional) |
Before Verikloak::Middleware by default when the gem is present |
Normalize or enforce trusted proxy headers such as X-Forwarded-Access-Token |
Verikloak::Middleware |
After Rails::Rack::Logger by default (configurable) |
Validate Bearer JWT (OIDC discovery + JWKS), set verikloak.user /verikloak.token , and honor skip_paths |
BFF Integration
Support for BFF header handling (e.g., normalizing or enforcing X-Forwarded-Access-Token
) now lives in a dedicated gem: verikloak-bff.
Note: verikloak-bff's HeaderGuard
never overwrites an existing Authorization
header.
- Gem: https://github.com/taiyaky/verikloak-bff
- Rails guide:
docs/rails.md
in that repository
When verikloak-bff
is on the load path, verikloak-rails
automatically inserts Verikloak::Bff::HeaderGuard
before the base middleware so forwarded headers are normalized before verification. Control this via config.verikloak.auto_insert_bff_header_guard
and the bff_header_guard_insert_before/after
knobs.
Use verikloak-bff alongside this gem when you front Rails with a BFF/proxy such as oauth2-proxy and need to enforce trusted forwarding and header consistency.
Configuration (initializer)
Keys
Keys under config.verikloak
:
Key | Type | Description | Default |
---|---|---|---|
discovery_url |
String | OIDC discovery URL | nil |
audience |
String or Array | Expected aud |
'rails-api' |
issuer |
String | Expected iss |
nil |
leeway |
Integer | Clock skew allowance (seconds) | 60 |
skip_paths |
Array |
Paths to skip verification | ['/up','/health','/rails/health'] |
logger_tags |
Array |
Tags to add to Rails logs. Supports :request_id , :sub |
[:request_id, :sub] |
error_renderer |
Object responding to render(controller, error) |
Override error rendering | built-in JSON renderer |
auto_include_controller |
Boolean | Auto-include controller concern | true |
render_500_json |
Boolean | Rescue StandardError , log the exception, and render JSON 500 |
false |
rescue_pundit |
Boolean | Rescue Pundit::NotAuthorizedError to 403 JSON when Pundit is present(auto-disabled when verikloak-pundit is loaded and the initializer leaves it unset) |
true |
middleware_insert_before |
Object/String/Symbol | Insert Verikloak::Middleware before this Rack middleware |
nil |
middleware_insert_after |
Object/String/Symbol | Insert Verikloak::Middleware after this Rack middleware (Rails::Rack::Logger when nil ) |
nil |
auto_insert_bff_header_guard |
Boolean | Auto insert Verikloak::Bff::HeaderGuard when the gem is present |
true |
bff_header_guard_insert_before |
Object/String/Symbol | Insert the header guard before this middleware (Verikloak::Middleware when nil ) |
nil |
bff_header_guard_insert_after |
Object/String/Symbol | Insert the header guard after this middleware | nil |
Environment variable examples are in the generated initializer.
Minimum Setup
- Required: set
discovery_url
to your provider’s OIDC discovery document URL. - Recommended: set
audience
(expectedaud
), andissuer
when known.
# config/initializers/verikloak.rb
Rails.application.configure do
config.verikloak.discovery_url = ENV['KEYCLOAK_DISCOVERY_URL']
config.verikloak.audience = ENV.fetch('VERIKLOAK_AUDIENCE', 'rails-api')
# Optional but recommended when you know it
# config.verikloak.issuer = 'https://idp.example.com/realms/myrealm'
# For BFF/proxy header handling, see verikloak-bff (auto inserted when present)
# To customize ordering:
# config.verikloak.middleware_insert_before = Rack::Attack
# config.verikloak.auto_insert_bff_header_guard = false
end
Notes:
- For array-like values (
audience
,skip_paths
), prefer defining Ruby arrays in the initializer. If passing via ENV, use comma-separated strings and parse in the initializer.
Full Example (selected options)
# config/initializers/verikloak.rb
Rails.application.configure do
config.verikloak.discovery_url = ENV['KEYCLOAK_DISCOVERY_URL']
config.verikloak.audience = ENV.fetch('VERIKLOAK_AUDIENCE', 'rails-api')
config.verikloak.leeway = Integer(ENV.fetch('VERIKLOAK_LEEWAY', '60'))
config.verikloak.skip_paths = %w[/up /health /rails/health]
config.verikloak. = %i[request_id sub]
config.verikloak.render_500_json = ENV.fetch('VERIKLOAK_RENDER_500', 'false') == 'true'
# Optional Pundit rescue (403 JSON). Leave commented if you use
# verikloak-pundit so it can disable the built-in handler automatically.
# config.verikloak.rescue_pundit = ENV.fetch('VERIKLOAK_RESCUE_PUNDIT', 'true') == 'true'
end
ENV Mapping
Key | ENV var |
---|---|
discovery_url |
KEYCLOAK_DISCOVERY_URL |
audience |
VERIKLOAK_AUDIENCE |
issuer |
VERIKLOAK_ISSUER |
leeway |
VERIKLOAK_LEEWAY |
render_500_json |
VERIKLOAK_RENDER_500 |
rescue_pundit |
VERIKLOAK_RESCUE_PUNDIT |
Errors
This gem standardizes JSON error responses and HTTP statuses. See ERRORS.md for details and examples.
Statuses
Status | Typical code(s) | When | Headers | Body (example) |
---|---|---|---|---|
401 Unauthorized | invalid_token , unauthorized |
Missing/invalid Bearer token; failed signature/expiry/issuer/audience checks | WWW-Authenticate: Bearer with optional error and error_description |
{ "error": "invalid_token", "message": "token expired" } |
403 Forbidden | forbidden |
Audience check failure via with_required_audience! ; optionally Pundit::NotAuthorizedError when rescue is enabled |
— | { "error": "forbidden", "message": "Required audience not satisfied" } |
503 Service Unavailable | jwks_fetch_failed , jwks_parse_failed , discovery_metadata_fetch_failed , discovery_metadata_invalid , invalid_discovery_url , discovery_redirect_error |
Upstream metadata/JWKS issues | — | { "error": "jwks_fetch_failed", "message": "..." } |
Customize
Customize rendering by assigning config.verikloak.error_renderer
.
Example: return a compact JSON shape while preserving WWW-Authenticate
for 401.
class CompactErrorRenderer
def render(controller, error)
code = error.respond_to?(:code) ? error.code : 'unauthorized'
= error..to_s
status = case code
when 'forbidden' then 403
when 'jwks_fetch_failed', 'jwks_parse_failed', 'discovery_metadata_fetch_failed', 'discovery_metadata_invalid' then 503
else 401
end
if status == 401
hdr = +'Bearer'
hdr << %( error="#{sanitize_quoted(code)}") if code
hdr << %( error_description="#{sanitize_quoted()}") if && !.empty?
controller.response.set_header('WWW-Authenticate', hdr)
end
controller.render json: { code: code, msg: }, status: status
end
end
Rails.application.configure do
config.verikloak.error_renderer = CompactErrorRenderer.new
end
Note: Always sanitize values placed into WWW-Authenticate
header parameters to avoid header injection. For example:
class CompactErrorRenderer
private
def sanitize_quoted(val)
# Escape quotes/backslashes and strip CR/LF (collapse runs to a single space)
val.to_s.gsub(/(["\\])/) { |m| "\\#{m}" }.gsub(/[\r\n]+/, ' ')
end
end
Optional Pundit Rescue
If the pundit
gem is present, Pundit::NotAuthorizedError
is rescued to a standardized 403 JSON. This is a lightweight convenience only; deeper Pundit integration (policies, helpers) is out of scope and can live in a separate plugin.
When the optional verikloak-pundit
gem is loaded, the built-in rescue is automatically disabled to avoid double-handling errors—as long as the initializer leaves config.verikloak.rescue_pundit
unset. Uncomment the initializer line (or set the value elsewhere) if you prefer different behavior.
Toggle
Toggle with config.verikloak.rescue_pundit
(default: true; leave unset to allow verikloak-pundit
to disable it). Environment example:
# config/initializers/verikloak.rb
Rails.application.configure do
# Disable the built-in rescue if you handle Pundit errors yourself
config.verikloak.rescue_pundit = ENV.fetch('VERIKLOAK_RESCUE_PUNDIT', 'true') == 'true'
end
Behavior Example
# When Pundit raises:
raise Pundit::NotAuthorizedError, 'forbidden'
# The concern rescues and renders:
# { error: 'forbidden', message: 'forbidden' } with status 403
Rails 8.0/8.1 Timezone Note
Rails 8.0 shows a deprecation for the upcoming 8.1 change where to_time
preserves the receiver timezone. This gem does not call to_time
, but your app may. To opt in and silence the deprecation, set:
# config/application.rb
module YourApp
class Application < Rails::Application
config.active_support.to_time_preserves_timezone = :zone
end
end
Development (for contributors)
Clone and install dependencies:
git clone https://github.com/taiyaky/verikloak-rails.git
cd verikloak-rails
bundle install
See Testing below to run specs and RuboCop. For releasing, see Publishing.
Testing
All pull requests and pushes are automatically tested with RSpec and RuboCop via GitHub Actions. See the CI badge at the top for current build status.
To run the test suite locally:
docker compose run --rm dev rspec
docker compose run --rm dev rubocop -a
Contributing
Bug reports and pull requests are welcome! Please see CONTRIBUTING.md for details.
Security
If you find a security vulnerability, please follow the instructions in SECURITY.md.
License
This project is licensed under the MIT License.
Publishing (for maintainers)
Gem release instructions are documented separately in MAINTAINERS.md.
Changelog
See CHANGELOG.md for release history.
References
- verikloak-rails (this gem): https://rubygems.org/gems/verikloak-rails
- verikloak-bff: https://rubygems.org/gems/verikloak-bff
- Verikloak (base gem): https://github.com/taiyaky/verikloak
- Verikloak on RubyGems: https://rubygems.org/gems/verikloak