middleware

package
v1.4.0 Latest Latest
Warning

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

Go to latest
Published: Jun 13, 2026 License: MIT Imports: 17 Imported by: 0

Documentation

Overview

Package middleware provides reusable Echo v5 middleware and context value accessors for common patterns.

It includes JWT validation with JWKS support, request ID generation/propagation, request logging, error handling, and typed context accessor functions. Each middleware is optional and can be composed based on application requirements.

Key components:

  • JWT validation via middleware.JWT() with support for Keycloak-style extended claims
  • Request ID tracking via middleware.RequestID()
  • Request logging via middleware.RequestLogger()
  • Centralized error handling via middleware.ErrorHandler()
  • Context getter functions: GetRequestID(), GetToken(), GetExtendedClaimsFromContext(), etc.

Example:

e := echo.New()
e.Use(middleware.RequestID())
e.Use(middleware.RequestLogger(logger))
e.Use(middleware.JWT(jwtConfig))
e.GET("/api/protected", protectedHandler)

All context values use typed keys (ContextKeyTenantID, ContextKeyRequestID, etc.) to prevent collisions.

Example (ErrorHandlerBasic)
e := echo.New()
e.Logger = slog.New(slog.DiscardHandler)

e.HTTPErrorHandler = middleware.ErrorHandler(e.HTTPErrorHandler)

e.GET("/users/:id", func(_ *echo.Context) error {
	return echo.NewHTTPError(http.StatusNotFound, "User not found")
})

go func() {
	_ = e.Start(":9090")
}()
time.Sleep(100 * time.Millisecond)
Example (ErrorHandlerCustomResponse)
e := echo.New()
e.Logger = slog.New(slog.DiscardHandler)

config := &middleware.ErrorHandlerConfig{ //nolint:exhaustruct
	CustomErrorResponse: func(ctx *echo.Context, err error, code int) map[string]any {
		return map[string]any{
			"success": false,
			"error": map[string]any{
				"code":     code,
				messageKey: err.Error(),
			},
			"path":      ctx.Request().URL.Path,
			"timestamp": "2024-01-01T00:00:00Z",
		}
	},
}
e.HTTPErrorHandler = middleware.ErrorHandler(e.HTTPErrorHandler, config)

e.GET("/users/:id", func(_ *echo.Context) error {
	return echo.NewHTTPError(http.StatusNotFound, "User not found")
})

go func() {
	_ = e.Start(":9093")
}()
time.Sleep(100 * time.Millisecond)
Example (ErrorHandlerProduction)
e := echo.New()
e.Logger = slog.New(slog.DiscardHandler)
logger := zerolog.New(os.Stdout).With().Timestamp().Logger()

config := &middleware.ErrorHandlerConfig{ //nolint:exhaustruct
	Logger:                &logger,
	LogErrors:             true,
	IncludeInternalErrors: false,
}
e.HTTPErrorHandler = middleware.ErrorHandler(e.HTTPErrorHandler, config)

e.GET("/users/:id", func(_ *echo.Context) error {
	dbErr := errors.New("database connection timeout") //nolint:err113
	baseErr := echo.NewHTTPError(http.StatusServiceUnavailable, "Service temporarily unavailable")

	return baseErr.Wrap(dbErr)
})

go func() {
	_ = e.Start(":9094")
}()
time.Sleep(100 * time.Millisecond)
Example (ErrorHandlerWithInternalErrors)
e := echo.New()
e.Logger = slog.New(slog.DiscardHandler)
logger := zerolog.New(os.Stdout).With().Timestamp().Logger()

config := &middleware.ErrorHandlerConfig{ //nolint:exhaustruct
	Logger:                &logger,
	LogErrors:             true,
	IncludeInternalErrors: true, // WARNING: Only use in development!
}
e.HTTPErrorHandler = middleware.ErrorHandler(e.HTTPErrorHandler, config)

e.GET("/users/:id", func(_ *echo.Context) error {
	dbErr := errors.New("database connection timeout") //nolint:err113
	baseErr := echo.NewHTTPError(http.StatusServiceUnavailable, "Service temporarily unavailable")

	return baseErr.Wrap(dbErr)
})

go func() {
	_ = e.Start(":9092")
}()
time.Sleep(100 * time.Millisecond)
Example (ErrorHandlerWithLogging)
e := echo.New()
e.Logger = slog.New(slog.DiscardHandler)
logger := zerolog.New(os.Stdout).With().Timestamp().Logger()

