claims

package
v0.48.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 23, 2026 License: MIT Imports: 5 Imported by: 0

Documentation

Overview

Package claims provides the canonical 3-header identity contract for every Base-derived service. There is exactly ONE way to read the authenticated caller's identity: FromHeaders.

The Hanzo Gateway validates the IAM JWT upstream and re-emits exactly three headers on the forwarded request. Services MUST NOT read any other variant (no X-Hanzo-*, no X-IAM-*, no singular X-User-Role, no X-Tenant-Id alias):

X-User-Id <- JWT "sub"
X-Org-Id  <- JWT "owner"
X-Roles   <- JWT "roles" (comma-joined if array)

Services MUST call StripIdentityHeaders on every inbound request before the JWT middleware re-injects trusted values. A client that sets any of these headers directly is rejected at the gateway; services defend in depth by stripping again locally in case a sidecar/mesh bypasses the gateway.

Gateway-upstream assertion and request-context helpers.

Every Base-derived service MUST sit behind hanzoai/gateway. The gateway is the sole JWT verifier in the stack. Services refuse to serve tenant-scoped routes unless this invariant is explicitly acknowledged at boot.

Usage in a service:

if err := claims.AssertGatewayUpstream(); err != nil {
    log.Fatal(err) // boot refuses
}
mux.Use(claims.Strip)           // defense-in-depth: drop forged headers
mux.Use(claims.RequireGateway)  // 503 if headers are missing on tenant routes
mux.Use(claims.Inject)          // pull Claims into ctx

Index

Constants

View Source
const (
	HeaderUserID = "X-User-Id"
	HeaderOrgID  = "X-Org-Id"
	HeaderRoles  = "X-Roles"
)

The canonical 3 identity headers. These are the ONLY headers a handler may read to determine the authenticated principal.

View Source
const EnvGatewayUpstream = "HANZO_GATEWAY_UPSTREAM"

EnvGatewayUpstream is the environment variable that acknowledges the service is reachable only through hanzoai/gateway. Any value other than "1" / "true" triggers a boot refusal.

View Source
const MaxIdentityValueLen = 256

MaxIdentityValueLen caps every header value consumed by this package. Chosen at 256 bytes: generous for ULIDs/UUIDs, IAM usernames, and the longest realistic comma-joined role list (~20 role names @ 12 chars). A value longer than this is either a bug or an exhaustion attempt and is discarded — the caller observes "no identity" and RequireGateway 503s.

Variables

View Source
var ErrGatewayBypass = errors.New("claims: gateway bypass detected — canonical identity headers missing")

ErrGatewayBypass is returned / surfaced when a tenant-scoped request reaches a handler without the canonical identity headers. It maps to HTTP 503; 401 would suggest the client can recover by authenticating, which is wrong — the deployment topology is broken, not the caller.

View Source
var ErrGatewayNotAsserted = errors.New("claims: HANZO_GATEWAY_UPSTREAM must be set to 1 — apps never re-verify JWT")

ErrGatewayNotAsserted is returned at boot when HANZO_GATEWAY_UPSTREAM is unset or falsy.

Functions

func AssertGatewayUpstream added in v0.44.0

func AssertGatewayUpstream() error

AssertGatewayUpstream returns an error if the gateway-upstream acknowledgement is missing. Call exactly once at service boot, before listening.

func Chain added in v0.44.0

func Chain(next http.Handler) http.Handler

Chain is the canonical tenant-route middleware chain: Strip → Inject → RequireGateway → next. This is the ONLY approved way to mount a tenant-scoped handler. Services MUST NOT compose Strip, Inject, and RequireGateway by hand — the PHILOSOPHY.md "one and only one way" principle applies, and a misordered hand-wired chain silently defeats the forged-header defense (verified by Red probe P7-H3).

Public routes (/healthz, /readyz, /metrics) MUST be mounted on a separate mux that does not pass through Chain; the chain would 503 every probe when gateway headers are absent.

func HasRole added in v0.44.0

func HasRole(ctx context.Context, role ...string) bool

HasRole reports whether the caller holds any of the requested roles.

func Inject added in v0.44.0

func Inject(next http.Handler) http.Handler

Inject is a middleware that parses the canonical 3 identity headers and attaches the resulting Claims to the request context. It does NOT validate presence — pair it with RequireGateway for tenant-scoped routes.

func OrgID added in v0.44.0

func OrgID(ctx context.Context) string

OrgID is a thin convenience that returns the caller's org slug.

func RequireGateway added in v0.44.0

func RequireGateway(next http.Handler) http.Handler

RequireGateway is a middleware for tenant-scoped routes. If either X-User-Id or X-Org-Id is missing after Strip + Inject have run, the gateway was bypassed (misconfigured ingress, direct pod access, etc.) and the handler MUST NOT serve. Returns 503 with a neutral body (no hint about which header was missing — that is an attacker oracle).

func RequireRole added in v0.44.0

func RequireRole(role ...string) func(http.Handler) http.Handler

RequireRole returns a middleware that enforces the caller holds at least one of the requested roles. On failure, the response is a 404 — not 403 — so probing for authorized endpoints does not leak their existence. If you want an explicit 403 for a user that IS in the tenant but lacks a role, check HasRole inside the handler instead.

func Strip

func Strip(next http.Handler) http.Handler

Strip is a net/http middleware that calls StripIdentityHeaders on every inbound request before delegating to next. Use at the outermost layer of a service, before any JWT middleware that populates the canonical 3 headers.

func StripIdentityHeaders

func StripIdentityHeaders(h http.Header)

StripIdentityHeaders removes every inbound identity-bearing header from h. Call this before JWT validation re-injects the canonical values. It also unconditionally drops every header whose name starts with "X-Hanzo-" or "X-IAM-" (case-insensitive), closing the "clever-new-prefix" attack vector.

func UserID added in v0.44.0

func UserID(ctx context.Context) string

UserID is a thin convenience that returns the caller's user id.

Types

type Claims

type Claims struct {
	UserID string
	OrgID  string
	Roles  []string
}

Claims is the verified identity of the current request as asserted by the upstream gateway's JWT validation. All three fields may be empty strings / empty slices when the request is unauthenticated (public endpoints).

func FromContext added in v0.44.0

func FromContext(ctx context.Context) Claims

FromContext returns the verified Claims attached by Inject. Returns the zero Claims{} if none were attached (e.g. public route).

func FromHeaders

func FromHeaders(r *http.Request) Claims

FromHeaders returns the canonical Claims for the request. It reads ONLY the three canonical headers; any legacy variant set by a client is ignored by design (and should have been stripped upstream).

Header values are sanitized: any value that contains a control character (byte < 0x20 or byte == 0x7f) or exceeds MaxIdentityValueLen bytes is discarded and the corresponding field becomes empty. This makes log / path / response-splitting injection unreachable through this parser, and makes RequireGateway fail closed (503) on a poisoned identity instead of forwarding hostile bytes into handlers.

Roles are decoded from a comma-separated list; empty roles, roles that exceed the length cap individually, and roles that contain control characters are each dropped.

func (Claims) HasRole

func (c Claims) HasRole(wanted ...string) bool

HasRole reports whether the caller holds any of the requested roles. Role names are matched exactly (case-sensitive); empty inputs return false.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL