Skip to content
wiki edited this page Sep 4, 2026 · 1 revision

rextension-security

Authentication, role and scope authorization, server-side sessions and CSRF protection for Rex.

go get github.com/kryovyx/rextension-security
import (
	"github.com/kryovyx/rex"
	security "github.com/kryovyx/rextension-security"
)

app := rex.New(security.WithSecurity(security.NewConfig(
	security.WithScheme(security.NewBearerScheme("jwt",
		security.TokenValidatorFunc(validateJWT))),
	security.WithCSRFPolicy(origins),
)))

Routes declare what must authenticate them:

type AdminRoute struct{ rxroute.Route }

func (r *AdminRoute) RequiredSchemes() []string { return []string{"jwt"} }
func (r *AdminRoute) RequiredRoles() map[string][]string {
	return map[string][]string{"jwt": {"admin"}}
}

A route that declares nothing is public.

What is here

Schemes Bearer, Basic, API key and session-cookie authentication, plus your own
Securing Routes declaring requirements on a route, and reading the principal in a handler
Roles and Scopes per-scheme role and OAuth2 scope enforcement
Sessions server-side sessions with idle and absolute timeouts, rotation on login
CSRF on by default for cookie-authenticated routes; origin check plus double-submit token
Startup Validation a misconfiguration fails the boot, not the request

The stance: fail closed

Three decisions follow from it, and each one fixed a real fail-open:

A requirement that cannot be enforced is a startup failure. A route declaring RequiredRoles: {"jwt": {"admin"}} against a scheme whose validator knows nothing about roles used to be served to every authenticated caller, with no diagnostic anywhere — an endpoint marked "admin only" that was not. Now the application refuses to start. See Startup Validation.

CSRF protection is on by default. An opt-in security control is off in every application whose author did not think about it, and those are exactly the applications that need it. It applies only where it can matter — an unsafe method on a route authenticated by a cookie-based scheme. A Bearer-only API is unaffected. See CSRF.

A scheme with no validator panics at construction. It cannot authenticate anything, so every request to a route requiring it fails — at request time, with a nil dereference or a permanent 401. Panicking at the line that caused it turns that into a startup failure where the mistake is.

Cookies are Secure unless you say otherwise. CookieOptions has an AllowInsecureTransport field rather than a Secure one, so the unsafe choice has to be named. It replaced a Secure bool that defaulted to false — making the unsafe option the one you got by not thinking about it.

What the client is told

A 401 or 403 is a problem document with a generic detail:

{"type":"urn:rex:problem:unauthorized","title":"Unauthorized","status":401,
 "detail":"credentials were not accepted"}

Never the reason. "token expired at 14:02 for subject u_1934" tells an attacker which half of a guess was right, and a validator's error text routinely names another account, an internal host, or the shape of the authorization model. The specifics go to the logger configured in MiddlewareConfig.Logger.

Cooperating with the rest of the ecosystem

The extension registers a SchemeRegistry in the DI container. The OpenAPI generator resolves it through the rextension.SchemeRegistry interface and documents the schemes; Swagger renders an authorization panel from the same data. Neither module imports this one.

That registry used to be a package-level global that replaced rather than appended and had no unregister — so two Rex instances in one process clobbered each other's schemes, and state leaked between tests in the same binary.

Clone this wiki locally