middleware

package
v1.1.0 Latest Latest
Warning

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

Go to latest
Published: May 12, 2026 License: MIT Imports: 31 Imported by: 0

Documentation

Overview

Package middleware provides common HTTP middleware handlers for use with MuxMaster or any net/http-compatible router.

Each middleware is a function that takes an http.Handler and returns an http.Handler, following the standard Go middleware pattern:

func(next http.Handler) http.Handler

Usage with MuxMaster:

import "github.com/FlavioCFOliveira/MuxMaster/middleware"

mux := muxmaster.New()
mux.Use(middleware.Logger(os.Stdout))
mux.Use(middleware.Recoverer)
mux.Use(middleware.CORS(middleware.CORSOptions{
    AllowedOrigins: []string{"https://example.com"},
}))

Index

Examples

Constants

View Source
const DefaultThrottlePerIPMaxTableSize = 100_000

DefaultThrottlePerIPMaxTableSize is the default upper bound on the number of distinct keys ThrottlePerIP will track concurrently. When the table is full, requests for NEW keys are rejected with 503 to bound memory under IP-churn attacks (MSR-2026-0068). Existing keys keep working.

Variables

This section is empty.

Functions

func APIKey

func APIKey(opts APIKeyOptions) func(http.Handler) http.Handler

APIKey authenticates requests by matching an extracted key against a pre-hashed set of valid keys. All keys are SHA-256 hashed at construction time; per-request cost is one SHA-256 hash plus a [32]byte map lookup — no iteration, no string comparison.

On success, the identity string associated with the key is injected into the request context and retrievable via GetAPIKeyIdentity. Panics if opts.Keys is nil or empty.

Example
package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"

	"github.com/FlavioCFOliveira/MuxMaster/middleware"
)

func main() {
	// Authenticate requests by API key with identity lookup.
	mw := middleware.APIKey(middleware.APIKeyOptions{
		Keys: map[string]string{
			"sk_test_abc": "user-123",
			"sk_test_def": "user-456",
		},
		Header: "X-API-Key",
	})

	inner := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		identity, ok := middleware.GetAPIKeyIdentity(r.Context())
		if ok {
			fmt.Println(identity)
		}
	})

	rec := httptest.NewRecorder()
	req := httptest.NewRequest("GET", "/api/data", nil)
	req.Header.Set("X-API-Key", "sk_test_abc")

	mw(inner).ServeHTTP(rec, req)
}
Output:
user-123

func BasicAuth

func BasicAuth(realm string, creds map[string]string) func(http.Handler) http.Handler

BasicAuth enforces HTTP Basic Authentication using constant-time credential comparison. Passwords are SHA-256 hashed at construction time so that subtle.ConstantTimeCompare always operates on equal-length inputs, eliminating both user-enumeration timing (MM-2026-0009) and password-length oracle (MM-2026-0020). Panics if creds is nil.

func CORS

func CORS(opts CORSOptions) func(http.Handler) http.Handler

CORS handles Cross-Origin Resource Sharing. Panics on invalid configuration.

SECURITY: AllowedOrigins must be set explicitly. Passing nil or an empty slice is a misconfiguration trap (HPS-2026-0003): the middleware would silently let cross-origin requests through with no ACAO header, hiding the issue from the operator. We panic at construction time so the misconfiguration is caught at boot.

ORDERING (MSR-2026-0070): CORS sets `Access-Control-Allow-Origin` (and related Access-Control-* + Vary headers) when its frame runs. If another middleware that calls `Header().Set(...)` runs AFTER CORS in the request flow (innermost in the Use() chain), the late Set will OVERWRITE the CORS-managed values, silently bypassing the configured whitelist. To keep CORS authoritative, register CORS as the INNERMOST middleware that touches these headers (i.e. last in the Use() chain that handles them) or avoid calling SetHeader on CORS-managed names. See SetHeader for the composition rule.

func CleanPath

func CleanPath() func(http.Handler) http.Handler

