jwt

package
v0.0.0-...-1a28f28 Latest Latest
Warning

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

Go to latest
Published: Jan 22, 2026 License: MIT Imports: 24 Imported by: 0

Documentation

Overview

Package jwt provides reusable JWT authentication middleware for HTTP services.

This package offers a composable authentication system that supports:

  • JWT validation with JWKS (JSON Web Key Set) or static keys
  • Multiple authentication modes (disabled, optional, required)
  • OIDC discovery for automatic key endpoint resolution
  • Background key refresh with configurable intervals
  • Pluggable metrics and observability hooks
  • Integration with connectrpc/authn-go for idiomatic Connect authentication

Basic Usage

Create an authenticator with configuration:

cfg := &jwt.Config{
	Mode: "required",
	JWKS: &jwt.JWKSConfig{
		URL: "https://auth.example.com/.well-known/jwks.json",
	},
	Issuers:   []string{"https://auth.example.com"},
	Audiences: []string{"my-service"},
}

authenticator, err := jwt.NewAuthenticator(cfg)
if err != nil {
	log.Fatal(err)
}
defer authenticator.Close()

Use the middleware to protect HTTP handlers:

handler := jwt.Middleware(authenticator, jwt.MiddlewareConfig{
	Mode: jwt.ModeRequired,
})(yourHandler)

ConnectRPC Integration (authn-go)

For ConnectRPC services, use AuthnFunc to create an authn.AuthFunc that wraps the JWT authenticator. This enables idiomatic authentication at the HTTP layer before request deserialization:

authenticator, _ := jwt.NewAuthenticator(cfg)
authFunc := jwt.AuthnFunc(authenticator, jwt.ModeRequired)
middleware := authn.NewMiddleware(authFunc)
handler := middleware.Wrap(mux)

Retrieve claims in handlers or interceptors:

claims := jwt.ClaimsFromAuthn(ctx)
if claims != nil {
	fmt.Println("User:", claims.Subject)
}

Check for anonymous access:

if jwt.IsAnonymousAuthn(ctx) {
	// Handle anonymous request
}

Benefits of authn-go integration:

  • Authentication before request deserialization (more efficient rejections)
  • Proper Connect error format with metadata (WWW-Authenticate, X-Auth-Error)
  • Context propagation via authn.GetInfo/SetInfo
  • Consistent with ConnectRPC ecosystem patterns

Static Keys (Development/Testing)

For development or air-gapped environments, use static keys:

cfg := &jwt.Config{
	Mode: "required",
	StaticKeys: []jwt.StaticKeyConfig{{
		KeyID:     "dev-key",
		Algorithm: "RS256",
		PublicKey: "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----",
	}},
}

Metrics Integration

Provide custom metrics recording via options:

authenticator, err := jwt.NewAuthenticator(cfg,
	jwt.WithMetrics(myMetricsRecorder),
)

OIDC Discovery

Auto-discover JWKS endpoint from issuer:

cfg := &jwt.Config{
	JWKS: &jwt.JWKSConfig{
		URL:           "https://auth.example.com",
		OIDCDiscovery: true,
	},
}

Security

This package enforces security-first defaults:

  • Only asymmetric algorithms are supported (RS256, ES256, EdDSA, PS256, etc.)
  • Symmetric algorithms (HS256, etc.) are intentionally rejected
  • Maximum token size is enforced (default 16KB) to prevent DoS
  • Clock skew is limited to 5 minutes maximum
  • JWKS refresh is rate-limited (minimum 5 minutes between refreshes)

Always use HTTPS in production to protect tokens in transit.

This package is used by Deputy's proxy server, gRPC server, and MCP server for consistent authentication across all HTTP-based services.

Index

Constants

View Source
const (
	// DefaultMaxTokenSize is the default maximum JWT token size (16KB).
	DefaultMaxTokenSize = 16 * 1024
	// MaxClockSkew is the maximum allowed clock skew (5 minutes).
	MaxClockSkew = 5 * time.Minute
)

Constants for configuration limits.

