middleware

package
v0.0.7-2 Latest Latest
Warning

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

Go to latest
Published: Oct 7, 2026 License: MIT Imports: 15 Imported by: 0

README

GoKit Middleware

Middlewares HTTP reutilizables para el ecosistema GoKit: autenticación JWT, autorización por rol, verificación de sesión activa, limitación de tasa (rate limiting), CORS y logging de peticiones. Es la pieza que conecta security/token, security/crypto y logger con cualquier servidor HTTP en Go.

🌟 Características

  • 100% compatible con net/http: todos los middlewares tienen la firma estándar func(http.Handler) http.Handler (tipo Middleware), por lo que funcionan con net/http, chi, gorilla/mux o cualquier router que siga esta convención.
  • RequireAuth: valida el access token Bearer con tu token.JWTManager e inyecta los claims en el contexto de la petición.
  • RequireRole: autorización simple por rol, encadenable después de RequireAuth.
  • RequireActiveSession: revocación real de acceso consultando token.SessionManager, algo que un JWT por sí solo no puede ofrecer.
  • RateLimit: limitación de tasa por clave (IP, usuario, API key...) con una implementación en memoria incluida y una interfaz RateLimiter para conectar tu propio backend (Redis, etc.).
  • CORS: configuración explícita de orígenes, métodos y headers permitidos, con manejo correcto de preflight y de la incompatibilidad "*" + credenciales.
  • RequestLogger: logging estructurado de cada petición (método, ruta, status, duración) usando tu logger favorito.
  • Chain: helper para componer varios middlewares sin anidar paréntesis manualmente.
  • Respuestas de error consistentes: siempre JSON {"error": "..."}, sin filtrar nunca el error interno al cliente.

📦 Instalación

go get github.com/AndresGT/GoKit/security/middleware

Este módulo depende de github.com/AndresGT/GoKit/security/token (JWTManager, Claims, SessionManager). La integración con logger es opcional y se hace mediante una interfaz mínima (Logger), no importando el paquete directamente.

🚀 Inicio rápido

package main

import (
    "net/http"
    "time"

    "github.com/AndresGT/GoKit/logger"
    "github.com/AndresGT/GoKit/security/middleware"
    "github.com/AndresGT/GoKit/security/token"
)

func main() {
    jwtManager, _ := token.NewJWTManager(token.JWTConfig{
        SecretKey: []byte("una-clave-secreta-de-al-menos-32-bytes"),
        Issuer:    "gokit-auth",
    })

    log := logger.New()
    limiter := middleware.NewMemoryRateLimiter(100, time.Minute) // 100 req/min por IP

    mux := http.NewServeMux()
    mux.HandleFunc("/salud", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("ok"))
    })

    authed := middleware.RequireAuth(jwtManager)
    mux.Handle("/perfil", authed(http.HandlerFunc(perfilHandler)))

    handler := middleware.Chain(mux,
        middleware.RequestLogger(log),
        middleware.CORS(middleware.CORSConfig{
            AllowedOrigins: []string{"https://app.example.com"},
        }),
        middleware.RateLimit(limiter, nil),
    )

    http.ListenAndServe(":8080", handler)
}

func perfilHandler(w http.ResponseWriter, r *http.Request) {
    claims, _ := middleware.ClaimsFromContext(r.Context())
    w.Write([]byte("Hola, " + claims.Username))
}

🔐 Autenticación y autorización

RequireAuth

Valida el header Authorization: Bearer <token> contra tu token.JWTManager. Si es válido, inyecta los *token.Claims en el contexto; si no, responde 401 Unauthorized sin ejecutar el handler protegido.

authed := middleware.RequireAuth(jwtManager)
mux.Handle("/perfil", authed(http.HandlerFunc(perfilHandler)))

Rechaza explícitamente cualquier token cuyo TokenType no sea token.TokenTypeAccess — un refresh token nunca se acepta como credencial de acceso.

ClaimsFromContext