config := &middleware.ErrorHandlerConfig{ //nolint:exhaustruct
	Logger:    &logger,
	LogErrors: true,
}
e.HTTPErrorHandler = middleware.ErrorHandler(e.HTTPErrorHandler, config)

e.GET("/users/:id", func(_ *echo.Context) error {
	return echo.NewHTTPError(http.StatusNotFound, "User not found")
})

go func() {
	_ = e.Start(":9091")
}()
time.Sleep(100 * time.Millisecond)

Index

Examples

Constants

View Source
const (
	ContextKeyTenantID      string = "tenantID"
	ContextKeyRateLimitReqs string = "rateLimitRequests"
	ContextKeyRateLimitWin  string = "rateLimitWindow"
	ContextKeyAPIKey        string = "apiKey"
	ContextKeyRequestID     string = "requestID"
	ContextKeyBody          string = "body"
	ContextKeyToken         string = "token"
	ContextKeyClaims        string = "claims"
	ContextKeyHandler       string = "handler"
)
View Source
const (
	HeaderXAPIKey                = "X-Api-Key" //nolint:gosec
	HeaderXRequestID             = "X-Request-ID"
	HeaderXSignature             = "X-Signature"
	HeaderXTimestamp             = "X-Timestamp"
	HeaderXInternalAuthorization = "X-Internal-Authorization"
)
View Source
const (
	HeaderRateLimitLimit     = "X-Ratelimit-Limit"
	HeaderRateLimitRemaining = "X-Ratelimit-Remaining"
	HeaderRateLimitReset     = "X-Ratelimit-Reset"
)
View Source
const DefaultJWTLeeway = 30 * time.Second

DefaultJWTLeeway is the clock-skew tolerance applied to time-based claims (exp, nbf, iat).

Variables

View Source
var (
	ErrInternalTokenRequired   = echo.NewHTTPError(http.StatusUnauthorized, "internal authorization is required")
	ErrInternalTokenInvalid    = echo.NewHTTPError(http.StatusForbidden, "internal authorization is invalid")
	ErrInternalClientForbidden = echo.NewHTTPError(http.StatusForbidden, "internal caller is not allowed")
)
View Source
var (
	ErrTokenRequired   = echo.NewHTTPError(http.StatusUnauthorized, "Authorization header is required")
	ErrJWKSFetchFailed = echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch JWKS")
	ErrInvalidToken    = echo.NewHTTPError(http.StatusUnauthorized, "Invalid token")
)
View Source
var (
	ErrClaimsNotFound            = errors.New("jwt claims: not found in context")
	ErrClaimsTypeAssertionFailed = errors.New("jwt claims: type assertion failed")
	ErrMissingJWTClaims          = errors.New("jwt claims: missing")
	ErrJWTSubjectEmpty           = errors.New("jwt claims: subject is empty")
)
View Source
var (
	ErrRealmRoleForbidden = errors.New("realm role: required role not found")
	ErrRealmRolesEmpty    = errors.New("realm role: no roles configured")
)
View Source
var ErrInternalTokenMintFailed = echo.NewHTTPError(
	http.StatusBadGateway, "failed to mint internal authorization token",
)

Functions

func CurrentUserID added in v1.3.10

func CurrentUserID(c *echo.Context) (string, error)

func DefaultValidMethods added in v1.4.0

func DefaultValidMethods() []string

DefaultValidMethods returns the JWT signing algorithms accepted by default. Only asymmetric algorithms are included: accepting HS* alongside a JWKS keyfunc would open the door to algorithm-confusion attacks where a public key is used as an HMAC secret.

func ErrorHandler

func ErrorHandler(next echo.HTTPErrorHandler, config ...*ErrorHandlerConfig) echo.HTTPErrorHandler

func GetAPIKey

func GetAPIKey(c *echo.Context) string

func GetHandler added in v1.3.0

func GetHandler(c *echo.Context) string

func GetRateLimitRequests

func GetRateLimitRequests(c *echo.Context) int

func GetRateLimitWindow

func GetRateLimitWindow(c *echo.Context) int

func GetRequestID

func GetRequestID(c *echo.Context) string

func GetTenantID

func GetTenantID(c *echo.Context) string

func GetToken

func GetToken(c *echo.Context) string

func InjectInternalToken added in v1.3.21

func InjectInternalToken(provider TokenProvider, header string) echo.MiddlewareFunc

func InjectInternalTokenWithConfig added in v1.3.21

