securex

package
v0.19.4 Latest Latest
Warning

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

Go to latest
Published: Sep 20, 2026 License: Apache-2.0 Imports: 32 Imported by: 0

Documentation

Overview

Code generated by apic; DO NOT EDIT.

Code generated by apic; DO NOT EDIT.

Code generated by apic; DO NOT EDIT.

Code generated by apic; DO NOT EDIT.

Code generated by apic; DO NOT EDIT.

Code generated by apic; DO NOT EDIT.

Code generated by apic; DO NOT EDIT.

Package securex — OIDC asymmetric verifier.

VerifyJWTWithJWKS is the resource-server counterpart to signerx.SignJWT. It is the runtime piece that closes the GENERATOR_BUGS.md "OIDC capability gap" — see Recommended generator work step (1) "Port/adapt htpx/oidc.go JWKS verification into the securex verifier". Specifically:

  • Accepts asymmetric algorithms only (RS256/384/512, ES256/384/512, PS256/384/512). The `none` algorithm and every HMAC family are rejected unconditionally — RFC 7518 alg confusion is the primary practical OIDC attack class and the verifier MUST refuse to even attempt HMAC verification under an asymmetric profile.
  • Honors the same Require{Expiry,Issuer,Audience} options that VerifyJWTWithOptions does, so OIDC profiles get strict-claim enforcement by default.
  • Delegates signature verification to jwx's jwt.Parse, which is the same library that signerx.SignJWT signs with.

Code generated by apic; DO NOT EDIT.

Code generated by apic; DO NOT EDIT.

Code generated by apic; DO NOT EDIT.

Code generated by apic; DO NOT EDIT.

Code generated by apic; DO NOT EDIT.

Index

Constants

View Source
const DevEnvVar = "APIC_ENV"

DevEnvVar names the explicit-environment signal (A-03). The insecure-dev bypass refuses to engage unless this is set to "development" (case insensitive), so that debug mode alone — which is gin's DEFAULT and ships by omission unless GIN_MODE=release — can never silently enable it. Operators must make a positive development assertion.

View Source
const GenericForbiddenCode = "forbidden"

GenericForbiddenCode is the machine-readable `code` value securex writes in the ApiError envelope on every 403. Sibling of GenericUnauthorizedCode — same rationale, same generic-token contract for downstream regression tests. Pentest follow-up A-005 (2026-05-24, LOW): the F-CR-004 closure scrubbed the 401 surface but the 403 paths in RequireRole / RequireABAC and the generator template's role/scope/attribute branches still rendered `securex: insufficient role` verbatim, leaving the same fingerprinting surface for any authenticated-but-under-privileged caller. This constant + WriteForbidden close that gap.

View Source
const GenericPayloadTooLargeCode = "payload_too_large"

GenericPayloadTooLargeCode is the machine-readable `code` value securex writes in the ApiError envelope on the HMAC body-size 413 path. Sibling of GenericUnauthorizedCode / GenericForbiddenCode — same generic-token contract so downstream regression tests can assert the code without matching the volatile requestId field. Matches the generator's errorCodeForStatus(413) token so the HMAC 413 and the REST/JSON 413 surfaces are byte-for-byte identical.

Pentest follow-up F-CR-004 / A-005 hardening (2026-09-20): the HMAC body-too-large path previously wrote `{"error":"securex: request body exceeds signing limit: body exceeds 1048576 bytes","error_code":413}`, which leaked BOTH the framework name and the exact configured byte limit. This constant + the generic writeHMACErrorEnvelope close that gap; the sentinel (and the exact maxBodyBytes) go to the audit sink only.

View Source
const GenericUnauthorizedCode = "unauthenticated"

GenericUnauthorizedCode is the machine-readable `code` value securex writes in the ApiError envelope on every 401. It deliberately omits the `securex:` framework prefix so an unauthenticated caller cannot fingerprint the auth-middleware library from the response payload alone. Pinned to the literal string so downstream regression tests (in apic and every consumer) can assert the code token without matching the volatile requestId field.

SONNY-1840: WriteUnauthorized now emits the spec ApiError envelope `{"code","message","requestId"}` instead of the legacy bare `{"error":"unauthenticated"}` body. The generic, framework-neutral code preserves the property below.

Pentest finding F-CR-004 (2026-05-24, LOW). Prior to the fix, the generator's writeJSONError helper rendered ErrAuthFailed.Error() verbatim — i.e. `{"error":"securex: authentication failed"}` — which gave the pentester a free fingerprint for the auth middleware. The sentinel error values themselves still carry the `securex:` prefix so operator log triage stays natural; the prefix simply never reaches the wire on the 401 path.

View Source
const InsecureDevEnv = "APIC_INSECURE_DEV"

InsecureDevEnv is the canonical environment variable that opts into the development-only insecure bypass flow. Callers should never rely on this being set in production.

Variables

View Source
var (
	ErrInvalidConfig = errors.New("securex: invalid configuration")
	ErrAuthFailed    = errors.New("securex: authentication failed")
	// ErrRateLimited's user-facing text deliberately OMITS the `securex:`
	// framework prefix. Unlike the auth sentinels (whose prefix is scrubbed
	// at the write site by WriteUnauthorized/WriteForbidden), ErrRateLimited
	// .Error() is rendered verbatim as the ApiError `message` on the 429 path
	// (generator writeJSONError, api.go.tmpl). Keeping the prefix here would
	// fingerprint the rate-limiter library on the wire (F-CR-004 / A-005).
	// errors.Is callers are unaffected — matching is identity-based, not
	// substring-based.
	ErrRateLimited      = errors.New("rate limit exceeded")
	ErrInsufficientRole = errors.New("securex: insufficient role")
	// ErrOwnershipDenied is returned (server-side, in audit logs) when an
	// object-level authorization check fails — i.e. the authenticated
	// subject does not own the requested resource (SEC-0027 BOLA defense).
	// The wire response is a generic 403 via WriteForbidden; the sentinel
	// is logged only.
	ErrOwnershipDenied = errors.New("securex: object ownership denied")
	// ErrInsufficientAMR is returned (server-side, in audit logs) when a
	// request reaches a webauthnx.RequireAMR-gated route without the
	// required RFC 8176 Authentication Methods References claim — i.e.
	// the bearer token authenticated but the principal did not complete
	// the required step-up method (typical value: "webauthn"). The wire
	// surface is the generic 403 envelope so the required AMR is never
	// disclosed to the caller. GENWA-R8.
	ErrInsufficientAMR = errors.New("securex: insufficient amr (step-up required)")
	// ErrBodyTooLarge is returned by HMAC verification paths when a request
	// body exceeds the per-call limit. Streaming the body through sha256
	// via io.LimitReader prevents an attacker from forcing the server to
	// buffer arbitrarily large payloads to be signed. (PERF-0012.)
	ErrBodyTooLarge = errors.New("securex: request body exceeds signing limit")

	// ErrAuthVerifierRequired is the bootstrap sentinel raised (wrapped in
	// a panic) by the generated RegisterGeneratedAPI / RegisterGeneratedWS
	// calls when the configuration declares at least one route with
	// auth: "jwt" but the caller supplied a nil JWT verifier in
	// APIOptions.AuthJWT / WSOptions.AuthJWT. Closing GAP-0076: previously
	// a missing verifier silently 401'd every request, which masked
	// configuration errors. The generator now panics at registration so
	// the misconfiguration cannot ship. Use pkg/securex.NewTestVerifier
	// in unit-test harnesses where you want to accept any non-empty
	// Bearer.
	ErrAuthVerifierRequired = errors.New("securex: AUTH_VERIFIER_REQUIRED: apic requires a JWT verifier when any route declares auth: \"jwt\" (set APIOptions.AuthJWT / WSOptions.AuthJWT; see pkg/securex.NewTestVerifier for the test escape hatch)")

	// ErrAuthAPIKeyRequired mirrors ErrAuthVerifierRequired for routes that
	// declare auth: "api_key". The generator refuses to register such a
	// route when APIOptions.Auth / WSOptions.Auth is nil. GAP-0076.
	ErrAuthAPIKeyRequired = errors.New("securex: AUTH_API_KEY_REQUIRED: apic requires an API-key verifier when any route declares auth: \"api_key\" (set APIOptions.Auth / WSOptions.Auth; see pkg/securex.NewTestVerifier for the test escape hatch)")

	// ErrAuthWebhookRequired is returned by the generated boot-time guard
	// when a route declares auth="webhook" but the consumer-supplied
	// APIOptions.AuthWebhook is nil. Fail-closed: refuse to start rather
	// than silently accept every webhook delivery.
	ErrAuthWebhookRequired = errors.New("securex: APIOptions.AuthWebhook is nil but at least one route declares auth=webhook")

	// ErrAuthMTLSRequired is the bootstrap sentinel raised (wrapped in a
	// panic) by the generated RegisterGeneratedAPI when the configuration
	// declares at least one route with auth: "mtls" but the caller supplied
	// a nil APIOptions.AuthMTLS hook. securex.VerifyMTLS deliberately never
	// enforces SupportedIssuers (only opts.AuthMTLS enforces issuer/identity
	// policy), so a nil hook fails OPEN — accepting any valid CA-chain client
	// cert regardless of issuer label. Mirrors ErrAuthVerifierRequired /
	// ErrAuthWebhookRequired: the generator panics at registration so the
	// misconfiguration cannot ship.
	ErrAuthMTLSRequired = errors.New("securex: AUTH_MTLS_REQUIRED: apic requires an mTLS verifier when any route declares auth: \"mtls\" (set APIOptions.AuthMTLS; the generated server installs server.RequireClientCert / server.IssuerCNVerifier as secure defaults — see WithAuthMTLS)")

	// ErrMTLSRuntimeRequired is raised (wrapped in a panic) by the generated
	// RegisterGeneratedAPI when the configuration declares at least one
	// route whose mtls block sets crl/ocsp/cac_piv/principal_mapping (L-51)
	// but the caller supplied a nil APIOptions.MTLSRuntimes map. Unlike
	// AuthMTLS, MTLSRuntimes is populated automatically by server.Serve
	// (server_lib.go.tmpl constructs it once at boot from the same config
	// that declared these fields) — this guard exists for hand-built
	// APIOptions callers (custom entrypoints, tests) that would otherwise
	// silently get NO CRL/OCSP/CAC-PIV enforcement despite the config
	// asking for it, reproducing the exact false-assurance bug L-51 fixes.
	ErrMTLSRuntimeRequired = errors.New("securex: MTLS_RUNTIME_REQUIRED: apic requires APIOptions.MTLSRuntimes when any route's mtls block declares crl/ocsp/cac_piv/principal_mapping (server.Serve wires this automatically; hand-built APIOptions must set it explicitly or drop those fields)")

	// ErrCSRFSignerRequired is raised (wrapped in a panic) by the generated
	// RegisterGeneratedAPI boot guard when the configuration enables
	// security.csrf but the caller supplied a nil APIOptions.CSRF signer.
	// Fail-closed: refuse to start rather than silently skip CSRF
	// enforcement on cookie-authenticated state-changing requests. F2.
	ErrCSRFSignerRequired = errors.New("securex: CSRF signer required")
)