Recupera los claims inyectados por RequireAuth dentro de cualquier handler protegido:

func perfilHandler(w http.ResponseWriter, r *http.Request) {
    claims, ok := middleware.ClaimsFromContext(r.Context())
    if !ok {
        http.Error(w, "no autorizado", http.StatusUnauthorized)
        return
    }
    fmt.Fprintf(w, "UserID: %s, Role: %s", claims.UserID, claims.Role)
}
RequireRole

Restringe el acceso según el claim Role. Debe encadenarse después de RequireAuth — depende de los claims que este inyecta. Responde 403 Forbidden si el rol no está permitido, o 401 Unauthorized si RequireAuth no se ejecutó antes.

adminOnly := middleware.RequireRole("admin", "superadmin")
mux.Handle("/admin", authed(adminOnly(http.HandlerFunc(adminHandler))))
RequireActiveSession

Un JWT verificado por firma sigue siendo válido hasta que expira, aunque el usuario haya cerrado sesión o se le haya revocado el acceso. RequireActiveSession cierra esa brecha consultando tu token.SessionManager en cada petición (usando claims.SessionID), a costa de una lectura adicional al SessionStore. Úsalo en las rutas donde la revocación inmediata importe.

activeSession := middleware.RequireActiveSession(sessionManager)
mux.Handle("/perfil", authed(activeSession(http.HandlerFunc(perfilHandler))))

🚦 Limitación de tasa (Rate Limiting)

type RateLimiter interface {
    Allow(key string) bool
}

Incluye MemoryRateLimiter, una implementación en memoria con algoritmo de ventana fija:

limiter := middleware.NewMemoryRateLimiter(60, time.Minute) // 60 peticiones/minuto por clave
handler := middleware.RateLimit(limiter, nil)(mux) // nil → usa la IP remota como clave
  • Por defecto la clave es la IP remota (RemoteIPKeyFunc). Detrás de un proxy/load balancer, r.RemoteAddr es la IP del proxy — provee tu propio KeyFunc que lea X-Forwarded-For/X-Real-IP con la validación adecuada para tu infraestructura.
  • Limitar por usuario autenticado en vez de por IP:
byUser := func(r *http.Request) string {
    if claims, ok := middleware.ClaimsFromContext(r.Context()); ok {
        return claims.UserID
    }
    return middleware.RemoteIPKeyFunc(r)
}
handler := authed(middleware.RateLimit(limiter, byUser)(mux))
  • MemoryRateLimiter es solo para una instancia: en despliegues con varias réplicas, el límite se aplica por réplica, no de forma global. Para un límite compartido, implementa RateLimiter sobre Redis u otro backend centralizado.
  • Llama a limiter.Cleanup() periódicamente: el mapa interno crece con cada clave nueva vista y nunca se reduce solo; sin limpieza periódica es un crecimiento de memoria no acotado en procesos de larga duración.
go func() {
    ticker := time.NewTicker(5 * time.Minute)
    for range ticker.C {
        limiter.Cleanup()
    }
}()

🌐 CORS

cfg := middleware.CORSConfig{
    AllowedOrigins:   []string{"https://app.example.com"},
    AllowedMethods:   []string{"GET", "POST"},   // opcional, por defecto un set razonable
    AllowedHeaders:   []string{"Authorization"}, // opcional
    AllowCredentials: true,
    MaxAge:           600,
}
handler := middleware.CORS(cfg)(mux)
  • Responde directamente a las peticiones preflight (OPTIONS) con 204 No Content, sin llegar al handler siguiente.
  • AllowedOrigins: []string{"*"} junto con AllowCredentials: true no está permitido por la especificación CORS: en ese caso el comodín se ignora y solo se refleja el origen si está en la lista explícita.
  • Coloca CORS antes de RequireAuth en la cadena: las peticiones preflight (OPTIONS) no llevan el header Authorization, así que si RequireAuth se ejecuta primero, el preflight fallaría con 401.

📝 Logging de peticiones

type Logger interface {
    Info(message string)
}