CleanPath normalises r.URL.Path via path.Clean before routing. When r.URL.RawPath is set, it is also cleaned; if the cleaned RawPath differs from what path.Clean produces for the percent-decoded Path, RawPath is zeroed to prevent encoded path-traversal bypass (MM-2026-0018).

func Compress

func Compress(level int) func(http.Handler) http.Handler

Compress compresses responses with gzip when the client accepts it. Responses smaller than 1024 bytes are passed through uncompressed. Uses streaming compression — memory usage is bounded regardless of response size. Panics on invalid compression level.

OPERATIONAL (DOS-2026-0007): the compress middleware buffers up to 8 KiB per stalled connection while sniffing whether the response is large enough to compress. Operators MUST configure http.Server.ReadHeaderTimeout and http.Server.WriteTimeout (and a connection cap via a Listener limit) to bound the total memory held by N stalled connections; the middleware itself does not enforce a per-connection timeout.

SECURITY (BREACH / DOS-2026-0006): do NOT echo user-controlled input alongside a secret (OAuth2 scope, CSRF token, session ID, JWT) inside a gzip-compressed response body. Compression amplifies tiny size differences that depend on whether the user's input matches a prefix of the secret — this is the BREACH oracle (Cohen's d > 10 measured in reports/dos-resilience-tester/harness/breach_oracle_test.go), letting an attacker recover the secret character-by-character with ~2 requests per character.

Mitigations, in order of preference:

  1. Do not compress endpoints that echo user input near secrets — wrap them with a different middleware chain that excludes Compress.
  2. Move secrets out of the response body (set them in headers, cookies, or separate API endpoints not reachable via attacker-controlled input).
  3. Add variable-length random padding (>= 256 bytes, length randomised per request) to the response body. Validated by TestBREACHOracleWithRandomPadding (Cohen's d drops below 0.03).

MuxMaster cannot apply these mitigations on the operator's behalf because they require knowledge of which fields are secret vs user-controlled. See SECURITY.md "BREACH mitigation" for the full pattern.

func GetAPIKeyIdentity

func GetAPIKeyIdentity(ctx context.Context) (string, bool)

GetAPIKeyIdentity returns the identity string associated with the validated API key, as injected by the APIKey middleware.

func GetRequestID

func GetRequestID(ctx context.Context) string

GetRequestID returns the request ID stored in ctx, or "" if absent.

func JWTAuth

func JWTAuth(opts JWTOptions) func(http.Handler) http.Handler

JWTAuth validates JWT Bearer tokens from the Authorization header. Signature is always verified before claims are parsed to prevent payload manipulation. On success, claims are injected into the request context via GetJWTClaims.

Panics if Algorithms is empty or if the required key material is missing for any listed algorithm.

Example
package main

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"net/http"
	"net/http/httptest"
	"time"

	"github.com/FlavioCFOliveira/MuxMaster/middleware"
)

// makeHS256JWT creates a signed HS256 JWT with the given claims map.
func makeHS256JWT(secret []byte, claims map[string]any) string {
	hdr := base64.RawURLEncoding.EncodeToString([]byte(`{"alg":"HS256","typ":"JWT"}`))
	payload, _ := json.Marshal(claims)
	pay := base64.RawURLEncoding.EncodeToString(payload)
	sigInput := hdr + "." + pay
	mac := hmac.New(sha256.New, secret)
	mac.Write([]byte(sigInput))
	return sigInput + "." + base64.RawURLEncoding.EncodeToString(mac.Sum(nil))
}

func main() {
	// Validate JWT tokens from the Authorization header.
	secret := []byte("test-secret")
	mw := middleware.JWTAuth(middleware.JWTOptions{
		Secret:     secret,
		Algorithms: []string{"HS256"},
	})

	inner := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		claims, ok := middleware.GetJWTClaims(r.Context())
		if ok {
			fmt.Println(claims.Subject)
		}
	})

	// Create a signed HS256 JWT with subject "alice".
	token := makeHS256JWT(secret, map[string]any{
		"sub": "alice",
		"iat": time.Now().Unix(),
		"exp": time.Now().Add(1 * time.Hour).Unix(),
	})

	rec := httptest.NewRecorder()
	req := httptest.NewRequest("GET", "/api/profile", nil)
	req.Header.Set("Authorization", "Bearer "+token)

	mw(inner).ServeHTTP(rec, req)
}
Output:
alice

