Skip to content

HOG v2.1.0

HOG v2.1.0 is an additive release: it adds six features behind new, optional configuration, and every v2.0.0 config keeps working unchanged. The headline is provider compatibility and multi-instance identity — verifying access tokens from providers that don't shape them like ID tokens, and handing an already-resolved identity to a second HOG instance without a second round trip to the IdP.

Highlights

  • Access tokens from a dedicated key set. Verify Authorization: Bearer tokens signed by a key set other than the ID token's jwks_uri — and accept tokens that carry no sub or no standard aud at all.
  • Subject from any claim. The session subject (X-User-Id) can come from a configured claim instead of always sub, on both the cookie and the Bearer paths.
  • Group prefix strip. Trim a configured prefix off each rendered group value, so a directory's fully-qualified role names become a clean group set.
  • Authorization deny-redirect. Send a denied app-route request to a same-origin page instead of a bare 403.
  • Identity assertion between instances. A HOG instance mints a short-lived, signed statement of the principal it resolved; a second HOG instance behind it verifies and trusts that instead of re-resolving the identity itself.
  • A shipped session store. plugins/statestore-valkey is a ready-to-use session.StateStore backed by Valkey, kept in its own module so its dependency never reaches the core.

Access tokens from a dedicated key set

Not every provider signs access tokens the way it signs ID tokens. With some, access tokens verify against a key set of their own (discovery's jwks_access_token_uri, or an explicit bearer.jwksURL), carry the client identity in a claim such as client_id rather than the standard aud (bearer.audienceClaim), and omit sub entirely. A new bearer: block on the IdP resource — jwksURL, audienceClaim, requireAudience, signingAlgs — covers all of it; a token missing sub is no longer an automatic rejection, provided the subject can be resolved from elsewhere (see the next feature). See configure authentication.

Subject claim

identity.subjectClaim names the claim that becomes Principal.Subject (and, projected, X-User-Id), on both the cookie flow and the Bearer path. It defaults to sub, reproducing today's behaviour exactly; set it to something like uid for a token whose own subject lives elsewhere, or that carries no sub at all. See configure authentication.

Group prefix strip

identity.groups.strip removes the first matching prefix (case-insensitive) from each rendered group value — useful when a directory's role names carry a fully-qualified prefix that a policy shouldn't have to spell out every time. An empty list, the default, changes nothing. See configure authentication.

Authorization deny-redirect

access.onDeny.redirect lets an app route answer a denied authorization check with a 302 to a same-origin page instead of the plain 403 forbidden body — a friendlier landing for a browser than an error page. The redirect target is validated as same-origin at config load, exactly like the login flow's return_to; a service route always keeps the 403 regardless, so an API client's response shape never changes. See configure authorization.

Identity assertion between instances

identity.assertion lets one HOG instance mint a compact, Ed25519-signed statement of the principal it resolved (subject, passport, groups) and a second instance behind it verify and trust that statement instead of calling userinfo a second time. The issuing side (assertion.issue) mints a fresh, short-lived assertion (60 seconds by default, plus 30 seconds of clock skew at the accepting end) for every request on a route with the new forwardIdentity: true handler option; the accepting side (assertion.accept) verifies the signature and issuer and, by default, only lets a valid assertion enrich a principal a Bearer token has already authenticated on the same request — a header alone authenticates nothing unless requireBearer: false is set explicitly. See configure authentication and design: the identity assertion for the trust model this implies between two deployments.

Valkey-backed session store

plugins/statestore-valkey is a complete session.StateStore implementation backed by Valkey, shipped as a nested Go module so github.com/valkey-io/valkey-go never becomes a dependency of the core hog module. List it in Gateway.spec.plugins and reference type: valkey from stateProvider to unlock silent token refresh with a real, supported backend instead of writing your own. See scaling and availability.

Configuration additions

Field Default Purpose
IdP.spec.bearer.jwksURL discovery's jwks_access_token_uri, else jwks_uri Key set for verifying Bearer access tokens.
IdP.spec.bearer.audienceClaim — (checks aud) Claim compared against bearerAudience instead of aud.
IdP.spec.bearer.requireAudience true false accepts an access token with no audience at all; rejected at config load if combined with audienceClaim.
IdP.spec.bearer.signingAlgs the provider's advertised ID-token algorithms, or RS256 when it advertises none Signature algorithms accepted for access tokens.
Gateway.spec.identity.subjectClaim sub Claim that becomes the principal subject, on both the cookie and Bearer paths.
Gateway.spec.identity.groups.strip [] Case-insensitive prefixes removed from each rendered group value.
Route.spec.access.onDeny.redirect — (403) Same-origin path an authorization denial redirects to, on app routes.
Gateway.spec.identity.assertion.issue — Mints a signed identity assertion for a proxied hop (name, keyId, key, ttl, header).
Gateway.spec.identity.assertion.accept — Verifies an inbound identity assertion (issuer, keys, header, requireBearer).
handler.forwardIdentity (reverse-proxy, api) false Attach the minted identity assertion to this backend hop.
stateProvider.type: valkey — The shipped Valkey-backed session.StateStore, from plugins/statestore-valkey.

Upgrade notes

The import path now carries the major version

The module is github.com/paulopiriquito/hog/v2. Go requires a major-version suffix from v2 onwards, and without it the module proxy served only the v1 tags: go get github.com/paulopiriquito/hog quietly resolved to v1.3.0, the pre-rewrite code, which is why building a plugin against v2 did not work. This release fixes that, so v2.1.0 is the first v2 that go get can actually fetch. The v2.0.0 tag stays unfetchable; nothing can change that after the fact.

If you import HOG directly, for framework mode or for a plugin:

go get github.com/paulopiriquito/hog/v2@v2.1.0

and add /v2 to your import paths. Nothing else changes: the package names, the API and the configuration are the same. A configuration file is unaffected, and so is anything built with hog-build, which resolves the core module from the source you point it at rather than from the proxy.

The Valkey state store ships as a separate module on an unsuffixed path with its own version line, so it is pinned as github.com/paulopiriquito/hog/plugins/statestore-valkey@v1.0.0.

This release is additive: every field above is optional, defaults to today's behaviour, and an existing v2.0.0 config needs no changes to keep meaning exactly what it already means. There is nothing to migrate.

The JSON Schema (internal/configschema/hog.schema.json, published on this site and baked into the binary — run hog schema to print it offline) landed just after the v2.0.0 tag and is covered by this release along with everything above — see editor setup & schema validation if you haven't wired it into your editor or CI yet.