RequestLogger acepta cualquier tipo que implemente esta interfaz mínima — el *logger.Logger de GoKit la cumple directamente, sin necesidad de un adaptador:

log := logger.New()
handler := middleware.RequestLogger(log)(mux)
// 2026-07-19 10:32:15 [INFO] GET /perfil 200 1.2ms

🔗 Componer middlewares con Chain

handler := middleware.Chain(mux,
    middleware.RequestLogger(log),   // se ejecuta primero
    middleware.CORS(corsConfig),
    middleware.RateLimit(limiter, nil),
    // el último de la lista es el más cercano al handler final
)

El primero de la lista es el más externo (se ejecuta primero en la petición, último en la respuesta) — equivalente a anidar mw1(mw2(mw3(handler))) pero más legible.

❗ Errores del paquete

var (
    ErrMissingAuthHeader = errors.New("falta el header Authorization")
    ErrInvalidAuthHeader = errors.New("el header Authorization debe tener el formato 'Bearer <token>'")
)

Todas las respuestas de error de este paquete usan el mismo formato JSON:

{"error": "token inválido o expirado"}

Estos mensajes son deliberadamente genéricos: el paquete nunca expone al cliente el error interno de validación (por ejemplo, la razón exacta por la que un JWT falló su verificación), para no dar pistas útiles a un atacante.

🔒 Notas de seguridad

  • RequireAuth verifica el TokenType del claim, no solo la firma: evita que un refresh token se use como credencial de acceso.
  • RequireActiveSession es la única forma de revocar el acceso de un access token antes de que expire por sí solo; sin este middleware, revocar una sesión no invalida los access tokens ya emitidos hasta que caducan.
  • Encadena RequireRole y RequireActiveSession siempre después de RequireAuth; sin claims en el contexto, ambos responden 401 Unauthorized por defecto (fail-closed, no fail-open).
  • El orden importa: CORS debe ir antes que RequireAuth para que el preflight OPTIONS no requiera autenticación.
  • RateLimit con RemoteIPKeyFunc confía en r.RemoteAddr; detrás de un proxy, valida cuidadosamente qué header de IP usas como clave para que un cliente no pueda falsificarlo y evadir el límite.
  • Ningún middleware de este paquete devuelve detalles internos de error al cliente.

📜 Licencia

MIT

Documentation

Overview

Package middleware provee middlewares HTTP reutilizables para el ecosistema GoKit: autenticación JWT, autorización por rol, verificación de sesión activa, limitación de tasa (rate limiting), CORS y logging de peticiones.

Todos los middlewares siguen la firma estándar de Go (Middleware = func(http.Handler) http.Handler), por lo que son compatibles con net/http y con routers populares como chi, gorilla/mux o Gin (vía adaptador).

Ejemplo combinando varios middlewares con Chain:

handler := middleware.Chain(mux,
    middleware.RequestLogger(log),
    middleware.CORS(corsConfig),
    middleware.RateLimit(limiter, nil),
)
http.ListenAndServe(":8080", handler)

Para proteger una ruta específica con autenticación y roles:

authed := middleware.RequireAuth(jwtManager)
admin := middleware.RequireRole("admin")
mux.Handle("/admin", authed(admin(adminHandler)))

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrMissingAuthHeader se retorna cuando la petición no incluye el
	// header Authorization.
	ErrMissingAuthHeader = errors.New("falta el header Authorization")

	// ErrInvalidAuthHeader se retorna cuando el header Authorization no
	// tiene el formato esperado "Bearer <token>".
	ErrInvalidAuthHeader = errors.New("el header Authorization debe tener el formato 'Bearer <token>'")
)

Functions

func Chain

func Chain(h http.Handler, mws ...Middleware) http.Handler

Chain compone varios middlewares en un único http.Handler. Se aplican en el orden en que se pasan: el primero de la lista es el más externo (se ejecuta primero en la petición, último en la respuesta).

Ejemplo de uso:

handler := middleware.Chain(finalHandler,
    middleware.RequestLogger(log),        // se ejecuta primero
    middleware.CORS(corsConfig),
    middleware.RequireAuth(jwtManager),    // se ejecuta justo antes del handler
)

func ClaimsFromContext

func ClaimsFromContext(ctx context.Context) (*token.Claims, bool)

ClaimsFromContext extrae los claims del JWT validado por RequireAuth desde el contexto de la petición. El segundo valor devuelto es false si RequireAuth no se ejecutó antes en la cadena de middlewares (por ejemplo, si se llama desde una ruta pública).

Ejemplo de uso dentro de un handler protegido por RequireAuth:

func perfilHandler(w http.ResponseWriter, r *http.Request) {
    claims, ok := middleware.ClaimsFromContext(r.Context())
    if !ok {
        http.Error(w, "no autorizado", http.StatusUnauthorized)
        return
    }
    fmt.Fprintf(w, "Hola, %s", claims.Username)
}

func FiberAuth

func FiberAuth() fiber.Handler

FiberAuth es el middleware de autenticación nativo para Fiber

func FiberLogger

func FiberLogger() fiber.Handler

FiberLogger registra peticiones usando el Logger Global de GoKit

func FiberRecovery

func FiberRecovery() fiber.Handler

FiberRecovery captura panics en Fiber

func GinAuth

func GinAuth() gin.HandlerFunc

GinAuth es el middleware de autenticación nativo para Gin. Valida el JWT y guarda los claims tanto en el mapa interno de Gin como en el context.Context estándar de GoKit.

func GinAuthWithManager

func GinAuthWithManager(manager *token.JWTManager) gin.HandlerFunc

GinAuthWithManager permite usar una instancia específica de JWTManager.

func GinRateLimit

func GinRateLimit(limiter RateLimiter, keyFunc KeyFunc) gin.HandlerFunc

func RegisterFiberRoutes

func RegisterFiberRoutes(group fiber.Router, routes []Route[fiber.Handler], opts ...RegisterOption)

func RegisterGinRoutes

func RegisterGinRoutes(group *gin.RouterGroup, routes []Route[gin.HandlerFunc], opts ...RegisterOption)

func RemoteIPKeyFunc

func RemoteIPKeyFunc(r *http.Request) string

RemoteIPKeyFunc usa la IP remota de la conexión TCP como clave de rate limit. Es el KeyFunc por defecto si RateLimit recibe nil.

Advertencia: si tu servicio está detrás de un proxy o balanceador de carga, r.RemoteAddr será la IP del proxy, no la del cliente real, y todas las peticiones compartirán el mismo límite. En ese caso, provee tu propio KeyFunc que lea (y valide) X-Forwarded-For o X-Real-IP según la configuración de tu infraestructura — sin validación, un cliente podría falsificar ese header para evadir el límite o afectar a otros.

func UserIDKeyFunc

func UserIDKeyFunc(req *http.Request) string

Types

type CORSConfig

type CORSConfig struct {
	// AllowedOrigins es la lista de orígenes permitidos (ej.
	// "https://app.example.com"). Usa "*" para permitir cualquier origen —
	// pero ten en cuenta que la especificación CORS prohíbe combinar "*"
	// con AllowCredentials = true; en ese caso, "*" se ignora.
	AllowedOrigins []string

	// AllowedMethods es la lista de métodos HTTP permitidos en la petición
	// real. Si está vacía, se usa un conjunto por defecto razonable
	// (GET, POST, PUT, PATCH, DELETE, OPTIONS).
	AllowedMethods []string

	// AllowedHeaders es la lista de headers que el cliente puede enviar.
	// Si está vacía, se usa un conjunto por defecto (Authorization, Content-Type).
	AllowedHeaders []string

	// AllowCredentials indica si se permite el envío de cookies/credenciales
	// (Access-Control-Allow-Credentials). No es compatible con
	// AllowedOrigins = ["*"] según la especificación CORS.
	AllowCredentials bool

	// MaxAge es cuánto tiempo, en segundos, el navegador puede cachear la
	// respuesta a una petición preflight (OPTIONS). Si es 0, no se envía
	// la cabecera y el navegador usa su valor por defecto.
	MaxAge int
}