View Source
const (
	// CodeMissingToken indicates no Authorization header was provided.
	CodeMissingToken = "missing_token"
	// CodeInvalidToken indicates the token could not be parsed or is malformed.
	CodeInvalidToken = "invalid_token"
	// CodeExpiredToken indicates the token's exp claim is in the past.
	CodeExpiredToken = "expired_token"
	// CodeInvalidIssuer indicates the token's iss claim is not in the allowed list.
	CodeInvalidIssuer = "invalid_issuer"
	// CodeInvalidAudience indicates the token's aud claim is not in the allowed list.
	CodeInvalidAudience = "invalid_audience"
	// CodeMissingClaim indicates a required claim is not present in the token.
	CodeMissingClaim = "missing_claim"
	// CodeKeyNotFound indicates the signing key (kid) was not found in JWKS or static keys.
	CodeKeyNotFound = "key_not_found"
	// CodeSignatureInvalid indicates the token signature verification failed.
	CodeSignatureInvalid = "signature_invalid"
)

Error codes for authentication failures. These are machine-readable codes that can be used for metrics, logging, and programmatic error handling.

View Source
const DefaultTenantClaimKey = "tenant"

DefaultTenantClaimKey is the default claim key used for tenant identification.

Variables

View Source
var DefaultAllowedAlgorithms = []string{
	"RS256", "RS384", "RS512",
	"ES256", "ES384", "ES512",
	"EdDSA",
	"PS256", "PS384", "PS512",
}

DefaultAllowedAlgorithms are the signing algorithms allowed by default. These are all asymmetric algorithms; symmetric algorithms (HS256, etc.) are excluded for security since they require shared secrets.

Functions

func AnonymousClaims

func AnonymousClaims() map[string]any

AnonymousClaims returns a claims map indicating anonymous access. Use this when no token was provided and anonymous access is allowed.

func AuthnFunc

func AuthnFunc(auth Authenticator, mode Mode) authn.AuthFunc

AuthnFunc creates an authn.AuthFunc that wraps the given Authenticator. This enables using Deputy's JWT authenticator with connectrpc/authn-go middleware.

The returned AuthFunc returns *Claims as the authentication info, which can be retrieved downstream via authn.GetInfo(ctx).(*jwt.Claims).

When mode is ModeRequired, missing tokens return authn.Errorf (connect.CodeUnauthenticated). When mode is ModeOptional, missing tokens return (nil, nil) for anonymous access. When mode is ModeDisabled, all requests pass through with nil info.

Example usage with authn.NewMiddleware:

authenticator, _ := jwt.NewAuthenticator(cfg)
authFunc := jwt.AuthnFunc(authenticator, jwt.ModeRequired)
middleware := authn.NewMiddleware(authFunc, handlerOptions...)
handler := middleware.Wrap(mux)

func ContextWithClaims

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

ContextWithClaims returns a new context with the given claims.

func DefaultErrorHandler

func DefaultErrorHandler(w http.ResponseWriter, r *http.Request, err *Error)

DefaultErrorHandler is the default error handler for authentication failures.

func IsAnonymousAuthn

func IsAnonymousAuthn(ctx context.Context) bool

IsAnonymousAuthn returns true if the request has no authentication info. Use this to check for anonymous access when using authn middleware.

func Middleware

func Middleware(auth Authenticator, cfg MiddlewareConfig) func(http.Handler) http.Handler

Middleware returns HTTP middleware that validates JWT tokens. It should be placed early in the middleware chain for proper correlation.

When auth is nil or mode is ModeDisabled, the middleware passes through without modification.

func ParsePublicKey

func ParsePublicKey(pemData string) (crypto.PublicKey, error)

ParsePublicKey parses a PEM-encoded public key.

func SimpleMiddleware

func SimpleMiddleware(auth Authenticator, mode Mode) func(http.Handler) http.Handler

SimpleMiddleware returns a simplified middleware with default configuration. For more control, use Middleware with MiddlewareConfig.

func TenantFromContext

func TenantFromContext(ctx context.Context) string