func Logger

func Logger(out io.Writer) func(http.Handler) http.Handler

Logger logs each request after it completes. Panics if out is nil.

Opt L1: the original implementation used fmt.Fprintf(out, "%s %s %s %d %s\n" + 5 args) which boxes each argument as interface{} (5 allocs) and allocated a fresh *statusRecorder per request (1 alloc that escaped to heap). The new implementation pools the statusRecorder and assembles the log line into a pooled []byte buffer via direct strconv.Append*. Output bytes are identical: the format is "<RFC3339> <method> <path> <status> <duration>\n".

Example

ExampleLogger demonstrates the Logger middleware writing access logs to an io.Writer.

package main

import (
	"bytes"
	"fmt"
	"net/http"
	"net/http/httptest"
	"strings"

	"github.com/FlavioCFOliveira/MuxMaster/middleware"
)

func main() {
	var buf bytes.Buffer
	mw := middleware.Logger(&buf)
	wrapped := mw(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		w.WriteHeader(http.StatusOK)
	}))
	rec := httptest.NewRecorder()
	wrapped.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/", nil))
	if strings.Contains(buf.String(), "GET / 200") {
		fmt.Println("logged ok")
	}
}
Output:
logged ok

func NoCache

func NoCache() func(http.Handler) http.Handler

NoCache sets response headers to prevent caching at every layer: browsers (Cache-Control / Pragma / Expires), CDNs (Surrogate-Control) and nginx-style reverse proxies (X-Accel-Expires). All headers are harmless to deployments that do not run an intermediate cache.

func OAuth2Introspect

func OAuth2Introspect(opts OAuth2Options) func(http.Handler) http.Handler

OAuth2Introspect validates Bearer tokens via RFC 7662 token introspection. Active tokens are cached (keyed by sha256(token)) to avoid per-request network calls. On success, the IntrospectResponse is available via GetOAuth2Claims.

Panics if opts.Endpoint is empty, malformed, or non-HTTPS (unless opts.AllowInsecureEndpoint is true). Bearer tokens transmitted over plaintext are exposed to passive observers (MSR-2026-0067 / RFC 7662 §4).

Example
package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"

	"github.com/FlavioCFOliveira/MuxMaster/middleware"
)

func main() {
	// Validate tokens via RFC 7662 introspection endpoint.
	// In production, use a real OAuth2 introspection endpoint.
	// This example uses a mock server for demonstration.

	mockServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// Echo back a valid introspection response.
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusOK)
		_, _ = w.Write([]byte(`{
			"active": true,
			"sub": "alice",
			"scope": "read write",
			"client_id": "app1"
		}`))
	}))
	defer mockServer.Close()

	mw := middleware.OAuth2Introspect(middleware.OAuth2Options{
		Endpoint:   mockServer.URL,
		CacheTTL:   0, // Disable caching for this example.
		HTTPClient: mockServer.Client(),
	})

	inner := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		resp, ok := middleware.GetOAuth2Claims(r.Context())
		if ok {
			fmt.Println(resp.Subject)
		}
	})

	rec := httptest.NewRecorder()
	req := httptest.NewRequest("GET", "/api/resource", nil)
	req.Header.Set("Authorization", "Bearer test-token-xyz")

	mw(inner).ServeHTTP(rec, req)
}
Output:
alice

func RealIP

func RealIP(trustedCIDRs ...*netip.Prefix) func(http.Handler) http.Handler

RealIP overwrites r.RemoteAddr with the client IP derived from the X-Forwarded-For or X-Real-IP header. Only mutates RemoteAddr when the direct peer is within one of the trusted CIDR prefixes.

