Api Rate Abuse in Rails with Openid Connect
Api Rate Abuse in Rails with Openid Connect — how this specific combination creates or exposes the vulnerability
Rate abuse in a Ruby on Rails API that uses OpenID Connect (OIDC) typically occurs when protection mechanisms are applied inconsistently across authentication boundaries. OIDC introduces its own tokens—ID tokens and access tokens—carrying identity and authorization claims that Rails must validate before enforcing rate limits.
Consider a scenario where an endpoint accepts an OIDC access token and maps current_user from the token payload. If rate limiting is applied only after token validation, an attacker can make many unauthenticated or minimally authenticated requests to the token endpoint or to public endpoints that perform token introspection. Each request consumes token validation resources and can lead to token replay or substitution attempts before rate limits kick in.
In Rails, common misconfigurations include placing before_action :authenticate_user! after rate-limit logic or applying rate limits at the controller level without considering the identity derived from OIDC claims. For example, if different scopes or roles (e.g., admin vs. user) map to the same rate limit bucket, privilege escalation becomes feasible via token substitution.
Another vector involves the userinfo endpoint or custom claims used for authorization. An attacker can exploit endpoints that rely on claims that haven’t been strictly validated for freshness or binding to the token issuer. Without binding rate limits to the authenticated subject (sub) and issuer (iss), tokens from different users can share the same rate quota, effectively bypassing intended throttling.
Operational concerns also arise when token introspection or JWKS fetching is performed on each request. Without proper caching and rate limiting on the introspection path itself, an attacker can amplify load on the authorization server or on Rails’ OIDC validation logic, leading to denial-of-service conditions.
Real-world patterns mirror findings seen in frameworks like OWASP API Top 10 2023’s Broken Object Level Authorization (BOLA) and excessive resource consumption. CVE examples often involve missing binding between token identity and rate-limit identifiers, allowing attackers to exhaust quotas under a single identity or across shared namespaces.
Openid Connect-Specific Remediation in Rails — concrete code fixes
To remediate rate abuse in Rails with OIDC, tie rate limits to the authenticated identity derived from the token and enforce limits at the earliest point in the request cycle. Use a stable subject derived from the validated OIDC claims.
Example OIDC configuration with the omniauth-openid-connect gem:
# config/initializers/omniauth.rb
Rails.application.config.middleware.use OmniAuth::Builder do
provider :openid_connect,
name: :oidc,
identifier: ENV['OIDC_CLIENT_ID'],
secret: ENV['OIDC_CLIENT_SECRET'],
discovery: true,
issuer: 'https://auth.example.com/',
redirect_uri: '/auth/oidc/callback',
scope: 'openid email profile offline_access',
response_type: 'code',
prompt: 'consent',
client_options: {
ssl: { verify_mode: OpenSSL::SSL::VERIFY_PEER }
},
id_token: true,
access_token: true
end
After a successful callback, derive a stable subject for rate limiting:
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
before_action :set_oidc_identity
before_action :enforce_rate_limit, :only => [:api_action]
private
def set_oidc_identity
return unless current_user_from_oidc
# Use subject and issuer to uniquely identify the token holder
@rate_limit_key = "oidc:#{current_user_from_oidc[:sub]}:#{current_user_from_oidc[:iss]"}
end
def current_user_from_oidc
return @current_user_from_oidc if defined?(@current_user_from_oidc)
# Assuming you store the decoded token in the session or request env
@current_user_from_oidc = request.env['omniauth.auth']&.info&.to_hash || {}
end
Apply rate limiting using a store that supports the composite key, for example with Redis and the rack-attack gem:
# config/initializers/rack_attack.rb
class Rack::Attack
throttle('oidc/requests', limit: 60, period: 60) do |req|
if req.env['omniauth.auth']&.info
"oidc:#{req.env['omniauth.auth'][:info][:sub']}:#{req.env['omniauth.auth'][:info][:iss]}"
end
end
throttle('oidc/introspect', limit: 30, period: 60) do |req|
"introspect:#{req.env['HTTP_AUTHORIZATION']&slice(/Bearer\s(\w+)/, 1)"}" if req.path.match?(/introspect|token/) && req.post?
end
end
For endpoints that rely on claims for authorization, validate the token’s scope and roles before applying business logic:
# app/controllers/api/v1/admin_controller.rb
class Api::V1::AdminController < ApplicationController
before_action :require_scope!
def require_scope!
token_scopes = request.env['omniauth.auth']&.extra&.raw_info&.scopes || []
unless token_scopes.include?('admin')
render json: { error: 'insufficient_scope' }, status: :forbidden
end
end
end
Additionally, apply rate limits on the introspection and token validation paths separately to prevent amplification against the authorization server. Cache validated tokens and their claims for the period defined by exp, and ensure that the nonce claim is validated for ID tokens to mitigate replay.
These steps align with guidance from frameworks such as OWASP and address patterns found in real advisories by ensuring that rate limits are bound to a unique and trustworthy identity derived from OIDC validation, rather than IP or unauthenticated endpoints alone.