module

Kemal::Session::CSRF

Rails-compatible CSRF ("authenticity token") support, built on the shared session cookie.

This is a port of ActionController::RequestForgeryProtection (actionpack 8.1) with its SessionStore token strategy, so tokens are exchangeable in both directions: a form rendered by Rails posts successfully to a Kemal route and vice-versa. The pieces, all of which must stay byte-compatible:

  • the real token lives in the session at _csrf_token, encoded as unpadded urlsafe base64 of 32 random bytes (Rails' SecureRandom.urlsafe_base64(32))
  • a token handed to a client is masked: raw = HMAC-SHA256(real, identifier), then urlsafe_base64(pad + (pad XOR raw)) for a fresh 32-byte pad
  • the identifier is "!real_csrf_token" for the global token, or "<action_path>#<method>" for a per-form token
  • verification unmasks and accepts the global token, the real token, or the per-form token for the current path+method

Only GET and HEAD skip verification — exactly like Rails, which does not exempt OPTIONS/TRACE.

Constants

GLOBAL_IDENTIFIER = "!real_csrf_token"

Identifier Rails HMACs to derive the global (non-per-form) token.

HEADER_NAME = "X-CSRF-Token"

Header inspected in addition to the request parameter.

SAFE_METHODS = ["GET", "HEAD"] of ::String

Request methods that skip verification, matching Rails' verified_request?.

SESSION_KEY = "_csrf_token"

Session key used by Rails' SessionStore token strategy.

TOKEN_LENGTH = 32

Length of the raw (unmasked) token in bytes (Rails' AUTHENTICITY_TOKEN_LENGTH).

Class methods

each_request_token(context : HTTP::Server::Context, & : String -> ) : Nil

Yields the candidate tokens carried by this request: the X-CSRF-Token header plus the authenticity-token parameter from the query string, form body or JSON body (Rails looks in the header and params).

Each source is touched only when the iteration reaches it, and they are ordered by what they cost to read. That matters beyond speed: parsing a multipart/form-data body spools every file part to disk, so a request that matches on the header never pays for — or leaves behind — an upload it didn't need to look at.

Source
hidden_field(context : HTTP::Server::Context, action : String | Nil = nil, method : String | Nil = nil) : String

The hidden input Rails' form_with/form_tag emits. action and method are the form's, and enable a per-form token.

Source
meta_tags(context : HTTP::Server::Context) : String

Rails' csrf_meta_tags, so Turbo/Rails UJS on a Kemal-rendered page picks the token up and sends it as X-CSRF-Token.

Source
normalize_action_path(action : String, request_path : String | Nil = nil) : String

Rails' normalize_action_path: strip query/fragment, drop a trailing slash, and resolve a relative action against the current request path.

Source
real_token(session : Session) : String

The encoded real token from the session, generated and persisted when absent (Rails' real_csrf_token plus its commit_csrf_token write).

Source
request_tokens(context : HTTP::Server::Context) : Array(String)

The candidate tokens carried by this request, as an array. Reads every source; each_request_token is the short-circuiting form.

Source
reset(session : Session) : Nil

Drops the real token, so the next real_token call rotates it. Rails does this on sign-in/sign-out (reset_csrf_token).

Source
token(session : Session, action : String | Nil = nil, method : String | Nil = nil, request_path : String | Nil = nil) : String

A masked token to hand to a client — the value Rails would render as form_authenticity_token. Pass action and method to get a per-form token (only honoured when config.csrf_per_form_tokens is on); action may be relative, in which case request_path is used to resolve it.

Source
valid_origin?(context : HTTP::Server::Context) : Bool

Rails' valid_request_origin?: an absent Origin is accepted (some user agents omit it), otherwise it must equal this request's base URL. Rails raises on a literal "null" origin; here it simply fails the check.

Source
valid_token?(session : Session, token : String | Nil, path : String | Nil = nil, method : String | Nil = nil) : Bool

Whether token is a valid authenticity token for session. path and method are the current request's, needed to accept per-form tokens.

Source
verified_request?(context : HTTP::Server::Context) : Bool

Whether this request passes CSRF verification: safe methods always do, anything else needs a same-origin request carrying a valid token in the X-CSRF-Token header or the authenticity-token parameter.

Source
verify!(context : HTTP::Server::Context) : Nil

Like verified_request?, but raises InvalidAuthenticityToken on failure (Rails' protect_from_forgery with: :exception).

Source

Nested types