Skip to content

Policies

A policy is a list of named checks run against a subject. The names are the documentation: reading a policy tells you what the request has to satisfy, without reading a method body.

class AuthorizationPolicy < Policy
uses ClientPolicy
checks :response_type_is_supported,
:client_may_use_the_code_grant,
:pkce_is_present_when_required,
:challenge_method_is_supported,
:scopes_are_permitted,
:resources_are_absolute
end
  • uses runs another policy first, in full. AuthorizationPolicy cannot check scopes against a client it has not yet established exists, so ClientPolicy runs ahead of it.
  • checks names methods on this policy. They run in declaration order.

Both inherit, so a subclass gets its parent’s checks and can add to them.

def pkce_is_present_when_required
if client.public? && code_challenge.blank?
deny!("invalid_request",
"a public client must send code_challenge",
redirectable: true)
end
end

An OAuth error code, and whether the failure is safe to hand back to the client’s redirect_uri.

That second flag is the load-bearing one. invalid_client and an unregistered redirect_uri are not redirectable — if they were, an attacker could point redirect_uri anywhere and use the error response itself as an open redirector. Everything after the client and its redirect URI have been established is redirectable, because by then the destination is one the client registered.

rescue Policy::Denied => denial
if denial.redirectable && authorization.client&.redirect_uri?(authorization.redirect_uri)
redirect_to authorization.redirect_with(issuer:, error: denial.error, …)
else
render :error, status: denial.status
end

The non-redirectable branch renders a page rather than a JSON body, because a refusal with nowhere safe to send it is one a person is looking at. A client that asks for JSON still gets it.

Subclass Policy, delegate what you need from the subject, and keep each check to one assertion.

class ClientPolicy < Policy
checks :client_is_known, :redirect_uri_is_registered
delegate :client, :redirect_uri, to: :subject
private
def client_is_known
deny!("invalid_client", "no client is registered with that client_id") if client.nil?
end
def redirect_uri_is_registered
if redirect_uri.blank?
deny!("invalid_request", "redirect_uri is required")
elsif !client.redirect_uri?(redirect_uri)
deny!("invalid_request", "redirect_uri is not registered for this client")
end
end
end

Then run it: ClientPolicy.new(authorization).call.