CORSConfig define la configuración del middleware CORS.

type KeyFunc

type KeyFunc func(r *http.Request) string

KeyFunc extrae la clave de limitación de tasa a partir de la petición (ej. la IP remota, un userID ya autenticado, una API key del header).

type Logger

type Logger interface {
	Info(message string)
}

Logger es la interfaz mínima que necesita RequestLogger. El *logger.Logger de GoKit (paquete github.com/AndresGT/GoKit/logger) la implementa directamente, ya que su método Info tiene esta misma firma; cualquier otro logger compatible funciona igual. No se importa el paquete logger aquí a propósito, para no forzar esa dependencia en quien no lo use.

type MemoryRateLimiter

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

MemoryRateLimiter implementa RateLimiter en memoria usando el algoritmo de ventana fija (fixed window): permite como máximo 'limit' peticiones por 'window' y por clave.

Adecuado para una sola instancia del servicio. En despliegues con varias instancias/réplicas, el límite se aplica de forma independiente en cada una (no es un límite global) — para eso necesitas un backend compartido (ej. Redis) implementando la interfaz RateLimiter.

func NewMemoryRateLimiter

func NewMemoryRateLimiter(limit int, window time.Duration) *MemoryRateLimiter

NewMemoryRateLimiter crea un limitador de tasa en memoria: como máximo 'limit' peticiones por 'window' y por clave. Si los valores no son válidos, se usan valores por defecto seguros (1 petición / 1 minuto).

func (*MemoryRateLimiter) Allow

func (l *MemoryRateLimiter) Allow(key string) bool

Allow implementa RateLimiter.

func (*MemoryRateLimiter) Cleanup

func (l *MemoryRateLimiter) Cleanup()

Cleanup elimina las claves cuya ventana ya expiró. El mapa interno de MemoryRateLimiter crece con cada clave nueva vista (IP, usuario...) y nunca se reduce por sí solo; en un proceso de larga duración esto es un crecimiento de memoria no acotado. Llama a Cleanup periódicamente (por ejemplo, con un time.Ticker cada pocos minutos) para evitarlo.

Ejemplo de uso:

limiter := middleware.NewMemoryRateLimiter(100, time.Minute)
go func() {
    ticker := time.NewTicker(5 * time.Minute)
    for range ticker.C {
        limiter.Cleanup()
    }
}()

type Middleware

type Middleware func(http.Handler) http.Handler

Middleware es el tipo estándar de middleware HTTP usado en este paquete: una función que envuelve un http.Handler con otro http.Handler. Es compatible con la firma que usan la mayoría de routers de Go.

func CORS

func CORS(cfg CORSConfig) Middleware

CORS aplica las cabeceras CORS según la configuración proporcionada y responde directamente (204 No Content) a las peticiones preflight (OPTIONS), sin llegar al siguiente handler.

Ejemplo de uso:

cfg := middleware.CORSConfig{
    AllowedOrigins:   []string{"https://app.example.com"},
    AllowCredentials: true,
}
handler := middleware.CORS(cfg)(mux)

func RateLimit

func RateLimit(limiter RateLimiter, keyFunc KeyFunc) Middleware

RateLimit limita el número de peticiones permitidas por clave (ver KeyFunc). Responde 429 Too Many Requests cuando se supera el límite.

Si keyFunc es nil, se usa RemoteIPKeyFunc.

Ejemplo de uso:

limiter := middleware.NewMemoryRateLimiter(60, time.Minute) // 60 req/min por IP
handler := middleware.RateLimit(limiter, nil)(mux)