XFF selection (MSR-2026-0065): the header is parsed as a comma-separated list and walked from RIGHTMOST toward leftmost, skipping entries that lie inside any trustedCIDRs. The first entry NOT inside a trusted CIDR is the real client IP. This rejects attacker-injected leftmost values: in a multi-hop chain (proxy1 + proxy2), if only proxy2 is trusted, the leftmost-XFF approach would pick a forged `client_ip` injected by the attacker, but the rightmost walk stops at proxy1 (the first untrusted hop) and falls back to its address. When the entire chain consists of trusted CIDRs, the leftmost entry is used as a last resort. With a single trusted proxy stripping inbound XFF (the documented baseline) the behaviour is identical to picking the leftmost.

SECURITY (MSR-2026-0055): calling RealIP() with no CIDRs trusts every peer — any client can spoof the X-Forwarded-For / X-Real-IP header and the router will accept it as the real client IP. This is only safe behind a single trusted proxy that strips inbound XFF; in any other deployment it is a trivial spoofing primitive that defeats ThrottlePerIP and IP-based access controls. The middleware emits a slog.Warn at construction time when called without CIDRs so the misconfiguration is visible in startup logs. ALWAYS pass the proxy CIDR list explicitly in production.

Proxy depth: each additional trusted proxy in the forwarding chain MUST be covered by a trustedCIDRs entry, otherwise the rightmost-walk stops too early and the proxy IP (rather than the real client) becomes RemoteAddr.

IP values are validated via netip.ParseAddr, which rejects CRLF injection and malformed addresses, and IPv6 zone IDs are stripped via WithZone("") (FPE-2026-003 / FPE-2026-003b).

func Recoverer deprecated

func Recoverer() func(http.Handler) http.Handler

Recoverer recovers from panics and writes a 500 response. Logs via slog.Default() — use RecovererWithLogger for a custom logger.

Deprecated: use RecovererWithLogger(slog.Default()) for explicit control.

Example

ExampleRecoverer demonstrates panic recovery.

package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"

	"github.com/FlavioCFOliveira/MuxMaster/middleware"
)

func main() {
	mw := middleware.Recoverer()
	wrapped := mw(http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {
		panic("boom")
	}))
	rec := httptest.NewRecorder()
	wrapped.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/", nil))
	fmt.Println(rec.Code)
}
Output:
500

func RecovererWithLogger

func RecovererWithLogger(logger *slog.Logger) func(http.Handler) http.Handler

RecovererWithLogger recovers from panics, logs the panic value and stack trace at Error level via logger, and writes a plain 500 response. The panic value is never written to the response body, preventing information leakage to clients (MM-2026-0023).

func RequestID

func RequestID() func(http.Handler) http.Handler

RequestID generates or propagates a request ID via X-Request-ID header. Incoming X-Request-ID values are validated; invalid or oversized values are replaced with a freshly generated random ID (MM-2026-0011).

Example

ExampleRequestID demonstrates injecting and reading a request ID.

package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"

	"github.com/FlavioCFOliveira/MuxMaster/middleware"
)

func main() {
	mw := middleware.RequestID()
	wrapped := mw(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if id := middleware.GetRequestID(r.Context()); id != "" {
			w.Header().Set("X-Got-ID", "yes")
		}
		w.WriteHeader(http.StatusOK)
	}))
	rec := httptest.NewRecorder()
	wrapped.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/", nil))
	fmt.Println(rec.Header().Get("X-Got-ID"))
}
Output:
yes

func SetHeader

func SetHeader(key, value string) func(http.Handler) http.Handler

SetHeader sets a fixed response header before calling the next handler. Panics at construction time if key or value contains CR or LF, as those bytes can reach downstream middleware in raw form even though Go's wire serialiser strips them before writing to the network.

