class

KemalIdentity::ForbiddenError

Inherits KemalIdentity::Error < Exception < Reference < Object

Raised by authorize! when the caller is authenticated and simply not allowed. Mapped to 403.

Deliberately not NotAuthenticatedError: answering a denied action with a 401 tells the caller to log in again, which for somebody who is already logged in is both wrong and a loop. It is also not FreshAuthenticationRequiredError — proving themselves again would change nothing, because the problem is the grant and not the credential.

The message must not name the permission, the tenant, or the reason. Authz::DenialReason exists for the audit log; a response that varies with it tells an attacker whether the tenant they guessed exists and whether they are inside it.

Constructors

new(message : String | Nil = nil, challenge_error : String | Nil = nil)
Source

Instance methods

challenge_error

The RFC 6750 error code this denial may be reported as, or nil for every denial that must not be described to the caller at all.

Why this is not the DenialReason

The handler needs one bit of protocol classification, not the six-way taxonomy the audit trail needs. Carrying Authz::DenialReason here would put NotAMember and TenantMismatch one to_s away from a response header, and the thing that keeps a denial from confirming that a guessed tenant exists would then be a convention rather than a boundary.

So authorize! projects instead: DenialReason::OutOfScope becomes "insufficient_scope", and everything else becomes nil. The handler cannot leak a reason it was never given.

Only OutOfScope maps, because RFC 6750 registers no code for "the account does not hold this permission" — and that is the one distinction worth keeping hidden. insufficient_scope says the presented credential is narrower than the action, which RFC 6750 §3.1 defines as information the resource server tells the client on purpose.

Source