Broken Access Control in Rails with Jwt Tokens
Broken Access Control in Rails with Jwt Tokens
Broken Access Control occurs when authorization checks are missing, incomplete, or bypassed, allowing a user to access functionality or data that should be restricted. In a Ruby on Rails application using JWT tokens for authentication, the separation between authentication (token validation) and authorization (role- or permission-based checks) can create subtle vulnerabilities if not explicitly enforced on every request.
JWT tokens typically carry a payload that may include a user identifier, roles, or scopes. Rails does not automatically enforce authorization based on this payload; it only provides the decoded claims when you add a before_action or a custom method to parse and verify the token. If authorization is omitted or applied inconsistently—such as only on some controllers, only for certain HTTP verbs, or only when the token is present—an attacker can manipulate access by using a valid token that lacks the required privileges.
Consider a scenario where an endpoint like DELETE /api/admin/users/:id lacks an explicit authorization check but relies on the presence of a JWT. If the token belongs to a standard user, the request may still succeed because Rails routes to the controller action and executes it, provided the token signature is valid and no role check is performed. This is a classic Broken Access Control (BOLA/IDOR) pattern: Insecure Direct Object Reference or lack of ownership/role checks on server-side resources. Attackers can test endpoints by swapping identifiers or escalating privileges if authorization is not enforced uniformly.
Another common pitfall is over-reliance on JWT scopes or roles that are embedded in the token but not validated on each request. A token issued with an admin role may be downgraded in the client, but if the server trusts the token without re-checking role-based permissions at the controller or service level, the application behaves as though the user retained admin rights. This becomes critical when endpoints expose sensitive data or perform state changes without verifying that the authenticated subject is explicitly permitted for that action.
Middleware or route constraints can also contribute to inconsistent enforcement. For example, applying authentication via a before_action in ApplicationController but skipping it in some controllers or actions can create inadvertent access paths. Even when using a library like knock or a custom JWT verifier, authorization must be explicit: verify roles or permissions from the token claims against required attributes for the resource and operation, and ensure this check runs for every request, including those that reference other users’ resources by ID.
Jwt Tokens-Specific Remediation in Rails
Remediation centers on ensuring that every controller action that accesses or modifies data performs explicit authorization checks based on the decoded JWT claims, not merely on the presence of a token. Below are concrete patterns and code examples for robust JWT-based access control in Rails.
1. Centralize JWT decoding and authorization in a base controller, and enforce it via a before_action. Decode the token once, extract roles/scopes, and expose a helper method for authorization checks.
class ApiController < ApplicationController
before_action :authenticate_user!
before_action :authorize_admin!, only: [:destroy, :update]
private
def authenticate_user!
token = request.headers['Authorization']&.split(' ')&.last
raise Errors::Unauthorized unless token
begin
decoded = JWT.decode(token, Rails.application.secrets.secret_key_base, true, { algorithm: 'HS256' })
@current_payload = decoded.first
rescue JWT::DecodeError, JWT::ExpiredSignature
raise Errors::Unauthorized
end
end
def current_user
@current_user ||= User.find_by(id: @current_payload['user_id'])
end
def authorize_admin!
raise Errors::Forbidden unless @current_payload['roles']&.include?('admin')
end
end
2. Apply per-action or per-resource authorization using a policy object or Pundit. After JWT decoding, check that the subject can perform the action on the specific resource.
class UserPolicy
def initialize(user_payload, record)
@user_payload = user_payload
@record = record
end
def destroy?
@user_payload['roles']&.include?('admin') || @user_payload['user_id'] == @record.id
end
end
class UsersController < ApiController
before_action :set_user, only: [:show, :update, :destroy]
def destroy
policy = UserPolicy.new(@current_payload, @user)
raise Errors::Forbidden unless policy.destroy?
@user.destroy!
head :no_content
end
private
def set_user
@user = User.find(params[:id])
end
end
3. Ensure that sensitive endpoints do not rely solely on route-level or method-level skips. If an action must be public (e.g., user registration), place it in a separate controller that does not include the authentication/authorization before_actions, or explicitly skip them and apply stricter checks on the actions that mutate data.
4. Validate and sanitize input IDs and use strong parameter checks to avoid IDOR regardless of role claims. Even when a user can access a resource, ensure they can only access their own data unless elevated privileges are verified.
class ProfilesController < ApiController
before_action :set_profile, only: [:show, :update]
def show
policy = UserPolicy.new(@current_payload, @profile)
raise Errors::Forbidden unless policy.show?
render json: @profile
end
private
def set_profile
@profile = Profile.where(user_id: @current_payload['user_id']).find(params[:id])
end
end
5. Rotate signing keys and validate token iss/aud claims where applicable to reduce the impact of leaked tokens. Combine short-lived access tokens with refresh token patterns stored server-side, and verify not only the signature but also the validity window and intended audience.
By combining consistent JWT decoding in a base controller with per-action or per-record authorization checks, Rails applications can mitigate Broken Access Control risks even when using token-based authentication.