ORDERING (MSR-2026-0070): SetHeader runs Header().Set on the response at the START of its frame, BEFORE calling next. Middleware composition in MuxMaster wraps from outermost to innermost — the first Use() call is outermost. So if Use(CORS, SetHeader(...)) is registered, SetHeader runs LAST in the request flow (innermost) and its Set() will OVERWRITE any header CORS already wrote. Specifically, Use(CORS, SetHeader( "Access-Control-Allow-Origin", "*")) silently bypasses the CORS allowed origins whitelist for every request.

To preserve CORS guarantees, register SetHeader BEFORE CORS in the Use() chain (so SetHeader is the outermost frame and CORS overwrites it for the headers it manages), or omit any SetHeader call that targets a CORS-managed header (Access-Control-Allow-Origin, -Methods, -Headers, -Credentials, -Max-Age, -Expose-Headers, Vary).

func StripSlashes

func StripSlashes() func(http.Handler) http.Handler

StripSlashes removes all trailing slashes from r.URL.Path before routing. Idempotent: "/a///" becomes "/a" (not "/a//").

When r.URL.RawPath is set (the original raw form is preserved by net/http only when it differs from the decoded Path), StripSlashes also strips trailing '/' bytes from RawPath. Without this the dispatch path would diverge between Path and RawPath when Mux.UseRawPath is true (HPS-2026-0004).

func ThrottleAllBacklog deprecated

func ThrottleAllBacklog(limit int, backlog int, timeout time.Duration) func(http.Handler) http.Handler

ThrottleAllBacklog is the renamed ThrottleBacklog — limits concurrency globally across ALL clients combined. Use ThrottlePerIP for per-client rate limiting.

Deprecated: Use ThrottleAllBacklog. ThrottleBacklog remains for compatibility.

func ThrottleBacklog

func ThrottleBacklog(limit int, backlog int, timeout time.Duration) func(http.Handler) http.Handler

ThrottleBacklog limits concurrent handler execution with a backlog queue. Panics if limit <= 0 or backlog < 0.

func ThrottlePerIP

func ThrottlePerIP(limit int, timeout time.Duration, keyFn func(*http.Request) string) func(http.Handler) http.Handler

ThrottlePerIP limits concurrent handler executions per client key. keyFn extracts the rate-limit key from the request; if nil, the host part of r.RemoteAddr is used. limit is the maximum concurrent requests per key; timeout is how long a request waits for a slot before receiving 503.

SECURITY (DOS-2026-0002): when keyFn is nil, ThrottlePerIP keys on r.RemoteAddr — which is whatever the TCP peer's address is unless RealIP has previously rewritten it. Behind a load balancer that does not strip the LB's own address from RemoteAddr, every request appears to come from the LB and the per-IP limit degrades to a global rate limit. RealIP (with explicit trusted-proxy CIDRs) MUST be registered BEFORE ThrottlePerIP for per-client limits to be effective. See SECURITY.md "RealIP + ThrottlePerIP ordering".

SECURITY (MSR-2026-0068): the internal per-key table is capped at DefaultThrottlePerIPMaxTableSize. When full, requests for NEW keys (those not already in the table) receive 503 immediately to bound memory under IP-churn attacks. Use ThrottlePerIPCapped to override the cap.

Panics if limit <= 0.

func ThrottlePerIPCapped

func ThrottlePerIPCapped(limit int, timeout time.Duration, maxTableSize int, keyFn func(*http.Request) string) func(http.Handler) http.Handler

ThrottlePerIPCapped is ThrottlePerIP with an explicit cap on the number of distinct keys tracked. maxTableSize <= 0 disables the cap (legacy unbounded behaviour, NOT recommended in production).

SATURATION BEHAVIOUR (TM-2026-013, DOS-2026-0057): when the table reaches maxTableSize and every slot has refs > 0 (i.e. all entries are in active use), new client IPs receive 503 immediately until at least one slot drains. An attacker controlling N >= maxTableSize distinct IPs can sustain this state for as long as their requests stay open. Mitigations:

  • deploy upstream DDoS scrubbing (Cloudflare, AWS Shield, etc.);
  • configure http.Server{ReadHeaderTimeout, IdleTimeout, ReadTimeout} so slow-handler connections cannot hold slots indefinitely;
  • lower maxTableSize for sensitive endpoints.