TenantFromContext extracts the tenant identifier from JWT claims in the context. Returns empty string if no tenant claim is present or if the request is anonymous. Uses the default claim key "tenant".

func TenantFromContextWithKey

func TenantFromContextWithKey(ctx context.Context, claimKey string) string

TenantFromContextWithKey extracts a tenant using a custom claim key. Returns empty string if:

  • ctx is nil
  • no claims are present in context (anonymous request)
  • the specified claim key doesn't exist
  • the claim value cannot be converted to a string

Types

type Authenticator

type Authenticator interface {
	// Authenticate validates the token and returns claims.
	// Returns nil claims and nil error for valid anonymous access (no token provided).
	// Returns *Error for authentication failures.
	Authenticate(ctx context.Context, r *http.Request) (*Claims, error)

	// Close releases any resources held by the authenticator.
	Close() error
}

Authenticator validates JWT tokens and extracts claims.

func NewAuthenticator

func NewAuthenticator(cfg *Config, opts ...Option) (Authenticator, error)

NewAuthenticator creates a new JWT authenticator from the given configuration.

type Claims

type Claims struct {
	// Standard claims (RFC 7519)
	Subject   string   `json:"sub,omitempty"`
	Issuer    string   `json:"iss,omitempty"`
	Audience  []string `json:"aud,omitempty"`
	ExpiresAt int64    `json:"exp,omitempty"`
	IssuedAt  int64    `json:"iat,omitempty"`
	NotBefore int64    `json:"nbf,omitempty"`
	JWTID     string   `json:"jti,omitempty"`

	// Custom claims (all other claims from the token)
	Custom map[string]any `json:"-"`
}

Claims represents verified JWT claims exposed to applications.

func ClaimsFromAuthn

func ClaimsFromAuthn(ctx context.Context) *Claims

ClaimsFromAuthn retrieves JWT claims from a context populated by authn middleware. This is a convenience wrapper around authn.GetInfo that handles type assertion.

Returns nil if:

  • No auth info is present (anonymous request)
  • Auth info is not *Claims (different auth provider)
  • Auth is disabled

func ClaimsFromContext

func ClaimsFromContext(ctx context.Context) *Claims

ClaimsFromContext retrieves verified JWT claims from the request context. Returns nil if no claims are present (anonymous request or auth disabled).

func (*Claims) Get

func (c *Claims) Get(name string) any

Get returns a claim value by name, checking both standard and custom claims. Returns nil if the claim doesn't exist.

func (*Claims) Has

func (c *Claims) Has(name string) bool

Has checks if a claim exists (standard or custom).

func (*Claims) ToMap

func (c *Claims) ToMap() map[string]any

ToMap converts claims to a map suitable for CEL evaluation or JSON serialization. The returned map includes an "anonymous" field set to false.

type Config

type Config struct {
	// Mode determines how authentication is enforced.
	// - "required": requests without valid tokens are rejected (401)
	// - "optional": tokens are validated if present, anonymous access allowed
	// - "disabled": no authentication (default for backward compatibility)
	Mode string `yaml:"mode,omitempty"`

	// JWKS configures JSON Web Key Set endpoints for key discovery.
	JWKS *JWKSConfig `yaml:"jwks,omitempty"`

	// StaticKeys provides inline public keys for validation.
	// Useful for development, testing, or air-gapped environments.
	StaticKeys []StaticKeyConfig `yaml:"static_keys,omitempty"`

	// Issuers lists trusted token issuers (iss claim).
	// If empty, issuer validation is skipped.
	Issuers []string `yaml:"issuers,omitempty"`

	// Audiences lists expected audiences (aud claim).
	// If empty, audience validation is skipped.
	Audiences []string `yaml:"audiences,omitempty"`

	// RequiredClaims specifies claims that must be present in tokens.
	RequiredClaims []string `yaml:"required_claims,omitempty"`

	// ClockSkew allows for clock drift when validating exp/nbf/iat.
	// Defaults to 0 (no skew allowed). Maximum 5 minutes.
	ClockSkew time.Duration `yaml:"clock_skew,omitempty"`

	// AllowedAlgorithms restricts accepted signing algorithms.
	// If empty, defaults to secure asymmetric algorithms (RS256, ES256, EdDSA, etc).
	// Use this to reject weaker or unwanted algorithms.
	AllowedAlgorithms []string `yaml:"allowed_algorithms,omitempty"`

	// MaxTokenSize limits the maximum size of JWT tokens in bytes.
	// Defaults to 16KB. Use this to prevent DoS via oversized tokens.
	MaxTokenSize int `yaml:"max_token_size,omitempty"`
}

