class

KemalIdentity::Kemal::PathGuard

Inherits Kemal::Handler < Kemal::HandlerInterface < HTTP::Handler < Reference < Object

Requires authentication for a whole path subtree.

It matches the path itself, for every method

Deliberately not only ["/admin/*"]. On Kemal 1.10.0 – 1.12.0 that rule defaults to GET and does not match HEAD, so middleware scoped that way never ran on a HEAD request while the GET handler executed and returned the headers it set. 1.13.0 fixes it; the floor does not have the fix, and matching on the path alone is immune either way.

The same property makes new methods safe by default: HTTP QUERY arrived in Kemal 1.13.0 and this guard covered it without a change, because there was never a list of methods to forget to update.

use KemalIdentity::Kemal::PathGuard.new(prefix: "/admin")
use KemalIdentity::Kemal::PathGuard.new(prefix: "/account", within: 5.minutes)

Reject before touching params

The guard answers from the request path alone and never reads env.params. On Kemal 1.10.0 – 1.12.0, anything that parsed a multipart body and then rejected the request leaked the temporary files it spooled, permanently — so an unauthenticated client could fill the disk one rejected upload at a time (docs/04-kemal-integration.md).

Constructors

new(prefix : String, within : Time::Span | Nil = nil, assurance : AssuranceLevel | Nil = nil, credentials : Array(CredentialKind) | Nil = nil)

within turns this into a step-up guard: the subtree then needs authentication that is also recent, and a session restored from a remember-me cookie will never satisfy it.

credentials says which kinds of credential the subtree accepts, for the monolith that serves pages and an API from one process:

use KemalIdentity::Kemal::PathGuard.new(
  prefix: "/api", credentials: [KemalIdentity::CredentialKind::ApiToken, KemalIdentity::CredentialKind::Jwt]
)

A credential of another kind is refused with ForbiddenError — a 403, not a 401: the caller is authenticated, they are using the wrong door, and telling them to log in again would be a loop. The refusal is deliberately after require!, so an anonymous request is still a 401 and nobody learns which credential classes a subtree takes without first holding one.

CredentialKind::Custom covers every credential an application's own RequestAuthenticator establishes, so a deployment with two custom families cannot tell them apart here — CredentialRef#name is what distinguishes those, and a subtree that needs to select on it writes its own handler (blueprints/0021).

Source

Instance methods

guards?(path : String) : Bool

Whether this guard covers path.

The subtree, and only the subtree. /admin guards /admin, /admin/, and /admin/users — and pointedly not /administrators, which a naive starts_with? would hand to an attacker as an unguarded path that looks guarded.

Source
prefix
Source