Panics if limit <= 0.

func Timeout

func Timeout(d time.Duration) func(http.Handler) http.Handler

Timeout applies a context deadline to each request. Panics if d <= 0.

SECURITY (DOS-2026-0003): Timeout cancels the request context after d, but it does NOT preempt the handler goroutine — Go has no preemption primitive for blocked syscalls. Handlers MUST observe ctx.Done() on every blocking call (DB, network, file I/O); a handler that ignores ctx.Done() will run to completion regardless of the timeout, accumulating goroutines under load and exhausting memory or upstream connections. Co-design Timeout with handler-level cooperation (use the *Context variants of the stdlib — sql.DB.QueryContext, net/http with http.Request, etc.).

Example

ExampleTimeout demonstrates wrapping a handler with a request deadline.

package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"
	"time"

	"github.com/FlavioCFOliveira/MuxMaster/middleware"
)

func main() {
	mw := middleware.Timeout(50 * time.Millisecond)
	wrapped := mw(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		w.WriteHeader(http.StatusOK)
	}))
	rec := httptest.NewRecorder()
	wrapped.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/", nil))
	fmt.Println(rec.Code)
}
Output:
200

func WithValue

func WithValue(key, val any) func(http.Handler) http.Handler

WithValue injects a value into the request context. To avoid context key collisions between packages, always use an unexported type as the key:

type ctxKey struct{}
mux.Use(middleware.WithValue(ctxKey{}, myValue))

MSR-2026-0059: passing a string (or any built-in type) as the key is a CWE-1021 cross-package collision risk — any package that uses the same string literal can read or overwrite this value. WithValue emits a slog.Warn at construction time when called with a string-kind key.

Types

type APIKeyOptions

type APIKeyOptions struct {
	// Keys maps raw API key values to identity strings injected into the request context.
	// Panics if nil or empty.
	Keys map[string]string
	// Header is the request header name to read when ExtractFn is nil. Default: "X-API-Key".
	Header string
	// ExtractFn overrides key extraction. If nil, the Header field is used.
	ExtractFn func(*http.Request) string
}

APIKeyOptions configures the APIKey middleware.

type CORSOptions

type CORSOptions struct {
	AllowedOrigins   []string
	AllowedMethods   []string
	AllowedHeaders   []string
	ExposedHeaders   []string
	AllowCredentials bool
	MaxAge           int
}

CORSOptions configures CORS behaviour.

type IntrospectResponse

type IntrospectResponse struct {
	Active    bool
	Subject   string
	Scope     string
	ClientID  string
	Username  string
	TokenType string
	ExpiresAt time.Time
	IssuedAt  time.Time
	NotBefore time.Time
	Issuer    string
	Audience  []string
}

IntrospectResponse holds the RFC 7662 token introspection response fields.

func GetOAuth2Claims

func GetOAuth2Claims(ctx context.Context) (*IntrospectResponse, bool)

GetOAuth2Claims returns the IntrospectResponse injected by the OAuth2Introspect middleware.

type JWTClaims

type JWTClaims struct {
	Subject   string
	Issuer    string
	Audience  []string
	ExpiresAt time.Time
	IssuedAt  time.Time
	NotBefore time.Time
	// RawPayload is the decoded JSON payload bytes, available for extracting custom claims.
	RawPayload []byte
}

JWTClaims holds the standard JWT claims extracted from a validated token. Custom claims can be unmarshalled from RawPayload.

func GetJWTClaims

func GetJWTClaims(ctx context.Context) (*JWTClaims, bool)

GetJWTClaims returns the JWT claims injected by the JWTAuth middleware.

type JWTOptions