func InjectInternalTokenWithConfig(config InjectInternalTokenConfig) echo.MiddlewareFunc

func InternalAuth added in v1.3.21

func InternalAuth(kf keyfunc.Keyfunc, allowedClients []string) echo.MiddlewareFunc

func InternalAuthWithConfig added in v1.3.21

func InternalAuthWithConfig(config InternalAuthConfig) echo.MiddlewareFunc

func JWT added in v1.3.4

func JWTWithConfig added in v1.3.4

func JWTWithConfig(config JWTConfig) echo.MiddlewareFunc

func RequestID

func RequestID(skipper middleware.Skipper) echo.MiddlewareFunc
Example
e := echo.New()

e.Use(middleware.RequestID(echomiddleware.DefaultSkipper))

e.GET("/api/users", func(c *echo.Context) error {
	requestID := middleware.GetRequestID(c)

	return c.JSON(http.StatusOK, map[string]string{
		requestIDKey: requestID,
		messageKey:   userListMessage,
	})
})

req := httptest.NewRequestWithContext(context.Background(), http.MethodGet, "/api/users", nil)
req.Header.Set(middleware.HeaderXRequestID, uuid.New().String())

rec := httptest.NewRecorder()

e.ServeHTTP(rec, req)

fmt.Println("Status:", rec.Code)
Output:
Status: 200

func RequestIDWithConfig

func RequestIDWithConfig(config RequestIDConfig) echo.MiddlewareFunc
Example (AutoGenerate)
e := echo.New()

config := middleware.RequestIDConfig{
	Skipper:      echomiddleware.DefaultSkipper,
	AutoGenerate: true,
	Generator:    uuid.NewString,
	Validator:    uuid.Validate,
}
e.Use(middleware.RequestIDWithConfig(config))

e.GET("/api/users", func(c *echo.Context) error {
	requestID := middleware.GetRequestID(c)

	return c.JSON(http.StatusOK, map[string]string{
		requestIDKey: requestID,
		messageKey:   userListMessage,
	})
})

req := httptest.NewRequestWithContext(context.Background(), http.MethodGet, "/api/users", nil)
rec := httptest.NewRecorder()

e.ServeHTTP(rec, req)

fmt.Println("Status:", rec.Code)
fmt.Println("Has Request ID in response:", rec.Header().Get(middleware.HeaderXRequestID) != "")
Output:
Status: 200
Has Request ID in response: true
Example (Custom)
e := echo.New()

config := middleware.RequestIDConfig{
	Skipper:      echomiddleware.DefaultSkipper,
	AutoGenerate: true,
	Generator: func() string {
		return fmt.Sprintf("REQ-%d-%s", 1234567890, uuid.New().String()[:8])
	},
	Validator: func(id string) error {
		if len(id) == 0 {
			return errors.New("request ID cannot be empty") //nolint:err113
		}

		return nil
	},
}
e.Use(middleware.RequestIDWithConfig(config))

e.GET("/api/users", func(c *echo.Context) error {
	requestID := middleware.GetRequestID(c)

	return c.JSON(http.StatusOK, map[string]string{
		requestIDKey: requestID,
		messageKey:   userListMessage,
	})
})

req := httptest.NewRequestWithContext(context.Background(), http.MethodGet, "/api/users", nil)
req.Header.Set(middleware.HeaderXRequestID, "CUSTOM-123-ABC")

rec := httptest.NewRecorder()

e.ServeHTTP(rec, req)

fmt.Println("Status:", rec.Code)
Output:
Status: 200

func RequestLogger

func RequestLogger(log zerolog.Logger, extraLogFieldExtractor ...LogFieldExtractor) echo.MiddlewareFunc

func RequireAnyRealmRole added in v1.3.10

func RequireAnyRealmRole(roles ...string) echo.MiddlewareFunc

Types

type ErrorHandlerConfig

type ErrorHandlerConfig struct {
	Logger    *zerolog.Logger
	LogErrors bool
	// IncludeInternalErrors adds the wrapped internal error string to HTTP
	// responses under the "internal" key. It exposes implementation details
	// (driver errors, hostnames, queries) to clients — only enable it in
	// development environments, never in production.
	IncludeInternalErrors bool
	CustomErrorResponse   func(*echo.Context, error, int) map[string]any
}

type ExtendedClaims

