middleware

package
v1.3.12 Latest Latest
Warning

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

Go to latest
Published: Apr 17, 2026 License: MIT Imports: 15 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.HTTPErrorHandler = middleware.ErrorHandler(e.HTTPErrorHandler)

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

_ = e.Start(":8080")
Example (ErrorHandlerCustomResponse)
e := echo.New()

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,
				"message": 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")
})

_ = e.Start(":8080")
Example (ErrorHandlerProduction)
e := echo.New()
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)
})

_ = e.Start(":8080")
Example (ErrorHandlerWithInternalErrors)
e := echo.New()
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)
})

_ = e.Start(":8080")
Example (ErrorHandlerWithLogging)
e := echo.New()
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")
})

_ = e.Start(":8080")

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"
)
View Source
const (
	HeaderRateLimitLimit     = "X-Ratelimit-Limit"
	HeaderRateLimitRemaining = "X-Ratelimit-Remaining"
	HeaderRateLimitReset     = "X-Ratelimit-Reset"
)

Variables

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")
)

Functions

func CurrentUserID added in v1.3.10

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

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 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{
		"request_id": requestID,
		"message":    "User list",
	})
})

req := httptest.NewRequest(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{
		"request_id": requestID,
		"message":    "User list",
	})
})

req := httptest.NewRequest(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{
		"request_id": requestID,
		"message":    "User list",
	})
})

req := httptest.NewRequest(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 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 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
}

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"`
}

Jump to

Keyboard shortcuts

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