Sentinel errors for security and auth. Kept at package level per repo conventions. Messages use a lowercase, package-prefixed, diagnostic form so log output reads naturally; errors.Is callers should rely on pointer equality rather than substring matching of Error().

View Source
var (
	ErrMTLSCertMissing     = errors.New("securex: client certificate missing")
	ErrMTLSCertNotVerified = errors.New("securex: client certificate not verified")
	ErrMTLSCertEKUMissing  = errors.New("securex: client cert lacks clientAuth EKU")
	// ErrMTLSNoIssuer is returned when a route's MTLSPolicy.Runtime configures
	// a CRL or OCSP RevocationChecker but the verified chain carries no
	// issuer certificate to check the leaf against (r.TLS.VerifiedChains[0]
	// has fewer than two entries). This should not happen once a listener
	// has actually verified the peer chain; it fails closed rather than
	// skipping the revocation check.
	ErrMTLSNoIssuer = errors.New("securex: mtls: no issuer certificate in verified chain for revocation check")
)

Sentinel errors raised by VerifyMTLS. Callers should match with errors.Is rather than substring-matching the messages.

View Source
var AllowedAsymmetricAlgs = []jwa.SignatureAlgorithm{
	jwa.RS256(), jwa.RS384(), jwa.RS512(),
	jwa.ES256(), jwa.ES384(), jwa.ES512(),
	jwa.PS256(), jwa.PS384(), jwa.PS512(),
	jwa.EdDSA(),
}

AllowedAsymmetricAlgs is the default alg allowlist for OIDC asymmetric verification. Callers may override via JWKSVerifyOptions.AllowedAlgs; in either case `none` and every HMAC alg remain hard-rejected.

View Source
var ErrClaimControlChar = errors.New("securex: subject/role/scope claim contains a control character")

