class

KemalIdentity::OIDC::PendingCodec

Inherits Reference < Object

Turns a Pending into one signed string, and back.

Why this is not left to the application

A flow's state has to survive a round trip through the provider, so it has to go somewhere: a table keyed by state, or a cookie. The cookie needs no schema, which makes it the obvious choice and also the one with a sharp edge — it holds the PKCE verifier, and an application that reaches for to_json and a plain cookie has just put a secret where the browser can read it and anybody can rewrite it.

So: signed with the application's key, and never treated as trustworthy input. A value that does not authenticate is nil, not an exception and not a partially-parsed struct.

What it is not

Not encryption. The payload is signed, not sealed, so the browser can read the verifier it is carrying. That is fine and is how PKCE works in a browser flow: the verifier proves that whoever redeems the code is whoever started the flow, and the browser is that party. What matters is that nobody can change it — swapping in their own state or nonce is precisely the attack, and the signature is what stops it.

Store the result in a cookie that is HttpOnly, Secure, SameSite=Lax and scoped to the callback path, and delete it as soon as the flow completes.

Constants

CONTEXT = "kemal_identity/oidc/pending/v1"

Domain separation from every other thing signed with the same key. A signature that is valid in two places is a signature that can be moved between them.

MAX_BYTES = 4096

Constructors

new(key : Secret)
Source

Instance methods

open?(value : String | Nil) : Pending | Nil

The Pending inside value, or nil if it does not authenticate.

Everything here arrives from a browser, so nothing raises: a truncated cookie, one signed with a rotated key, one somebody edited, and one that is not a cookie at all all produce the same nil.

Source
seal(pending : Pending) : String

payload.signature, both base64url.

Raises ArgumentError for a flow too large to be carried, rather than returning a value #open? will refuse. The cap is checked on both sides on purpose: a cookie that seals and does not open is a login that silently never completes, and the browser would be holding the evidence.

Source