Config defines authentication settings.

func (*Config) GetMode

func (c *Config) GetMode() Mode

GetMode returns the auth mode, defaulting to disabled.

func (*Config) Validate

func (c *Config) Validate() error

Validate checks the configuration for errors.

type Error

type Error struct {
	// Code is a machine-readable error code from the Code* constants.
	Code string
	// Message is a human-readable description of the error.
	Message string
	// Cause is the underlying error, if any.
	Cause error
}

Error represents an authentication or authorization failure. It contains both a machine-readable Code and a human-readable Message.

func NewError

func NewError(code, message string) *Error

NewError creates a new Error with the given code and message.

func WrapError

func WrapError(code, message string, cause error) *Error

WrapError creates a new Error that wraps an underlying error.

func (*Error) Error

func (e *Error) Error() string

Error implements the error interface.

func (*Error) HTTPStatus

func (e *Error) HTTPStatus() int

HTTPStatus returns the appropriate HTTP status code for the error.

Authentication failures (identity unknown/unverifiable) return 401 Unauthorized:

  • missing_token, invalid_token, expired_token, signature_invalid, key_not_found

Authorization failures (identity known but insufficient) return 403 Forbidden:

  • invalid_issuer, invalid_audience, missing_claim

func (*Error) Unwrap

func (e *Error) Unwrap() error

Unwrap returns the underlying error for use with errors.Is/As.

type ErrorHandler

type ErrorHandler func(w http.ResponseWriter, r *http.Request, err *Error)

ErrorHandler handles authentication errors and writes the HTTP response.

type JWKSCache

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

JWKSCache manages JWKS key sets with background refresh.

func NewJWKSCache

func NewJWKSCache(cfg *JWKSConfig, opts ...JWKSCacheOption) (*JWKSCache, error)

NewJWKSCache creates a new JWKS cache with the given configuration.

func (*JWKSCache) Close

func (c *JWKSCache) Close() error

Close stops background refresh and releases resources. It is safe to call Close multiple times.

func (*JWKSCache) ForceRefresh

func (c *JWKSCache) ForceRefresh(ctx context.Context) error

ForceRefresh triggers an immediate refresh if min interval has passed.

func (*JWKSCache) GetKey

func (c *JWKSCache) GetKey(ctx context.Context, kid string) (crypto.PublicKey, error)

GetKey returns the public key for the given key ID.

func (*JWKSCache) LastError

func (c *JWKSCache) LastError() error

LastError returns the last error from a refresh attempt, if any.

func (*JWKSCache) LastRefresh

func (c *JWKSCache) LastRefresh() time.Time

LastRefresh returns the time of the last successful refresh.

type JWKSCacheOption

type JWKSCacheOption func(*JWKSCache)

JWKSCacheOption configures a JWKSCache.

func WithJWKSHTTPClient

func WithJWKSHTTPClient(client *http.Client) JWKSCacheOption

WithJWKSHTTPClient sets a custom HTTP client for the JWKS cache.

func WithJWKSMetrics

func WithJWKSMetrics(m MetricsRecorder) JWKSCacheOption

WithJWKSMetrics sets the metrics recorder for the JWKS cache.

type JWKSConfig