type JWTOptions struct {
	// Secret is the HMAC signing key, required for HS256, HS384, HS512.
	Secret []byte
	// PublicKey is the RSA or ECDSA public key, required for RS*/ES* algorithms.
	PublicKey crypto.PublicKey
	// Algorithms lists accepted signing algorithms. Must be non-empty.
	// Supported: HS256, HS384, HS512, RS256, RS384, RS512, ES256, ES384, ES512.
	// SEE the SECURITY note on JWTOptions about mixing families.
	Algorithms []string
	// Issuers, if non-empty, restricts accepted "iss" claim values.
	Issuers []string
	// Audiences, if non-empty, requires at least one "aud" entry to match.
	Audiences []string
	// ClockSkew is the permitted clock drift applied to exp and nbf checks. Default: 0.
	ClockSkew time.Duration
	// RequireExpiry, when true, rejects any token whose payload has no "exp"
	// claim. RFC 8725 §4.4 recommends rejecting tokens without expiry unless
	// there is a compelling reason: a stolen token without "exp" is valid
	// indefinitely (TM-2026-001).
	//
	// SECURITY: production deployments SHOULD set RequireExpiry: true. The
	// default is false ONLY for backward compatibility with code written
	// before this option existed. JWTAuth emits a slog.Warn at construction
	// time when this option is left at the unsafe default so the
	// misconfiguration is visible in startup logs.
	//
	// Default: false (backward compatible — DO NOT use in production).
	RequireExpiry bool
}

JWTOptions configures the JWTAuth middleware.

SECURITY (TSC-2026-0003): mixing algorithm families (HS* with RS* or ES*) in Algorithms leaks the algorithm path via response latency. HMAC verifies in ~1 µs, RSA-2048 verifies in ~300 µs, and an attacker submitting tokens with different alg labels can determine which path the server runs from the response time alone — narrowing the attack surface for algorithm-confusion attacks (RFC 8725 §3.1). Configure each endpoint with a single algorithm family. JWTAuth emits a slog.Warn at construction time when a mixed-family Algorithms list is detected.

type OAuth2Options

type OAuth2Options struct {
	// Endpoint is the RFC 7662 introspection URL. Required.
	// MUST use the https:// scheme — bearer tokens transmitted over plaintext
	// are exposed to passive observers and MITM attackers (RFC 7662 §4 / RFC
	// 6749 §1.6). Construction panics on a non-HTTPS endpoint unless
	// AllowInsecureEndpoint is explicitly set to true (testing/localhost only).
	Endpoint string
	// AllowInsecureEndpoint disables the HTTPS-only enforcement on Endpoint.
	// Set to true ONLY for testing or trusted-local-loopback deployments —
	// production traffic must always use HTTPS. When true, a one-time slog
	// warning is emitted at construction time. Default: false.
	AllowInsecureEndpoint bool
	// ClientID and ClientSecret authenticate to the introspection endpoint via HTTP Basic.
	ClientID     string
	ClientSecret string
	// CacheTTL is how long active tokens are cached. Default: 60s.
	// Set to -1 (or any negative value) to disable caching entirely — every
	// request hits the introspection endpoint, eliminating the cache-poisoning
	// blast radius (MSR-2026-0063) at the cost of higher IDP load. Use
	// disabled caching for high-security endpoints; the singleflight group
	// (DOS-OAUTH2-001 fix) still coalesces concurrent introspection calls
	// for the same token.
	// Cache respects the token's own exp: effective TTL = min(CacheTTL, token.exp - now).
	// Note: caching means revoked tokens remain valid until TTL expires.
	CacheTTL time.Duration
	// MaxCacheSize caps the number of cached active tokens. Default: 10000.
	MaxCacheSize int
	// HTTPClient is used for introspection requests. Default: 10s timeout.
	HTTPClient *http.Client
	// ExtractFn overrides token extraction. Default: "Authorization: Bearer <token>".
	ExtractFn func(*http.Request) string
}

OAuth2Options configures the OAuth2Introspect middleware.

Jump to

Keyboard shortcuts

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