middleware

package
v1.3.21 Latest Latest
Warning

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

Go to latest
Published: Jun 10, 2026 License: MIT Imports: 16 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"
	HeaderXInternalAuthorization = "X-Internal-Authorization"
)
View Source
const (
	HeaderRateLimitLimit     = "X-Ratelimit-Limit"
	HeaderRateLimitRemaining = "X-Ratelimit-Remaining"
	HeaderRateLimitReset     = "X-Ratelimit-Reset"
)

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

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
}

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