type JWKSConfig struct {
	// URL is the JWKS endpoint (e.g., https://issuer/.well-known/jwks.json).
	URL string `yaml:"url"`

	// OIDCDiscovery enables OIDC discovery from issuer URL.
	// When true, URL should be the issuer URL; JWKS URI is auto-discovered.
	OIDCDiscovery bool `yaml:"oidc_discovery,omitempty"`

	// RefreshInterval controls background JWKS refresh (default: 1h).
	RefreshInterval time.Duration `yaml:"refresh_interval,omitempty"`

	// CacheDuration controls how long keys are cached (default: 24h).
	// Deprecated: Use RefreshInterval instead.
	CacheDuration time.Duration `yaml:"cache_duration,omitempty"`
}

JWKSConfig configures JWKS endpoint discovery.

type MetricsRecorder

type MetricsRecorder interface {
	// RecordSuccess records a successful authentication.
	RecordSuccess()
	// RecordAnonymous records an anonymous request (no token, allowed).
	RecordAnonymous()
	// RecordError records an authentication error by error code.
	RecordError(code string)
	// RecordJWKSRefresh records a JWKS refresh attempt result.
	RecordJWKSRefresh(success bool)
	// RecordJWKSKeyLookup records a JWKS key lookup attempt.
	RecordJWKSKeyLookup(found bool)
}

MetricsRecorder defines the interface for recording authentication metrics. Implement this interface to integrate with your preferred metrics system (expvar, Prometheus, OpenTelemetry, etc.).

type MiddlewareConfig

type MiddlewareConfig struct {
	// Mode determines how authentication is enforced.
	Mode Mode

	// Metrics records authentication metrics.
	Metrics MetricsRecorder

	// ErrorHandler handles authentication errors.
	// If nil, DefaultErrorHandler is used.
	ErrorHandler ErrorHandler

	// OnSuccess is called after successful authentication.
	// Use this for custom OTel span events or additional logging.
	OnSuccess func(ctx context.Context, claims *Claims)

	// OnAnonymous is called when no token is provided (and mode is optional).
	OnAnonymous func(ctx context.Context)

	// OnRejected is called when authentication fails.
	OnRejected func(ctx context.Context, code string)

	// RequestIDFunc extracts a request ID from context for logging.
	// If nil, no request ID is logged.
	RequestIDFunc func(ctx context.Context) string
}

MiddlewareConfig configures the authentication middleware.

type Mode

type Mode string

Mode defines how authentication is enforced.

const (
	// ModeDisabled disables authentication entirely (default).
	ModeDisabled Mode = "disabled"
	// ModeOptional validates tokens if present but allows anonymous access.
	ModeOptional Mode = "optional"
	// ModeRequired rejects requests without valid tokens.
	ModeRequired Mode = "required"
)

type NoopMetrics

type NoopMetrics struct{}

NoopMetrics is a no-op implementation of MetricsRecorder. Use this as a default when metrics are not needed.

func (NoopMetrics) RecordAnonymous

func (NoopMetrics) RecordAnonymous()

func (NoopMetrics) RecordError

func (NoopMetrics) RecordError(string)

func (NoopMetrics) RecordJWKSKeyLookup

func (NoopMetrics) RecordJWKSKeyLookup(bool)

func (NoopMetrics) RecordJWKSRefresh

func (NoopMetrics) RecordJWKSRefresh(bool)

func (NoopMetrics) RecordSuccess

func (NoopMetrics) RecordSuccess()

type Option

type Option func(*authenticator)

Option configures an Authenticator.

func WithJWKSCacheOptions

func WithJWKSCacheOptions(opts ...JWKSCacheOption) Option

WithJWKSCacheOptions sets additional options for the JWKS cache. This is primarily useful for testing to inject a custom HTTP client.

func WithMetrics

func WithMetrics(m MetricsRecorder) Option

WithMetrics sets the metrics recorder for the authenticator.

type StaticKeyConfig

type StaticKeyConfig struct {
	// KeyID is the key identifier (matches JWT header "kid").
	KeyID string `yaml:"kid"`

	// Algorithm specifies the signing algorithm (e.g., RS256, ES256, EdDSA).
	Algorithm string `yaml:"alg"`

	// PublicKey is the PEM-encoded public key.
	PublicKey string `yaml:"public_key"`
}

StaticKeyConfig defines an inline public key.

Jump to

Keyboard shortcuts

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