ErrClaimControlChar is returned (wrapped in ErrAuthFailed by the verifiers) when a subject, role or scope claim string carries a C0 control character (U+0000..U+001F) or DEL (SEC-0081, #381; extended to sub by SEC-NEW2-05, #411). All three are matched exactly against operator-configured names or stored owner ids, so a control byte can never make a legitimate match; its only uses are as a cache-key or log-injection payload (mcpx's tools/list memoization joined roles on NUL, and sub reaches every audit sink and the CSRF session binding verbatim). Refusing the token at the claims-typing seam -- VerifyJWTWithClaims and ParseClaims, which every verifier including the JWKS path goes through -- makes "no control characters in an identity or authorization claim" an enforced invariant rather than an assumption.

View Source
var ErrJWKSEmpty = errors.New("securex: jwks empty (no keys to verify against)")

ErrJWKSEmpty is returned when the supplied jwk.Set has no keys. A live IdP outage that drained the cache is a separable failure mode from signature mismatch and worth surfacing distinctly to callers.

View Source
var ErrJWTSubjectRequired = errors.New("securex: jwt sub claim required")

ErrJWTSubjectRequired indicates JWTVerifyOptions.RequireSubject rejected a token whose "sub" claim is absent, non-string, or empty after trimming. SEC-0061: generated servers derive the CSRF session id from the verified "sub" claim (opts.CSRFSessionID); an empty/absent subject collapses every caller onto csrfx's "" binding, which Verify now refuses outright (csrfx.ErrCSRFUnbound). Requiring "sub" here stops the ambiguity at the JWT verifier instead of only at the CSRF layer.

View Source
var ErrJWTUnsupported = errors.New("securex: jwt algorithm or token unsupported")

ErrJWTUnsupported indicates VerifyJWT cannot handle the given algorithm/token.

View Source
var ErrNoCookieToken = errors.New("securex: missing auth cookie")

ErrNoCookieToken indicates the named auth cookie was absent or empty.

View Source
var ErrVerifierMisconfigured = errors.New("securex: verifier misconfigured (Require* set without expected value)")

ErrVerifierMisconfigured is returned when the verifier options are internally inconsistent in a way that would silently weaken authentication. A-05: RequireIssuer/RequireAudience with no corresponding expected value (Issuer / Audience left empty) is a presence-only check that accepts ANY issuer/audience — it looks like a security control but enforces nothing. Rather than honour that footgun, the verifier fails closed with this configuration error so the misconfiguration is surfaced loudly at the first verification attempt.

View Source
var ErrWebhookFailed = errors.New("securex: webhook verification failed")

ErrWebhookFailed is returned by VerifyWebhook for every cause of rejection EXCEPT the body-cap case (which returns ErrBodyTooLarge directly so callers can map it to HTTP 413). The handler must NEVER include the wrapped reason in the response body — 401 responses go through WriteUnauthorized which writes the generic envelope. Internal reasons surface only via obsx.LogAudit.

Functions

func APIKeyAuth

func APIKeyAuth(r *http.Request, secret []byte, window time.Duration, seen *sync.Map) error

APIKeyAuth verifies Authorization: Bearer <hmac-hex> using a canonical request signature bound to method/path/body/nonce/timestamp and secret. It enforces a time window and replay protection via the seen map.

The request body is streamed through the hash via io.LimitReader with defaultHMACBodyLimit so that a single oversized request cannot exhaust server memory. See APIKeyAuthWithLimit for the explicit-limit variant.

func APIKeyAuthHTTP

func APIKeyAuthHTTP(w http.ResponseWriter, r *http.Request, secret []byte, window time.Duration, seen *sync.Map) bool

APIKeyAuthHTTP wraps APIKeyAuth with HTTP error mapping for PERF #171 (sign-or-413). On success it returns true and the caller continues dispatch with r.Body re-staged (pooledBuffer). On failure it writes the appropriate status (413 for ErrBodyTooLarge, 401 for any other auth error) and a JSON envelope carrying error_code = <status> so observability pipelines can route alerts off a structured field instead of regex-matching on Message.

The 413 status is reserved for the body-size condition; any other authentication failure (bad signature, missing headers, expired timestamp, replayed nonce, etc.) collapses to 401 with the same envelope shape. This is "Option B" from PERF_GAP_ANALYSIS row 13.

func APIKeyAuthHTTPWithLimit

func APIKeyAuthHTTPWithLimit(w http.ResponseWriter, r *http.Request, secret []byte, window time.Duration, seen *sync.Map, maxBodyBytes int64) bool

APIKeyAuthHTTPWithLimit is the explicit-limit variant of APIKeyAuthHTTP. A maxBodyBytes of 0 resolves to defaultHMACBodyLimit so that callers who have not migrated to an explicit cap still get the 1 MiB safe default. (PERF #171.)

func APIKeyAuthWithLimit

func APIKeyAuthWithLimit(r *http.Request, secret []byte, window time.Duration, seen *sync.Map, maxBodyBytes int64) error

APIKeyAuthWithLimit is the explicit-limit variant of APIKeyAuth. The maxBodyBytes parameter caps the number of bytes that will be read from r.Body when computing the canonical signature. A value of 0 resolves to defaultHMACBodyLimit. A body strictly larger than maxBodyBytes is rejected with an error that wraps ErrBodyTooLarge; callers can inspect via errors.Is.

func AllowInsecureDev

func AllowInsecureDev(r *http.Request, debugMode bool) bool

AllowInsecureDev reports whether the per-request INSECURE_DEV bypass should be honoured for this request. FOUR conditions must all hold (A-03):

  1. The TCP peer is a loopback address. The decision is made against r.RemoteAddr ONLY. X-Forwarded-For, X-Real-IP, and other proxy hints are intentionally ignored so that a reverse proxy cannot be tricked into forging a loopback origin.
  2. debugMode is true. The caller derives this from its own runtime mode (e.g. gin.Mode()==gin.DebugMode). Because debug is gin's DEFAULT (active unless GIN_MODE=release), debug mode alone is deliberately NOT sufficient — see condition 4.
  3. The environment variable APIC_INSECURE_DEV is set to a truthy value (1, true, yes, y, on - case insensitive).
  4. The environment variable APIC_ENV is set to "development" (case insensitive). This is the EXPLICIT positive development assertion that A-03 requires so the bypass cannot engage merely because debug mode shipped by omission.

If any condition fails, the bypass is denied. As a fail-closed guard, when APIC_INSECURE_DEV is truthy while the runtime is in release mode (GIN_MODE=release) the function PANICS — refusing to serve — rather than silently denying, because that combination is an unambiguous misconfiguration. A warning is logged at most once per process lifetime when the bypass first activates.

func AllowInsecureDevAtStartup

func AllowInsecureDevAtStartup(debugMode bool) bool

AllowInsecureDevAtStartup is the setup-time variant of AllowInsecureDev. No HTTP request is available yet, so only debugMode and the environment variable are checked. This is the correct helper for listener wiring, CORS configuration, and OIDC-missing startup branches - places where the per-request loopback check cannot yet run.

A-03: like AllowInsecureDev it refuses to start (panics) when APIC_INSECURE_DEV is truthy under release mode (GIN_MODE=release). The explicit APIC_ENV=development assertion is deliberately NOT required here: this helper governs the plain-HTTP-listener / TLS bypass (SEC-0002), a separate control from the per-request JWT-validation bypass that A-03 names. The release-mode hard-fail is the relevant fail-closed guard for the startup path; the stricter development-assertion gate is enforced on the per-request AllowInsecureDev, where the auth bypass actually decides a request.

func ContextWithAuthSource added in v0.18.3

func ContextWithAuthSource(ctx context.Context, src AuthSource) context.Context

ContextWithAuthSource returns a copy of ctx recording src as the credential transport a gate consumed for this request. Passing a nil ctx is safe and returns a context.Background()-derived value (mirrors ContextWithClaims).

func ContextWithClaims

func ContextWithClaims(ctx context.Context, claims *Claims) context.Context

ContextWithClaims returns a copy of ctx carrying the supplied Claims pointer. The generated per-route middleware first checks ClaimsFromContext(r.Context()) before re-parsing the bearer token, so a verifier (e.g. APIOptions.AuthJWT) that calls this once per request avoids the second base64-decode-and-Unmarshal per route. (S-NEW-3.)

Callers MUST only stash claims they have cryptographically verified (signature and expiry checked). Stashing claims from an unverified token is a security defect: downstream RBAC trusts these claims without re-checking the signature.

Passing a nil claims pointer stores a typed-nil so subsequent ClaimsFromContext(ctx) returns (nil, false) — this mirrors the "auth ran, but no claims attached" sentinel used by mcpx's ContextWithRoles when called with a nil slice.

Passing a nil ctx is safe and returns a context.Background()-derived value so downstream code can always derive a new ctx from the result.

func ContextWithMTLSPrincipal added in v0.18.3

func ContextWithMTLSPrincipal(ctx context.Context, principal, classification string) context.Context

ContextWithMTLSPrincipal returns ctx carrying the identity a route's CertPolicy verifier resolved for the verified client certificate: principal is the value selected by mtls.principal_mapping (subject_cn / upn / san_email / san_dns_first / edipi) and classification is the CAC/PIV classification (cacpiv.Classification.String()). VerifyMTLSRequest calls this ONLY after every check in the policy has passed; callers layering their own verifier on top should honor the same rule and never store an identity for a certificate that was not accepted.

func EvalComposite

func EvalComposite(r *http.Request, groups [][]Gate) (*http.Request, error)

EvalComposite evaluates a disjunctive-normal-form auth expression: the outer slice is OR-of-groups, each inner slice is an AND of gates. It tries groups left-to-right and returns the request from the FIRST group whose every gate passes (short-circuit). Each non-empty group starts from a deep clone of r (via r.Clone) so that header/trailer map mutations AND context-carried claims written by a partially-passing-then-failing group cannot leak into the next group or the handler. Returns the original request and ErrAuthFailed when no group fully passes (fail-closed: empty/nil groups also fail).

Note: r.Clone shares the request Body (io.ReadCloser) across groups — auth gates read TLS state, headers, and cookies, not the body, so this is intentional and out of scope for this isolation guarantee.

func ExtractCookieToken

func ExtractCookieToken(r *http.Request, cookieName string) (string, error)

ExtractCookieToken returns the JWT carried in the named HttpOnly auth cookie.

func MTLSPrincipalFromContext added in v0.18.3

func MTLSPrincipalFromContext(ctx context.Context) (principal, classification string, ok bool)

MTLSPrincipalFromContext returns the identity recorded by ContextWithMTLSPrincipal. ok is false -- and both strings are empty -- when the request was not admitted through an mTLS route with a CertPolicy (no principal_mapping / cac_piv configured), when the policy rejected the certificate, or when ctx is nil. A false ok never means "anonymous but present": routes without a CertPolicy get no fabricated identity, so consumers that need one must configure principal_mapping.

func NewTestVerifier

func NewTestVerifier() func(*http.Request) error

NewTestVerifier returns an Auth-style request verifier that accepts any request carrying a non-empty `Authorization: Bearer <token>` header. Empty Authorization headers, malformed headers, and missing tokens all return ErrAuthFailed.

The returned function is intended for unit-test ergonomics ONLY. It does not validate the bearer token; any non-empty value passes. Production code MUST NOT wire this verifier -- the apic-generated registration calls explicitly panic with ErrAuthVerifierRequired if the caller supplies nil for a route declaring auth: "jwt", which makes the "forgot to wire a real verifier" failure mode loud at boot.

Example use in a test:

api.RegisterGeneratedAPI(mux, srv, api.APIOptions{
    AuthJWT: securex.NewTestVerifier(),
    Auth:    securex.NewTestVerifier(),
})

The same function value satisfies both APIKey and JWT verifier slots because both contracts are `func(*http.Request) error`.

func RequestWithAuth

func RequestWithAuth(r *http.Request) *http.Request

RequestWithAuth applies the hardened default request auth policy.

func RequestWithAuthPolicy

func RequestWithAuthPolicy(r *http.Request, policy RequestAuthPolicy) *http.Request

RequestWithAuthPolicy clones r and fills auth headers from the configured transport sources, including websocket subprotocol auth markers when browser clients cannot set custom headers during the upgrade.

func RequireABAC

func RequireABAC(secret []byte, policies ...ABACPolicy) func(http.Handler) http.Handler

RequireABAC returns an http.Handler middleware that enforces ABAC policies. The JWT is verified using the secret, and all policies must evaluate to true. Fails with 401 Unauthorized for invalid tokens, and 403 Forbidden for policy failures.

func RequireABACWithOptions

func RequireABACWithOptions(secret []byte, opts JWTVerifyOptions, policies ...ABACPolicy) func(http.Handler) http.Handler

RequireABACWithOptions is the iss/aud-aware variant of RequireABAC (A-01). The supplied JWTVerifyOptions bind the in-handler HS256 fallback to an expected issuer/audience. Zero-value opts reproduce the legacy behavior.

func RequireOwnership

func RequireOwnership(secret []byte, extract OwnerExtractor, next http.Handler) http.Handler

RequireOwnership returns middleware enforcing object-level authorization (SEC-0027 / OWASP API1:2023 BOLA): the authenticated subject (JWT "sub") must equal the resource-owner id returned by extract. This is the building block the generated by-id handlers (e.g. GET/PATCH/DELETE /v1/users/{userId}) cannot supply themselves, because only the consumer knows which claim maps to which resource. Wire it per owner-scoped route.

Secure-by-default: a missing/invalid token is 401; a subject that does not match the resource owner is a generic 403 (ErrOwnershipDenied, logged server-side only). If extract reports no owner-scoped resource, the request passes through unchanged so non-owned routes are unaffected. An empty JWT subject can never match and is therefore always denied.

func RequireOwnershipWithOptions

func RequireOwnershipWithOptions(secret []byte, opts JWTVerifyOptions, extract OwnerExtractor, next http.Handler) http.Handler

RequireOwnershipWithOptions is the iss/aud-aware variant of RequireOwnership (A-01). The supplied JWTVerifyOptions bind the in-handler HS256 fallback to an expected issuer/audience. Zero-value opts reproduce the legacy behavior.

func RequireRole

func RequireRole(minimum Role, secret []byte, next http.Handler) http.Handler

RequireRole returns an http.Handler middleware that enforces a minimum role. The JWT is read from "Authorization: Bearer <token>" and verified with secret. Requests whose highest role is below minimum receive 403 Forbidden. Requests with missing/invalid tokens receive 401 Unauthorized.

func RequireRoleH added in v0.19.2

func RequireRoleH(h *RoleHierarchy, minimum Role, secret []byte, opts JWTVerifyOptions, next http.Handler) http.Handler

RequireRoleH is RequireRoleWithOptions bound to an EXPLICIT role vocabulary. Prefer it over RequireRole/RequireRoleWithOptions, which can only resolve against the built-in admin > manager > user vocabulary: a server whose security.roles declares platform_admin > assessor > viewer needs its own hierarchy, and before ENG-4634 the only way to install one was SetRoleHierarchy, which mutated a process-wide graph shared with every other server in the binary.

A nil hierarchy falls back to the built-in default, so RequireRoleH(nil, ...) is exactly RequireRoleWithOptions.

func RequireRoleWithOptions

func RequireRoleWithOptions(minimum Role, secret []byte, opts JWTVerifyOptions, next http.Handler) http.Handler

RequireRoleWithOptions is the iss/aud-aware variant of RequireRole (A-01). The supplied JWTVerifyOptions are honoured on the in-handler HS256 fallback, binding the token to an expected issuer/audience so a validly-signed token minted for a different service is rejected (401) rather than accepted. When opts is the zero value this behaves exactly like the legacy RequireRole; set Issuer/Audience (and the matching Require* flags) to enforce binding.

func SetAuditSink added in v0.18.3

func SetAuditSink(fn func(event string, fields map[string]any))

SetAuditSink installs fn as the destination for WriteUnauthorized and WriteForbidden's audit events. Passing nil restores the default sink, which logs via slog.Default().Warn(event, "fields", fields). pkg/securex intentionally does not import pkg/obsx (avoiding an import cycle); callers that want auth decisions routed through obsx's redacted fanout call securex.SetAuditSink(obsx.LogAudit) themselves (generated servers do this in server_lib.go.tmpl / cli/serve).

func SetRateHeaders

func SetRateHeaders(w http.ResponseWriter, limit, remaining int, reset time.Time)

SetRateHeaders sets basic rate-limit headers on the response.

func SignAPIKeyRequest

func SignAPIKeyRequest(r *http.Request, secret []byte, now time.Time) error

SignAPIKeyRequest signs r using the same canonical request rules enforced by APIKeyAuth. It sets Authorization, X-Nonce, and X-Timestamp on the request.

func SignAPIKeyRequestWithValues

func SignAPIKeyRequestWithValues(r *http.Request, secret []byte, nonce, ts string) error

SignAPIKeyRequestWithValues signs r with explicit nonce and timestamp values. This is useful for deterministic tests and for flows that need to precompute signed WebSocket auth material.

func SignWebhookRequest

func SignWebhookRequest(r *http.Request, secret []byte, alg WebhookAlg, now time.Time) error

SignWebhookRequest stamps r with X-Webhook-Id, X-Timestamp, and the HMAC signature header so the request passes VerifyWebhook against the same secret. Use this from outbound delivery code and from tests; the receiver does NOT depend on this helper.

The id is a 16-byte random nonce (base64-url, no padding) — the same shape APIKeyAuth uses for X-Nonce so consumers see a uniform id format across both auth modes. now is supplied so tests can pin a deterministic timestamp; production callers pass time.Now().

func StartNonceGC

func StartNonceGC(ctx context.Context, seen *sync.Map, interval time.Duration) chan struct{}

StartNonceGC starts a GC loop to remove expired nonces stored by APIKeyAuth. N-15: context-first (ctx is the first parameter, matching the repo's documented context-first convention) and clamps a non-positive interval to defaultNonceGCInterval instead of letting time.NewTicker panic. gen.SafeLoop recovers a panic and restarts the loop body, but interval is a closed-over value that never changes across restarts, so an invalid interval previously spun forever in a panic/backoff/restart cycle rather than ever making progress. The returned channel keeps its existing contract (closing it stops the loop); ctx cancellation stops it too, whichever comes first.

func StartNonceGCLegacy deprecated added in v0.17.0

func StartNonceGCLegacy(seen *sync.Map, interval time.Duration) chan struct{}

StartNonceGCLegacy is a back-compat shim for callers written against the pre-N-15 two-argument StartNonceGC(seen, interval) signature (Q-3).

Deprecated: use StartNonceGC(ctx, seen, interval). N-15 added a leading ctx parameter to StartNonceGC so the GC loop honors caller cancellation (the repo's documented context-first convention); that changed StartNonceGC's signature without a compatibility path or a BREAKING CHANGE footer, and it shipped uncompilable example code in docs/WEBHOOK_MODE.md as a result. StartNonceGCLegacy runs the loop with context.Background() -- identical to StartNonceGC's pre-N-15 behavior, which never observed cancellation either -- so old call sites keep compiling and behaving exactly as before while new code uses StartNonceGC directly for ctx-aware shutdown.

func VerifyJWT

func VerifyJWT(token string, secret []byte) error

VerifyJWT verifies a compact JWS (HS256) with the provided secret. Returns ErrJWTUnsupported if algorithm is not HS256 or token malformed.

SECURE DEFAULT (SEC-0028): VerifyJWT REQUIRES the "exp" claim. A token that omits "exp" never expires, which turns a single leaked bearer token into a permanent authorization-server compromise primitive. RFC 7519 §4.1.4 marks "exp" OPTIONAL, but a resource server MUST NOT accept a non-expiring access token. Callers that truly need the legacy accept-no-exp behavior must opt in explicitly via VerifyJWTAllowNoExpiry (or VerifyJWTWithOptions with RequireExpiry:false) and own that risk.

func VerifyJWTAllowNoExpiry

func VerifyJWTAllowNoExpiry(token string, secret []byte) error

VerifyJWTAllowNoExpiry is the explicit, clearly-named opt-out from the VerifyJWT secure default (SEC-0028). It verifies the HS256 signature and any present "exp"/iss/aud claims, but — unlike VerifyJWT — it ACCEPTS a token that omits "exp" entirely (a never-expiring token).

This exists only for narrow legacy/back-compat call sites that knowingly issue or accept non-expiring tokens. Prefer VerifyJWT (or VerifyJWTWithOptions with the appropriate Require* fields) for anything exposed to untrusted input. Using this on an internet-facing resource server is an authorization weakness.

func VerifyJWTWithJWKS

func VerifyJWTWithJWKS(_ context.Context, token string, keys jwk.Set, opts JWKSVerifyOptions) (jwt.Token, error)

VerifyJWTWithJWKS parses and verifies a compact JWS token using the supplied jwk.Set. Returns the parsed jwt.Token on success, or ErrAuthFailed / ErrJWTUnsupported / ErrJWKSEmpty on failure.

The asymmetric-only contract is enforced before signature verification: the JWS header `alg` is parsed and compared against the allowlist; any `none` or HMAC alg returns ErrJWTUnsupported without touching keys or running the underlying jwt.Parse signature path.

func VerifyJWTWithOptions

func VerifyJWTWithOptions(token string, secret []byte, opts JWTVerifyOptions) error

VerifyJWTWithOptions verifies a compact JWS (HS256) with the provided secret and enforces optional issuer/audience checks when configured.

func VerifyMTLS

func VerifyMTLS(r *http.Request, policy MTLSPolicy) error

VerifyMTLS enforces a per-route mTLS policy against an HTTP request whose connection terminated on a TLS listener. Call after the listener has completed the TLS handshake but before any handler logic runs.

Checks, in order:

  1. r and r.TLS are non-nil. Defends against an accidentally plaintext reverse-proxy hop in front of the server stripping mTLS state.
  2. If policy.Required, at least one peer certificate is present.
  3. The peer cert chain has been verified by the listener (r.TLS.VerifiedChains is non-empty).
  4. If policy.EKUValidation, the leaf certificate carries the clientAuth (or ExtKeyUsageAny) Extended Key Usage.
  5. If policy.Runtime.CRL and/or policy.Runtime.OCSP is configured (L-51), the leaf is checked against the issuer's CRL/OCSP revocation state. A zero-value Runtime is a no-op here.
  6. If policy.Runtime.CertPolicy is configured (L-51), the leaf is verified against the CAC/PIV certificate-classification policy.

SupportedIssuers is intentionally NOT enforced. Callers that need issuer-label validation should layer their own verifier on top after VerifyMTLS returns nil; the apic-emitted handler invokes opts.AuthMTLS for exactly that purpose.

VerifyMTLS is the error-only form: it enforces exactly what VerifyMTLSRequest enforces but cannot hand back the request enriched with the identity step 6 resolved. Callers that run a business handler after the gate should use VerifyMTLSRequest so the principal reaches it (L-55).

func VerifyMTLSRequest added in v0.18.3

func VerifyMTLSRequest(r *http.Request, policy MTLSPolicy) (*http.Request, error)

VerifyMTLSRequest is VerifyMTLS that also returns the request to continue with. When policy.Runtime.CertPolicy accepts the leaf, the returned request's context carries the resolved principal and classification (MTLSPrincipalFromContext) and -- when the verifier implements CertPolicyContextVerifier, as the generated server's cacpiv adapter does -- the full identity object (mtlsx.From). Otherwise r is returned as-is: a route with no CertPolicy gets NO fabricated identity, and on any error the returned request carries nothing new (the Gate contract's "stash only on success" rule), so a failed AND-group leaves no principal residue.

func VerifyWebhook

func VerifyWebhook(r *http.Request, secret []byte, pol WebhookPolicy) error

VerifyWebhook checks r against pol using the supplied secret. Returns nil on success. Returns ErrBodyTooLarge (detectable via errors.Is) when the request body exceeds pol.MaxBodyBytes so callers can map it to HTTP 413 — mirrors APIKeyAuthWithLimit. Returns ErrWebhookFailed for every other rejection: empty secret, missing/malformed headers, non-allowlisted alg, expired timestamp, bad signature, replay.

The body is rewrapped on success so the user handler can re-read it; on failure the body may be partially consumed and the caller should treat r as untrusted.

secret is supplied per-request (not stored on pol) so the generator can resolve it from APIOptions.AuthWebhook keyed on pol.Name. Zero (nil/empty) secret causes verification to fail regardless of any other input.

func WebhookReplayStorePtr

func WebhookReplayStorePtr() *sync.Map

WebhookReplayStorePtr returns a pointer to the package-shared webhook replay store. Pass this to StartNonceGC at server start so expired entries (entries whose value time.Time is in the past) are reaped on the same cadence as the API-key nonce store. Task 10 of the webhook-receiver plan wires this from server.go.tmpl.

Returning a *sync.Map (not a copy) is intentional: StartNonceGC mutates the map in place to delete expired keys.

func WriteForbidden

func WriteForbidden(w http.ResponseWriter, internalErr error)

WriteForbidden writes the package-standard 403 response: Content-Type application/json, status 403, and the spec ApiError envelope `{"code","message","requestId"}` with a generic, framework-neutral code (GenericForbiddenCode) and message. The supplied internalErr (typically ErrInsufficientRole, or a wrapped scope / attribute reason) is logged at WARN level via log/slog so operators can still triage the underlying cause — it is intentionally NOT echoed into the response body.

Mirrors WriteUnauthorized in shape so the two helpers stay behaviour-symmetric for downstream regression tests that diff the 401 and 403 surfaces side-by-side.

All package-internal call sites that previously emitted `http.Error(w, ErrInsufficientRole.Error(), 403)` or `writeJSONError(w, securex.ErrInsufficientRole.Error(), 403)` MUST route through this helper to guarantee the A-005 closure across the generated server set.

func WriteUnauthorized

func WriteUnauthorized(w http.ResponseWriter, internalErr error)

WriteUnauthorized writes the package-standard 401 response: Content-Type application/json, status 401, and the spec ApiError envelope `{"code","message","requestId"}` with a generic, framework-neutral code (GenericUnauthorizedCode) and message. The supplied internalErr (typically one of the package-level Err* sentinels) is logged at WARN level via log/slog so operators can still triage the underlying cause from server logs — it is intentionally NOT echoed into the response body.

All package-internal call sites that previously emitted `http.Error(w, ErrAuthFailed.Error(), 401)` or `writeJSONError(w, securex.ErrAuthFailed.Error(), 401)` MUST route through this helper to guarantee the F-CR-004 closure across the generated server set.

Types

type ABACPolicy

type ABACPolicy interface {
	// Evaluate returns true if the claims satisfy the policy.
	Evaluate(c *Claims) bool
}

ABACPolicy defines an attribute evaluation policy.

func HasAttribute

func HasAttribute(key string) ABACPolicy

HasAttribute returns a policy that evaluates to true if the attribute key exists.

func MatchAttribute

func MatchAttribute(key, value string) ABACPolicy

MatchAttribute returns a policy that evaluates to true if the attribute exactly matches the given value.

type ABACPolicyFunc

type ABACPolicyFunc func(c *Claims) bool

ABACPolicyFunc is a function type that implements ABACPolicy.

func (ABACPolicyFunc) Evaluate

func (f ABACPolicyFunc) Evaluate(c *Claims) bool

Evaluate calls the underlying function.

type AuthSource

type AuthSource int

AuthSource identifies how a request presented its credential. The CSRF middleware enforces only AuthSourceCookie (ambient browser credential); AuthSourceHeader (Bearer) is exempt because a cross-origin page cannot set the Authorization header.

const (
	// AuthSourceNone means no credential was found on the request.
	AuthSourceNone AuthSource = iota
	// AuthSourceHeader means the credential was a well-formed
	// "Authorization: Bearer <token>" header.
	AuthSourceHeader // Authorization: Bearer …
	// AuthSourceCookie means the credential was the named HttpOnly auth
	// cookie — an ambient browser credential, which is why the CSRF
	// middleware enforces only this source.
	AuthSourceCookie // HttpOnly auth cookie
	// AuthSourceQuery means the credential was carried in the
	// ?access_token= query parameter.
	AuthSourceQuery // ?access_token=…
)

func AuthSourceConsumed added in v0.18.3

func AuthSourceConsumed(r *http.Request) AuthSource

AuthSourceConsumed reports the credential transport a request was actually admitted on, for CSRF classification AFTER an auth ladder has run. It is the post-authentication counterpart of AuthSourceForRequest: when the winning auth path lifted and verified the session cookie it stamped AuthSourceCookie on the context (ContextWithAuthSource) and that marker is authoritative; otherwise the ambient cookie was NOT the credential -- an api_key, mTLS, webhook, or bearer path admitted the request without ever reading it -- so its mere presence must not classify the request as cookie-authenticated. Before this helper the generated handlers fell back to AuthSourceForRequest for that case, which reports Cookie whenever a session cookie is present and no well-formed bearer header is, so an api_key/mTLS/webhook client carrying a stale browser cookie was refused with 403 on every state-changing call (deny-side only, but wrong). The bearer-header and ?access_token= classifications are kept so a state-changing request admitted on a query token is still refused.

func AuthSourceForRequest

func AuthSourceForRequest(r *http.Request, cookieName string) AuthSource

AuthSourceForRequest reports the credential transport, mirroring the extraction priority: Authorization header > auth cookie > ?access_token=.

SEC-0060: this used to treat ANY non-empty Authorization header as AuthSourceHeader, including a malformed value like "Authorization: junk" that no verifier would ever accept. On a cookie-auth route, a forged header (attacker-controllable cross-site — unlike the victim's ambient session cookie) made this report Header while the cookie-lift auth branch still authenticated the request from the real cookie, so the CSRF middleware (which enforces only the Cookie source) never ran. Only a well-formed "Bearer <token>" header now counts as the header source; a malformed one falls through to the cookie check.

func AuthSourceFromContext added in v0.18.3

func AuthSourceFromContext(ctx context.Context) (AuthSource, bool)

AuthSourceFromContext returns the AuthSource stashed by ContextWithAuthSource, or (AuthSourceNone, false) if none was stashed.

type Bucket

type Bucket = ratebucket.Bucket

Bucket provides a simple token bucket limiter for per-route/tool scoping. Bucket aliases the canonical token bucket. securex previously carried a byte-identical private copy of this algorithm (QG-046/059/PERF-0044); Allow/Snapshot/AllowAndSnapshot now come from the shared ratebucket type.

func NewBucket

func NewBucket(rate, burst float64) *Bucket

NewBucket creates a new token bucket with the provided rate and burst.

type BucketSnapshot

type BucketSnapshot = ratebucket.Snap

BucketSnapshot is the value form of (limit, remaining, resetTime) returned by AllowAndSnapshot. Aliases ratebucket.Snap (fields Limit, Remaining, Reset).

type CertPolicyContextVerifier added in v0.18.3

type CertPolicyContextVerifier interface {
	CertPolicyVerifier
	VerifyContext(ctx context.Context, leaf *x509.Certificate) (enriched context.Context, principal, classification string, err error)
}

CertPolicyContextVerifier is an optional extension of CertPolicyVerifier. A verifier that can also produce a richer identity object (the pkg/securex/cacpiv adapter attaches the full *mtlsx.Principal via mtlsx.ContextWith) implements VerifyContext; VerifyMTLSRequest prefers it over Verify when present so the enriched context flows to the handler.

Contract: on a non-nil error the returned context MUST be ctx unchanged (nothing stashed on the failure path -- the same rule Gate imposes on claims), and the string results carry the same meaning as CertPolicyVerifier.Verify's. This package never imports mtlsx (mtlsx imports securex), which is why the rich principal travels through the verifier-owned context rather than a type named here.

type CertPolicyVerifier added in v0.17.0

type CertPolicyVerifier interface {
	Verify(leaf *x509.Certificate) (principal, classification string, err error)
}

CertPolicyVerifier enforces certificate-classification policy (e.g. DOD CAC / federal PIV require_person / required_policy_oids / reject_unknown_classification) against a verified leaf certificate and resolves the configured mtls.principal_mapping to an identity string. A non-nil error fails the request closed. Concrete implementations live in pkg/securex/cacpiv (Adapter.Verify).

type CertPolicyVerifierFunc added in v0.17.0

type CertPolicyVerifierFunc func(leaf *x509.Certificate) (principal, classification string, err error)

CertPolicyVerifierFunc adapts a plain function to CertPolicyVerifier, mirroring the standard library's http.HandlerFunc idiom. The generated server uses this to wrap a boot-constructed cacpiv.Adapter (whose Verify method takes an extra mapping argument baked in via closure) without requiring pkg/securex to import pkg/securex/cacpiv.

func (CertPolicyVerifierFunc) Verify added in v0.17.0

Verify implements CertPolicyVerifier by calling f.

type Claims

type Claims struct {
	Subject    string            `json:"sub"`
	Roles      []string          `json:"roles"`
	Scopes     []string          `json:"scopes"`
	Attributes map[string]string `json:"attributes,omitempty"`

	// Email is the standard `email` claim, surfaced for handler convenience
	// (F4). Empty when the token omits it.
	Email string `json:"email,omitempty"`
	// Exp is the standard `exp` (expiry) claim as a Unix timestamp (seconds).
	// Zero when absent. Signature/expiry are enforced by VerifyJWT, not here.
	Exp int64 `json:"exp,omitempty"`

	// AMR is the RFC 8176 Authentication Methods References list, e.g.
	// ["webauthn"], ["pwd","otp"], ["mfa"]. Populated by ParseClaims
	// from the `amr` JSON array claim when present; consumed by
	// webauthnx.RequireAMR (GENWA-R8) to gate step-up routes. An empty
	// or absent claim leaves the slice nil — RequireAMR treats nil as
	// "no method asserted" and 403s any required-AMR gate.
	AMR []string `json:"amr,omitempty"`
}

Claims holds the JWT payload fields relevant to RBAC and scope enforcement. Call ParseClaims only after the token signature has been verified.

func ClaimsFrom

func ClaimsFrom(ctx context.Context) (*Claims, bool)

ClaimsFrom is the public, ergonomically-named accessor for the verified claims a JWT-verifying gate stashed in ctx (F4). Thin alias of ClaimsFromContext: returns (claims, true) when present, else (nil, false).

func ClaimsFromContext

func ClaimsFromContext(ctx context.Context) (*Claims, bool)

ClaimsFromContext returns the Claims pointer stashed by ContextWithClaims, or (nil, false) if no claims have been attached. The returned pointer is the same value passed to ContextWithClaims (not a copy); callers MUST treat it as read-only or risk torn reads across goroutines that share the request context. (S-NEW-3.)

func ParseClaims

func ParseClaims(token string) (*Claims, error)

ParseClaims decodes the payload of a compact JWS token and returns the Claims. It does NOT verify the token signature; callers must call VerifyJWT first. It DOES refuse (ErrClaimControlChar, wrapped in ErrAuthFailed) a role or scope string carrying a control character, so every verifier that types its claims through here shares that invariant (SEC-0081, #381).

func VerifyJWTWithClaims added in v0.17.0

func VerifyJWTWithClaims(token string, secret []byte, opts JWTVerifyOptions) (*Claims, error)

VerifyJWTWithClaims verifies a compact JWS (HS256) exactly as VerifyJWTWithOptions does, and on success returns the *Claims decoded from the SAME payload segment the verifier already base64-decoded.

NP-05: the pre-existing call pattern was VerifyJWTWithOptions (decodes the payload into map[string]any to run the exp/nbf/iat/iss/aud checks) immediately followed by ParseClaims(token) (re-splits the compact JWS, re-base64-decodes the SAME payload segment, and re-unmarshals it into Claims) on every authenticated request. VerifyJWTWithClaims shares the split + signature verification + base64 payload decode with VerifyJWTWithOptions via verifyJWTPayload, and pays only ONE extra json.Unmarshal (into the typed Claims struct, which is not interchangeable with the map[string]any the claim checks need — Claims has no iss/aud/nbf fields) instead of re-doing the split and base64 decode as well.

Every check below is byte-for-byte the same as VerifyJWTWithOptions (verifyJWTPayload IS that check body) — this is a security-sensitive path and callers must be able to rely on identical accept/reject behavior between the two entry points.

func (*Claims) HasAMR

func (c *Claims) HasAMR(method string) bool

HasAMR reports whether c.AMR contains the given method reference. RFC 8176 §1 defines the claim as a JSON array of case-sensitive strings; matching is exact (no case-folding) to stay faithful to the registry (https://www.iana.org/assignments/amr-values).

func (*Claims) HasScope

func (c *Claims) HasScope(scope string) bool

HasScope returns true if the claims list contains the exact scope string.

func (*Claims) HighestRole

func (c *Claims) HighestRole() Role

HighestRole returns the highest Role found in the claims' Roles list, resolved against the BUILT-IN admin > manager > user vocabulary. Returns RoleUser if the list is empty or contains no recognised role names.

A server configured with a custom role vocabulary MUST use (*RoleHierarchy).Highest against its own hierarchy instead: this method cannot see one, and every name outside the built-in three resolves to RoleUser here (ENG-4634). apic emits the hierarchy-aware form.

type Gate

type Gate func(r *http.Request) (*http.Request, error)

Gate is one composable authentication check used by EvalComposite. It returns the (possibly claim-augmented) request and nil on success, or the request and a non-nil error on failure. A Gate MUST stash verified claims (via ContextWithClaims on the returned request) ONLY when it succeeds — never on the failure path — so that a failed AND-group leaves no claim residue. A Gate's verifier MUST verify the token signature and expiry BEFORE stashing claims via ContextWithClaims; stashing claims for an unverified token is a security defect because downstream RBAC (RequireRole, authzFromClaims) trusts these claims without re-checking the signature.

func GateAPIKey

func GateAPIKey(verifyAPIKey func(*http.Request) error) Gate

GateAPIKey runs the API-key verifier.

func GateCookie

func GateCookie(cookieName string, verifyJWT func(*http.Request) error) Gate

GateCookie lifts a session JWT from the named cookie into the Authorization header (on a clone, so a failed OR-group cannot leak the header) and validates it with the shared JWT verifier.

SEC-0060/0061: when this gate actually lifts a session cookie and the verification succeeds, it stamps AuthSourceCookie on the returned request's context via ContextWithAuthSource -- ONLY on that success path (mirrors the Gate contract's "stash only on success" rule for claims) and ONLY when the cookie was actually present and consumed, never when this gate fell through to an already-present Authorization header with no cookie in play. This lets CSRF classification (in the generated handler) read back the credential THIS gate actually consumed instead of assuming every route whose auth expression carries a "cookie" leaf always consumed the cookie — a composite route like "api_key || cookie" may be admitted by a different OR-group without this gate ever running.

func GateDeny

func GateDeny() Gate

GateDeny always fails closed; emitted for an unrecognized DNF leaf.

func GateJWT

func GateJWT(verifyJWT func(*http.Request) error) Gate

GateJWT runs the JWT verifier (which stashes verified claims on success).

func GateMTLS

func GateMTLS(hook func(*http.Request, MTLSPolicy) error, policy MTLSPolicy) Gate

GateMTLS verifies client-cert presence/EKU via VerifyMTLSRequest, then runs the optional issuer/CAC-PIV policy hook. A nil hook still requires a valid cert (fail-closed). cac/piv tokens map to GateMTLS; their issuer policy is enforced by the wired hook plus the route's MTLSPolicy.

L-55: on success the returned request is the one VerifyMTLSRequest enriched with the resolved principal/classification (see MTLSPrincipalFromContext / mtlsx.From), and the hook observes that same enriched request. On any failure -- VerifyMTLSRequest or the hook -- the gate hands back the request it was given, so no identity leaks out of a failed AND-group.

type JWKSVerifyOptions

type JWKSVerifyOptions struct {
	// Issuer, when non-empty, is matched against the token's `iss` claim.
	Issuer string
	// Audience, when non-empty, is matched against the token's `aud`.
	Audience string

	// RequireExpiry / RequireIssuer / RequireAudience demand presence of
	// the corresponding claim (independent of the string fields' value
	// matching). OIDC profiles should set all three.
	//
	// A-05: RequireIssuer/RequireAudience are presence-only and would accept
	// ANY value unless the matching Issuer/Audience field is also set. Setting
	// RequireIssuer with an empty Issuer (or RequireAudience with an empty
	// Audience) is a misconfiguration that VerifyJWTWithJWKS rejects with
	// ErrVerifierMisconfigured rather than silently authorizing arbitrary
	// issuers/audiences. RequireExpiry has no value field and is exempt.
	RequireExpiry   bool
	RequireIssuer   bool
	RequireAudience bool

	// AllowedAlgs, when non-empty, overrides AllowedAsymmetricAlgs.
	// `none` and HMAC family algs are rejected regardless of override.
	AllowedAlgs []jwa.SignatureAlgorithm
}

JWKSVerifyOptions configures VerifyJWTWithJWKS.

type JWTVerifyOptions

type JWTVerifyOptions struct {
	// Issuer is the expected "iss" claim value. Empty disables issuer-value
	// comparison (see RequireIssuer for presence enforcement).
	Issuer string
	// Audience is the expected "aud" claim value. Empty disables
	// audience-value comparison (see RequireAudience for presence enforcement).
	Audience string

	// RequireExpiry rejects any token that omits the "exp" claim.
	// RFC 7519 §4.1.4 marks exp OPTIONAL, but OIDC resource servers
	// MUST reject non-expiring access tokens (a never-expiring
	// bearer token is an authorization-server compromise primitive).
	RequireExpiry bool

	// RequireIssuer rejects any token that omits the "iss" claim.
	// Has no effect when Issuer == "" (no expected value to compare
	// against). When both Issuer != "" and RequireIssuer == true,
	// the claim must be present AND match.
	RequireIssuer bool

	// RequireAudience rejects any token that omits the "aud" claim.
	// Has no effect when Audience == "". When both Audience != "" and
	// RequireAudience == true, the claim must be present AND match.
	RequireAudience bool

	// RequireSubject rejects any token whose "sub" claim is absent,
	// non-string, or empty after trimming whitespace. Unlike
	// RequireIssuer/RequireAudience it has no companion expected-value
	// field — it is a pure presence-and-non-emptiness check, since "sub" is
	// per-caller by design. Generated servers set this from
	// security.csrf.enabled (SEC-0061): the CSRF session binding is derived
	// from "sub", and an empty subject is indistinguishable from "no
	// session" (csrfx.Verify now refuses that fail-closed via
	// ErrCSRFUnbound), so refusing it here closes the gap at its source.
	RequireSubject bool
}

JWTVerifyOptions are optional constraints for issuer/audience claim checks.

The boolean Require* fields demand presence of the corresponding claim: a missing claim returns ErrAuthFailed regardless of the string fields' content. Use the Require* form for OIDC resource-server profiles where RFC 6749 / OpenID Connect Core mandate the claim. Without Require*, a missing claim is silently accepted (legacy backwards-compatible behavior — see GENERATOR_BUGS.md "OIDC capability gap" A-S2).

type MTLSPolicy

type MTLSPolicy struct {
	// Required, when true, demands a peer certificate be presented and
	// verified. When false, requests that omit a client cert are allowed
	// through; this matches the tls.VerifyClientCertIfGiven listener mode.
	Required bool
	// EKUValidation, when true, demands the leaf certificate carry the
	// x509.ExtKeyUsageClientAuth extended key usage (or ExtKeyUsageAny).
	EKUValidation bool
	// SupportedIssuers is plumbed through for caller-side issuer-label
	// policy (PIV/CAC/custom). VerifyMTLS does NOT enforce this slice;
	// the mapping from label to certificate Subject/Issuer is deployment-
	// specific and must be supplied via the opts.AuthMTLS hook.
	SupportedIssuers []string
	// Runtime carries the boot-constructed CRL/OCSP revocation checkers and
	// CAC/PIV certificate-policy verifier for this route (L-51). The zero
	// value (all nil fields) enforces nothing beyond the fields above,
	// matching pre-L-51 behavior exactly.
	Runtime MTLSRuntime
}

MTLSPolicy is the per-route mTLS verification policy emitted by the apic code generator and consumed by VerifyMTLS at request time. The shape mirrors the MTLSContract block in the apic config.

type MTLSRuntime added in v0.17.0

type MTLSRuntime struct {
	// CRL, when non-nil, is consulted after EKU validation. A non-nil
	// error (including ErrCertRevoked from a hard-fail checker) rejects
	// the request. A checker constructed with AllowSoftFail:true instead
	// returns nil on fetch failure, which is exactly the "warn and
	// continue" behavior L-51 requires for allow_soft_fail:true.
	CRL RevocationChecker
	// OCSP mirrors CRL for OCSP-based revocation checks. When both CRL and
	// OCSP are configured, CRL is checked first.
	OCSP RevocationChecker
	// CertPolicy, when non-nil, is invoked AFTER CRL/OCSP checks pass. It
	// enforces cac_piv policy (require_person / required_policy_oids /
	// reject_unknown_classification) and resolves principal_mapping. On
	// success VerifyMTLSRequest records the returned principal and
	// classification on the request context (MTLSPrincipalFromContext) and,
	// when the verifier also implements CertPolicyContextVerifier, lets it
	// attach its richer identity object as well (the cacpiv adapter stores
	// the full *mtlsx.Principal, readable via mtlsx.From) -- L-55.
	CertPolicy CertPolicyVerifier
}

MTLSRuntime bundles the boot-constructed, per-route revocation checkers and CAC/PIV policy verifier that VerifyMTLS enforces IN ADDITION to Required/EKUValidation/SupportedIssuers (L-51). The zero value performs no additional checks, so embedding it in MTLSPolicy is fully backward compatible with the pre-L-51 policy shape.

CRLChecker/OCSPChecker hold response caches and make network calls, so the generated server constructs each route's MTLSRuntime exactly ONCE at boot (server_lib.go.tmpl) and shares the same instance across every request for that route -- never reconstruct one per request.

type OwnerExtractor

type OwnerExtractor func(r *http.Request) (ownerID string, ok bool)

OwnerExtractor pulls the resource-owner identifier out of an inbound request — typically a path parameter such as {userId}. It returns the owner id and true when one is present, or false when the request carries no owner-scoped resource (in which case RequireOwnership lets the request through to be handled by other authz layers). The extractor MUST NOT trust any value from the request body for the owner id; it should read the routed path/segment so it cannot be spoofed independently of the route.

type RequestAuthPolicy

type RequestAuthPolicy struct {
	// AllowAuthorizationHeader accepts a credential presented in the normal
	// HTTP "Authorization" header (Bearer or Signature).
	AllowAuthorizationHeader bool
	// AllowSubprotocolBearer accepts a bearer token carried in the WebSocket
	// Sec-WebSocket-Protocol subprotocol list (the browser WS API cannot set
	// arbitrary headers, so the token rides the subprotocol negotiation).
	AllowSubprotocolBearer bool
	// AllowSubprotocolSignature accepts an HTTP-signature credential carried
	// in the WebSocket Sec-WebSocket-Protocol subprotocol list.
	AllowSubprotocolSignature bool
	// AllowQueryBearer accepts a bearer token supplied as a query-string
	// parameter. Off by default in the hardened policy: query strings leak
	// into access logs, proxies, and Referer headers.
	AllowQueryBearer bool
	// AllowQuerySignature accepts an HTTP-signature credential supplied as a
	// query-string parameter (same logging-exposure caveat as AllowQueryBearer).
	AllowQuerySignature bool
}

RequestAuthPolicy controls which alternate auth transports are accepted when a request cannot set normal Authorization headers directly.

func DefaultRequestAuthPolicy

func DefaultRequestAuthPolicy() RequestAuthPolicy

DefaultRequestAuthPolicy is the hardened default used by generated servers.

type RevocationChecker added in v0.17.0

type RevocationChecker interface {
	Check(ctx context.Context, leaf, issuer *x509.Certificate) error
}

RevocationChecker checks whether a leaf certificate has been revoked by its issuer (CRL or OCSP). Concrete implementations live in pkg/securex/mtlsx (CRLChecker / OCSPChecker); MTLSPolicy.Runtime references only this interface so that this package never imports mtlsx (mtlsx already imports securex -- importing back would cycle). AllowSoftFail behavior (warn-and-continue vs hard-fail) is baked into the concrete checker at construction time, not re-decided here: L-51 requires allow_soft_fail:false to hard-fail and the checker itself is the single source of truth for that decision.

type Role

type Role int

Role represents an RBAC privilege level. Higher values mean greater privilege.

const (
	// RoleUser is the lowest privilege level in the default hierarchy, and
	// the fallback returned by RoleFromString/HighestRole when no role
	// matches.
	RoleUser Role = iota // lowest privilege (default)
	// RoleManager is the intermediate privilege level in the default
	// hierarchy (above RoleUser, below RoleAdmin).
	RoleManager // intermediate privilege
	// RoleAdmin is the highest privilege level in the default hierarchy.
	RoleAdmin // highest privilege
)

func RoleFromString

func RoleFromString(s string) (Role, error)

RoleFromString parses a case-insensitive role name against the BUILT-IN admin > manager > user vocabulary. Returns ErrInvalidConfig for unknown names. A server with a custom vocabulary must call (*RoleHierarchy).Parse on its own hierarchy instead — this function has no way to see it, which is precisely why the process-wide SetRoleHierarchy was removed (ENG-4634).

func (Role) HasRole

func (r Role) HasRole(required Role) bool

HasRole returns true if r has at least the privilege level of required. Implements the role hierarchy: admin ≥ manager ≥ user.

func (Role) String

func (r Role) String() string

String returns the canonical name of a role in the BUILT-IN vocabulary. Ranks outside it render as "user" (the historical fallback). Use (*RoleHierarchy).Name for a custom vocabulary.

type RoleHierarchy added in v0.19.2

type RoleHierarchy struct {
	// contains filtered or unexported fields
}

RoleHierarchy is an ordered, case-insensitive RBAC role vocabulary. It is immutable after construction and therefore safe for concurrent use, so two servers in one process may hold DIFFERENT vocabularies without either one re-ranking the other's roles (ENG-4634 / GitLab #364).

It replaces the package-level roleRank/roleNames pair that SetRoleHierarchy used to overwrite: that pair was one process-wide graph, so a second generated server's init() silently re-ranked the first server's roles — and the generator's own emitter documented the limitation as "composing services with DIFFERENT role vocabularies into one binary is unsupported". It is supported now: hand each surface its own hierarchy.

func DefaultRoleHierarchy added in v0.19.2

func DefaultRoleHierarchy() *RoleHierarchy

DefaultRoleHierarchy returns the built-in admin > manager > user vocabulary that RoleFromString, Role.String and Claims.HighestRole resolve against. Generated servers that declare a custom security.roles list build their own with NewRoleHierarchy instead.

func MustRoleHierarchy added in v0.19.2

func MustRoleHierarchy(namesHighestFirst []string) *RoleHierarchy

MustRoleHierarchy is NewRoleHierarchy for a vocabulary that is known good at build time — the built-in default, and the generated `var _roleHierarchy` apic emits from security.roles (which the generator already validated). It panics rather than returning an error so a malformed vocabulary can never degrade into "every role unparseable", i.e. a server that 403s every RBAC-gated route at runtime instead of refusing to start.

func NewRoleHierarchy added in v0.19.2

func NewRoleHierarchy(namesHighestFirst []string) (*RoleHierarchy, error)

NewRoleHierarchy builds a vocabulary from names ordered MOST-PRIVILEGED FIRST (index 0 = highest privilege), matched case-insensitively. It fails closed on an empty list, a blank name, or a case-insensitive duplicate — each of which would otherwise produce a silently wrong privilege ordering. (SetRoleHierarchy skipped blanks and let a duplicate overwrite a rank, leaving a hole in roleNames and two names sharing one rank.)

func (*RoleHierarchy) Highest added in v0.19.2

func (h *RoleHierarchy) Highest(c *Claims) Role

Highest returns the most privileged role in c that this vocabulary knows. Unknown role strings are ignored — a role name this vocabulary never declared cannot grant privilege — and an empty/nil claim set yields the lowest rank (RoleUser), matching Claims.HighestRole.

func (*RoleHierarchy) Name added in v0.19.2

func (h *RoleHierarchy) Name(r Role) string

Name returns the canonical (lowercased) name for a rank in this vocabulary, or "" when the rank is outside it.

func (*RoleHierarchy) Names added in v0.19.2

func (h *RoleHierarchy) Names() []string

Names returns this vocabulary's role names ordered MOST-PRIVILEGED FIRST — the same order NewRoleHierarchy consumed. The returned slice is a copy.

func (*RoleHierarchy) Parse added in v0.19.2

func (h *RoleHierarchy) Parse(s string) (Role, error)

Parse resolves a case-insensitive role name against this vocabulary. A nil receiver resolves against the built-in default so a zero-valued consumer fails the same way it always did rather than panicking.

type WebhookAlg

type WebhookAlg string

WebhookAlg names the HMAC family the verifier accepts. The generator emits an allowlist literal per route; the verifier rejects every alg outside the allowlist, defending against downgrade attempts where a sender stamps `alg=md5` and we silently honour it.

const (
	// WebhookAlgSHA256 selects HMAC-SHA256 for webhook signature
	// verification. This is the default when a request omits the
	// signature-algorithm header and the default allowlist member when
	// WebhookPolicy.Algs is empty.
	WebhookAlgSHA256 WebhookAlg = "sha256"
	// WebhookAlgSHA512 selects HMAC-SHA512 for webhook signature
	// verification. Must be explicitly listed in WebhookPolicy.Algs to be
	// accepted; it is not part of the implicit default allowlist.
	WebhookAlgSHA512 WebhookAlg = "sha512"
)

type WebhookPolicy

type WebhookPolicy struct {
	// Name is the operator-facing webhook name (e.g. "stripe", "github").
	// Passed to APIOptions.AuthWebhook to fetch the secret and recorded
	// in audit events.
	Name string

	// Algs is the HMAC family allowlist. Empty (nil/zero-length) means
	// "sha256 only" so a config that forgets to set this still defaults
	// to a safe value. The verifier compares case-insensitively against
	// the value carried in the X-Signature-Alg header (or the default
	// sha256 when absent).
	Algs []WebhookAlg

	// Window bounds the |now - timestamp| acceptable skew. A zero value
	// is treated as 5*time.Minute by VerifyWebhook (Task 2). The
	// 5-minute default matches the industry de-facto window for
	// HMAC-signed webhooks (Stripe, GitHub, PagerDuty).
	Window time.Duration

	// MaxBodyBytes caps the body the verifier will hash. Zero resolves
	// to defaultHMACBodyLimit (1 MiB) at codegen.
	MaxBodyBytes int64

	// SignatureHeader is the header the signature lives in. Defaults
	// to "X-Signature" when empty. Generator emits the configured
	// override (e.g. "X-Hub-Signature-256") verbatim.
	SignatureHeader string

	// TimestampHeader is the unix-seconds header. Defaults to
	// "X-Timestamp" when empty.
	TimestampHeader string

	// IDHeader is the per-event id header used for replay protection.
	// Defaults to "X-Webhook-Id" when empty. The (id, timestamp) tuple
	// goes into the package-shared replay map so an attacker cannot
	// re-deliver an intercepted POST.
	IDHeader string
}

WebhookPolicy is the verification contract for a single webhook receiver route. The generator emits a literal of this struct directly into the per-route handler. Secret resolution is external: APIOptions.AuthWebhook(name) returns the HMAC key at request time, keyed on the WebhookPolicy.Name field below.

Directories

Path Synopsis
Code generated by apic; DO NOT EDIT.
Code generated by apic; DO NOT EDIT.
Code generated by apic; DO NOT EDIT.
Code generated by apic; DO NOT EDIT.
Code generated by apic; DO NOT EDIT.
Code generated by apic; DO NOT EDIT.
Package harness provides RBAC/ABAC test helpers for downstream services that consume the generated API.
Package harness provides RBAC/ABAC test helpers for downstream services that consume the generated API.
pkg/securex/hashx/algorithm.go Package hashx implements salted, self-describing, FIPS-140-3-aware one-way hashing and constant-time verification for passwords and other authentication material.
pkg/securex/hashx/algorithm.go Package hashx implements salted, self-describing, FIPS-140-3-aware one-way hashing and constant-time verification for passwords and other authentication material.
Code generated by apic; DO NOT EDIT.
Code generated by apic; DO NOT EDIT.
Package sessionx implements FedRAMP AC-7 account lockout, AC-11 inactivity timeout, and AC-12 session termination tracking.
Package sessionx implements FedRAMP AC-7 account lockout, AC-11 inactivity timeout, and AC-12 session termination tracking.
Code generated by apic; DO NOT EDIT.
Code generated by apic; DO NOT EDIT.
pkcs11
Code generated by apic; DO NOT EDIT.
Code generated by apic; DO NOT EDIT.
softfile
Code generated by apic; DO NOT EDIT.
Code generated by apic; DO NOT EDIT.
Code generated by apic; DO NOT EDIT.
Code generated by apic; DO NOT EDIT.

Jump to

Keyboard shortcuts

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