verikloak-pundit
Pundit integration for the Verikloak family. This gem maps Keycloak roles from JWT claims (e.g., realm_access.roles, resource_access[client].roles) into a convenient UserContext that Pundit policies can consume.
- Requires
verikloakat runtime and pairs well withverikloak-railsfor Rails integrations. - Provides a
pundit_userhook so policies can useuser.has_role?(:admin)etc. - Keeps role mapping configurable (project-specific mappings differ).
Features
- UserContext: lightweight wrapper around JWT claims
-
Delegations:
has_role?,in_group?,resource_role?(client, role)helpers for controllers and policies - RoleMapper: optional map from Keycloak roles → domain permissions
-
Controller integration:
pundit_userprovider for Rails controllers -
Generator:
rails g verikloak:pundit:installcreates initializer + policy template (withhas_permission?support for realm roles plus the configured resource scope)
Installation
bundle add verikloak-punditIf you're on Rails:
rails g verikloak:pundit:installThis generates:
config/initializers/verikloak_pundit.rb-
app/policies/application_policy.rb(template if missing; optional)
For error-handling guidance, see ERRORS.md.
Quick Start (Rails)
# app/controllers/application_controller.rb
class ApplicationController < ActionController::API
include Pundit::Authorization
include Verikloak::Pundit::Controller # provides pundit_user
# If you're also using verikloak-rails:
# before_action :authenticate_user!
endYour policy can then do:
class NotePolicy < ApplicationPolicy
def update?
user.has_role?(:admin) || user.resource_role?(:'rails-api', :editor)
end
endWhere user is the UserContext provided by pundit_user.
Configuration
Most settings have sensible defaults and can be auto-configured. You only need to customize what's different for your application.
Environment Variables
| Variable | Description | Default |
|---|---|---|
KEYCLOAK_RESOURCE_CLIENT |
Default resource client ID for resource roles | "rails-api" |
Auto-configuration with verikloak-rails
When used alongside verikloak-rails, the following settings are automatically synchronized:
-
env_claims_key: Inherits fromVerikloak::Rails.config.user_env_keyif you haven't explicitly set it. This ensures both gems read claims from the same Rack env key.
This means in most cases you can use a minimal initializer:
Verikloak::Pundit.configure do |c|
c.role_map = {
admin: :manage_all,
editor: :write_notes
}
endWorking with other Verikloak gems
-
verikloak-bff: When your Rails application sits behind the BFF, the access
token presented to verikloak-pundit typically originates from the BFF
(e.g. via the
x-verikloak-userheader). Make sure your Rack stack stores the decoded claims under the sameenv_claims_key(default:"verikloak.user", which works out of the box withverikloak-bff >= 0.3). If the BFF issues tokens for multiple downstream services, setpermission_resource_clientsto the limited list of clients whose roles should affect Rails-side authorization to avoid accidentally inheriting permissions meant for other services. -
verikloak-audience: Audience services often mint resource roles with a
service-specific prefix (for example,
audience-service:editor). Align yourrole_mapkeys with that naming convention souser.has_permission?resolves correctly. If Audience adds its own client entry insideresource_access, add that client id topermission_resource_clientswhen you need to consume those roles from Rails.
Integrating with Database User Models
Overview
Verikloak::Pundit::UserContext wraps JWT claims for use in Pundit policies. However, real applications often need to access database User models for additional attributes (e.g., user.admin?, user.organization_id).
Custom UserContext Pattern
Create a custom UserContext that holds both JWT claims and a database user reference:
# app/lib/app_user_context.rb
class AppUserContext < Verikloak::Pundit::UserContext
attr_reader :db_user
def initialize(claims, db_user: nil, resource_client: nil, config: nil)
super(claims, resource_client: resource_client, config: config)
@db_user = db_user
end
# Delegate database user methods
delegate :admin?, :organization_id, :active?, to: :db_user, allow_nil: true
# Custom authorization helpers
def owns?(record)
return false unless db_user && record
record.respond_to?(:user_id) && db_user.id == record.user_id
end
def same_organization?(record)
return false unless db_user && record
record.respond_to?(:organization_id) && db_user.organization_id == record.organization_id
end
endController Setup
Override pundit_user in your ApplicationController. This example assumes you are using verikloak-rails, which provides current_user_claims and related helpers:
# app/controllers/application_controller.rb
class ApplicationController < ActionController::API
include Pundit::Authorization
include Verikloak::Pundit::Controller # Provides pundit_user and verikloak_claims
# If using verikloak-rails for JWT verification:
# include Verikloak::Rails::Controller # Provides current_user_claims, current_subject, etc.
def pundit_user
@pundit_user ||= AppUserContext.new(
verikloak_claims, # From Verikloak::Pundit::Controller
db_user: find_or_create_current_user,
config: Verikloak::Pundit.config
)
end
private
def find_or_create_current_user
# Extract subject from claims
sub = verikloak_claims&.dig('sub')
return nil unless sub
User.find_or_create_by(sub: sub) do |user|
user.email = verikloak_claims&.dig('email')
user.name = verikloak_claims&.dig('name')
end
end
endNote: If you are also using
verikloak-rails, you can use itscurrent_user_claimsmethod instead ofverikloak_claims. Both provide access to the JWT claims from the Rack environment.
Policy Example
Now your policies can use both JWT claims and database attributes:
# app/policies/document_policy.rb
class DocumentPolicy < ApplicationPolicy
def show?
# Combine JWT roles with database attributes
user.has_role?(:admin) || user.owns?(record) || user.same_organization?(record)
end
def update?
user.admin? || user.owns?(record) # Uses delegated db_user.admin?
end
def destroy?
user.has_role?(:admin) && user.active? # JWT role + DB attribute
end
endDelegations Module
Overview
Verikloak::Pundit::Delegations provides shortcut methods for role and permission checks in policies.
Usage
Include in your ApplicationPolicy to access helpers directly:
class ApplicationPolicy
include Verikloak::Pundit::Delegations
# Now you can use:
# - has_role?(:admin) instead of user.has_role?(:admin)
# - in_group?(:editors) instead of user.in_group?(:editors)
# - resource_role?(client, role)
# - has_permission?(:manage_all)
endRequirements
- The policy must have a
usermethod that returns aVerikloak::Pundit::UserContext(or subclass) - If
userisnil, delegation methods will raiseNoMethodError
Compatibility with Custom UserContext
Delegations work with any class that inherits from Verikloak::Pundit::UserContext:
# Works with AppUserContext (shown above)
class DocumentPolicy < ApplicationPolicy
include Verikloak::Pundit::Delegations
def update?
has_role?(:admin) || has_permission?(:write_documents)
end
endHandling nil user
For policies that may receive nil users (e.g., public endpoints), you must guard against nil before calling delegation methods:
class PublicDocumentPolicy < ApplicationPolicy
def show?
return true if record.public?
return false unless user # Guard against nil user before using delegations
has_role?(:viewer)
end
endAlternatively, create a helper method in your ApplicationPolicy:
class ApplicationPolicy
include Verikloak::Pundit::Delegations
private
def authenticated?
!user.nil?
end
def safe_has_role?(role)
authenticated? && has_role?(role)
end
endNon-Rails / custom usage
claims = { "sub" => "123", "email" => "a@b", "realm_access" => {"roles" => ["admin"]} }
ctx = Verikloak::Pundit::UserContext.new(claims, resource_client: "rails-api")
ctx.has_role?(:admin) # => true
ctx.resource_role?(:"rails-api", :writer) # depends on resource_access
ctx.has_permission?(:manage_all) # from role_map, realm or resource rolesTesting
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 -aWhen writing specs, call Verikloak::Pundit.reset! in your test teardown to ensure configuration changes do not leak between examples:
RSpec.configure do |config|
config.after { Verikloak::Pundit.reset! }
endContributing
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.
Operational Guidance
- Enabling
permission_role_scope = :all_resourcespulls roles from every Keycloak client inresource_access. Review the granted roles carefully to ensure you are not expanding permissions beyond what the application expects. - Combine
permission_role_scope = :all_resourceswithpermission_resource_clientsto explicitly opt-in the clients that may contribute permissions. Leaving the whitelist blank (the default) reverts to the legacy behavior of trusting every client in the token. - Leaving
expose_helper_method = trueexposesverikloak_claimsto the Rails view layer. If the claims include personal or sensitive data, consider switching it tofalseand pass only the minimum required information through controller-provided helpers.
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 (core): https://github.com/taiyaky/verikloak
- verikloak-rails (Rails integration): https://github.com/taiyaky/verikloak-rails
- verikloak-pundit on RubyGems: https://rubygems.org/gems/verikloak-pundit