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
- Variables
- func AnonymousClaims() map[string]any
- func AuthnFunc(auth Authenticator, mode Mode) authn.AuthFunc
- func ContextWithClaims(ctx context.Context, claims *Claims) context.Context
- func DefaultErrorHandler(w http.ResponseWriter, r *http.Request, err *Error)
- func IsAnonymousAuthn(ctx context.Context) bool
- func Middleware(auth Authenticator, cfg MiddlewareConfig) func(http.Handler) http.Handler
- func ParsePublicKey(pemData string) (crypto.PublicKey, error)
- func SimpleMiddleware(auth Authenticator, mode Mode) func(http.Handler) http.Handler
- func TenantFromContext(ctx context.Context) string
- func TenantFromContextWithKey(ctx context.Context, claimKey string) string
- type Authenticator
- type Claims
- type Config
- type Error
- type ErrorHandler
- type JWKSCache
- type JWKSCacheOption
- type JWKSConfig
- type MetricsRecorder
- type MiddlewareConfig
- type Mode
- type NoopMetrics
- type Option
- type StaticKeyConfig
Constants ¶
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.
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.
const DefaultTenantClaimKey = "tenant"
DefaultTenantClaimKey is the default claim key used for tenant identification.
Variables ¶
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 ¶
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 ¶
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 ¶
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 ¶
ParsePublicKey parses a PEM-encoded public key.
func SimpleMiddleware ¶
SimpleMiddleware returns a simplified middleware with default configuration. For more control, use Middleware with MiddlewareConfig.
func TenantFromContext ¶
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 ¶
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 ¶
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 ¶
ClaimsFromContext retrieves verified JWT claims from the request context. Returns nil if no claims are present (anonymous request or auth disabled).
func (*Claims) Get ¶
Get returns a claim value by name, checking both standard and custom claims. Returns nil if the claim doesn't exist.
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.
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 (*Error) HTTPStatus ¶
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
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 ¶
Close stops background refresh and releases resources. It is safe to call Close multiple times.
func (*JWKSCache) ForceRefresh ¶
ForceRefresh triggers an immediate refresh if min interval has passed.
func (*JWKSCache) LastRefresh ¶
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 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.