type ExtendedClaims struct {
	// Token metadata
	Typ string `json:"typ"`
	Azp string `json:"azp"`
	Sid string `json:"sid"`
	Acr string `json:"acr"`

	// Authorization
	Scope          string         `json:"scope"`
	RealmAccess    RoleAccess     `json:"realm_access"`    //nolint:tagliatelle
	ResourceAccess ResourceAccess `json:"resource_access"` //nolint:tagliatelle
	AllowedOrigins []string       `json:"allowed-origins"` //nolint:tagliatelle

	// User profile
	Name              string `json:"name"`
	PreferredUsername string `json:"preferred_username"` //nolint:tagliatelle
	GivenName         string `json:"given_name"`         //nolint:tagliatelle
	FamilyName        string `json:"family_name"`        //nolint:tagliatelle
	Email             string `json:"email"`
	EmailVerified     bool   `json:"email_verified"` //nolint:tagliatelle

	jwt.RegisteredClaims
}

func GetExtendedClaimsFromContext

func GetExtendedClaimsFromContext(c *echo.Context) (*ExtendedClaims, error)

func (*ExtendedClaims) GetAzp added in v1.3.4

func (c *ExtendedClaims) GetAzp() string

func (*ExtendedClaims) GetRealmRoles added in v1.3.9

func (c *ExtendedClaims) GetRealmRoles() []string

func (*ExtendedClaims) GetResourceRoles added in v1.3.9

func (c *ExtendedClaims) GetResourceRoles(resource string) []string

func (*ExtendedClaims) HasRealmRole added in v1.3.9

func (c *ExtendedClaims) HasRealmRole(role string) bool

func (*ExtendedClaims) HasResourceRole added in v1.3.9

func (c *ExtendedClaims) HasResourceRole(resource, role string) bool

type InjectInternalTokenConfig added in v1.3.21

type InjectInternalTokenConfig struct {
	Skipper  middleware.Skipper
	Provider TokenProvider
	Header   string
}

type InternalAuthConfig added in v1.3.21

type InternalAuthConfig struct {
	Skipper        middleware.Skipper
	Keyfunc        keyfunc.Keyfunc
	Header         string
	AllowedClients []string

	// ValidMethods lists the accepted JWT signing algorithms. Empty means
	// DefaultValidMethods(). Never include HS* methods when keys come from a JWKS.
	ValidMethods []string

	// Issuer, when non-empty, requires the token "iss" claim to match exactly.
	Issuer string

	// Leeway is the clock-skew tolerance for time-based claims.
	// Zero means DefaultJWTLeeway.
	Leeway time.Duration
}

func DefaultInternalAuthConfig added in v1.3.21

func DefaultInternalAuthConfig() InternalAuthConfig

type JWTConfig added in v1.3.4

type JWTConfig struct {
	Skipper       middleware.Skipper
	Logger        *zerolog.Logger
	Keyfunc       keyfunc.Keyfunc
	NewClaimsFunc func(*echo.Context) jwt.Claims
	ContextKey    string
	TokenLookup   string

	// ValidMethods lists the accepted JWT signing algorithms. Empty means
	// DefaultValidMethods(). Never include HS* methods when keys come from a JWKS.
	ValidMethods []string

	// Issuer, when non-empty, requires the token "iss" claim to match exactly.
	// Set this to the realm issuer URL (e.g. https://auth.example.com/realms/my-realm)
	// so tokens minted by other issuers are rejected.
	Issuer string

	// Audiences, when non-empty, requires the token "aud" claim to contain at
	// least one of the listed values. Set this to reject tokens issued for
	// other services in the same realm.
	Audiences []string

	// Leeway is the clock-skew tolerance for time-based claims.
	// Zero means DefaultJWTLeeway; set a negative-free small value (e.g. time.Nanosecond)
	// to effectively disable leeway.
	Leeway time.Duration
}

func DefaultJWTConfig added in v1.3.4

func DefaultJWTConfig() JWTConfig

type LogFieldExtractor

type LogFieldExtractor func(*echo.Context) map[string]any

type RequestIDConfig

type RequestIDConfig struct {
	Skipper      middleware.Skipper
	Generator    func() string
	AutoGenerate bool
	Validator    func(string) error
}

func DefaultRequestIDConfig

func DefaultRequestIDConfig() RequestIDConfig

type ResourceAccess added in v1.3.9

type ResourceAccess map[string]RoleAccess

type RoleAccess added in v1.3.9

type RoleAccess struct {
	Roles []string `json:"roles"`
}

type TokenProvider added in v1.3.21

type TokenProvider interface {
	GetToken(ctx context.Context) (string, error)
}

Jump to

Keyboard shortcuts

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