Kyrux
Back

Middleware

Global middlewares (applied to every route)

Registered in bootstrap — already enabled by default:

Middleware Description
Recovery() Catches panics — shows the debug page (dev) or 500 (prod)
MaxBodySize(32MB) Limits body size — defense against giant payloads
SecureHeaders HSTS, X-Frame-Options, CSP (configurable — see below) — production only (APP_ENV=development doesn't enable it)
AllowedHosts() Blocks unauthorized hosts (ignored in dev)
csrf.Middleware Validates the CSRF token on unsafe methods

Optional middlewares

Middleware Usage Description
Compress r.Use(compress.Compress) Gzip compression of responses
CORS(origins) r.Use(secmiddleware.CORS(...)) CORS headers for allowed origins
RequireAuth(a) per route or global Requires a Bearer JWT token — stateless APIs
RequireLogin(store, url) per route or global Requires an active session — SSR views; redirects to url if not authenticated
RateLimit(max, window) per route (recommended on login) Maximum of max requests per IP per window; excess requests get a 429
LocalhostOnly per route (e.g. internal dashboards) 403 for any IP that isn't loopback — this is what protects /kyrux/debug/
MaxBodySize(n) per route, in addition to the global 32MB A smaller (or larger) limit specific to a route (e.g. upload)
import (
    "kyrux/core/compress"
    secmiddleware "kyrux/core/security/middleware"
)

// Gzip compression
r.Use(compress.Compress)

// CORS
r.Use(secmiddleware.CORS([]string{"https://mysite.com"}))

// Require an active session on SSR views — redirects to /login/ if not authenticated
r.Use(secmiddleware.RequireLogin(fw.Sessions, "/login/"))

// Require JWT on API routes
r.Use(secmiddleware.RequireAuth(fw.Auth))

// Rate limit per route — essential on login (Argon2 is expensive by design):
router.Path("POST", "/login/",
    secmiddleware.RateLimit(10, time.Minute)(views.LoginPost(fw)), "login_post"),

Custom middleware

func LogMiddleware(next router.HandlerFunc) router.HandlerFunc {
    return func(ctx *router.Context) {
        log.Printf("%s %s", ctx.Request.Method, ctx.Request.URL.Path)
        next(ctx)
    }
}

// Apply globally
r.Use(LogMiddleware)

Per-route middleware (via wrapper)

func Register(r *router.Router) {
    // Session-protected SSR route
    loginRequired := secmiddleware.RequireLogin(fw.Sessions, "/login/")
    r.Handle("GET /dashboard/", loginRequired(func(ctx *router.Context) {
        render.For("painel").Render(ctx, "dashboard.html", nil)
    }))

    // JWT-protected API route
    jwtRequired := secmiddleware.RequireAuth(fw.Auth)
    r.Handle("GET /api/perfil/", jwtRequired(func(ctx *router.Context) {
        claims := ctx.Get("claims").(*auth.Claims)
        ctx.JSON(200, map[string]string{"user_id": claims.UserID})
    }))
}

Configurable Content-Security-Policy (CSP)

The CSP sent by SecureHeaders in production has a strict default (secmiddleware.DefaultCSP: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:) — configurable at two levels, without touching core/:

Global — swaps the policy for the whole site, normally from CSP_POLICY in .env (bootstrap already calls this for you):

secmiddleware.SetCSP(cfg.Security.CSPPolicy) // an empty policy keeps DefaultCSP

Per route — overrides just one page that needs a one-off exception (a third-party script/iframe only there — e.g. an embedded map or a captcha provider), without loosening the rest of the site's policy:

mapCSP := "default-src 'self'; frame-src https://www.google.com/maps/"

router.Path("GET", "/contact/",
    secmiddleware.CSPOverride(mapCSP)(views.ContactView(fw)), "contact"),

This works because per-route middleware runs after the global one (SecureHeaders included) in the chain — the Set in CSPOverride replaces, never adds to, the header SecureHeaders already wrote. It also works with SecureHeaders off (development): it just adds one extra header, harmless.