// Limitar por usuario autenticado en vez de por IP:
byUser := func(r *http.Request) string {
    if claims, ok := middleware.ClaimsFromContext(r.Context()); ok {
        return claims.UserID
    }
    return middleware.RemoteIPKeyFunc(r)
}
handler := middleware.RequireAuth(jwtManager)(
    middleware.RateLimit(limiter, byUser)(mux),
)

func RequestLogger

func RequestLogger(log Logger) Middleware

RequestLogger registra cada petición HTTP (método, ruta, status y duración) usando el Logger proporcionado. Si el handler nunca llama explícitamente a WriteHeader, se asume el código 200 por defecto, tal como hace net/http.

Ejemplo de uso:

log := logger.New()
handler := middleware.RequestLogger(log)(mux)

func RequireActiveSession

func RequireActiveSession(sessions *token.SessionManager) Middleware

RequireActiveSession verifica, además de la firma y expiración del JWT, que la sesión asociada (claims.SessionID) siga activa según el SessionManager proporcionado. Responde 401 Unauthorized si la sesión fue revocada, expiró o no existe.

Esto cubre una limitación inherente de los JWT: un access token firmado correctamente sigue siendo válido hasta que expira por sí solo, aunque el usuario haya cerrado sesión, cambiado su contraseña o se le haya revocado el acceso. Encadenar este middleware permite invalidar el acceso de forma inmediata a costa de una consulta adicional al SessionStore en cada petición — una decisión de diseño consciente entre rendimiento y capacidad de revocación instantánea; úsalo en las rutas donde esa garantía importe.

Debe encadenarse DESPUÉS de RequireAuth.

Ejemplo de uso:

authed := middleware.RequireAuth(jwtManager)
activeSession := middleware.RequireActiveSession(sessionManager)
mux.Handle("/perfil", authed(activeSession(http.HandlerFunc(perfilHandler))))

func RequireAuth

func RequireAuth(manager *token.JWTManager) Middleware

RequireAuth valida el access token Bearer del header Authorization usando el JWTManager proporcionado. Si el token es válido, inyecta sus claims en el contexto de la petición (recuperables con ClaimsFromContext) y continúa la cadena; en caso contrario responde 401 Unauthorized sin llegar al siguiente handler.

Rechaza explícitamente los refresh tokens: solo un token cuyo TokenType sea token.TokenTypeAccess se considera una credencial de acceso válida.

Ejemplo de uso:

authed := middleware.RequireAuth(jwtManager)
mux.Handle("/perfil", authed(http.HandlerFunc(perfilHandler)))

func RequireRole

func RequireRole(roles ...string) Middleware

RequireRole restringe el acceso a los usuarios cuyo claim Role esté entre los roles permitidos. Responde 403 Forbidden si el rol no está permitido.

Debe encadenarse DESPUÉS de RequireAuth: depende de los claims que este último inyecta en el contexto. Si RequireAuth no se ejecutó antes, responde 401 Unauthorized.

Ejemplo de uso:

authed := middleware.RequireAuth(jwtManager)
adminOnly := middleware.RequireRole("admin", "superadmin")
mux.Handle("/admin", authed(adminOnly(http.HandlerFunc(adminHandler))))

type RateLimiter

type RateLimiter interface {
	// Allow indica si, en este momento, se permite una nueva petición para
	// la clave indicada (ej. una IP, un userID, una API key).
	Allow(key string) bool
}

RateLimiter define el contrato para backends de limitación de tasa. Permite implementar el algoritmo o backend que prefieras (memoria, Redis, etc.) sin modificar el middleware RateLimit que lo consume.

type RegisterOption

type RegisterOption func(*RegisterOptions)

func WithAuthManager

func WithAuthManager(manager *token.JWTManager) RegisterOption

func WithGroupName

func WithGroupName(name string) RegisterOption

type RegisterOptions

type RegisterOptions struct {
	AuthManager *token.JWTManager
	GroupName   string
}

type Route

type Route[H any] struct {
	Method    string
	Path      string
	Handler   H
	RateLimit int
	Protected bool
}

Jump to

Keyboard shortcuts

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