-
Notifications
You must be signed in to change notification settings - Fork 0
Securing Routes
A route declares its requirements by implementing interfaces. A route that implements none is public.
type SecuredRoute interface {
RequiredSchemes() []string // empty or nil → public
}type AdminRoute struct{ rxroute.Route }
func (r *AdminRoute) RequiredSchemes() []string { return []string{"jwt"} }
app.RegisterRoute(&AdminRoute{
Route: rxroute.New("DELETE", "/users/{id}", deleteUser),
})⚠ With a pointer receiver, the route must be registered as a pointer or the type assertion fails and the route is treated as public. This is the single easiest way to accidentally publish an endpoint.
A small helper keeps it terse:
func secured(rt rxroute.Route, schemes ...string) rxroute.Route {
return &securedRoute{Route: rt, schemes: schemes}
}
type securedRoute struct {
rxroute.Route
schemes []string
}
func (r *securedRoute) RequiredSchemes() []string { return r.schemes }
app.RegisterRoute(secured(rxroute.New("DELETE", "/users/{id}", deleteUser), "jwt"))| Interface | Method | Enforced |
|---|---|---|
SecuredRoute |
RequiredSchemes() []string |
every named scheme must authenticate |
RoleGuardedRoute |
RequiredRoles() map[string][]string |
all listed roles must be present |
ScopedSecuredRoute |
RequiredScopes() map[string][]string |
all listed scopes must be present |
CSRFExemptRoute |
CSRFExempt() bool |
opts out of CSRF |
func (r *AdminRoute) RequiredSchemes() []string { return []string{"jwt"} }
func (r *AdminRoute) RequiredRoles() map[string][]string {
return map[string][]string{"jwt": {"admin", "user:write"}} // ALL required
}
func (r *AdminRoute) RequiredScopes() map[string][]string {
return map[string][]string{"jwt": {"users.write"}}
}The map key is a scheme name that must appear in RequiredSchemes. Roles
declared for a scheme the route does not require are a startup error.
At rextension.PriorityAuth (400) — after rate limiting, before CSRF (450) and
validation (500). So a flood of unauthenticated requests is shed before it costs
a token verification, and a body is never parsed for a request that was going to
be refused.
The middleware attaches only to routes that declare requirements.
func deleteUser(ctx rxroute.Context) {
user, ok := security.GetPrincipalAs[*User](ctx.Request())
if !ok {
// unreachable on a secured route, but do not assume
rextension.WriteProblem(ctx.ResponseWriter(), ctx.Request(),
401, rextension.ProblemUnauthorized, "credentials were not accepted")
return
}
…
}| Helper | |
|---|---|
GetPrincipal(r) |
the primary principal, as interface{}
|
GetPrincipalAs[T](r) |
the same, type-asserted |
GetPrincipalForScheme(r, name) |
the principal a specific scheme established |
GetPrincipalForSchemeAs[T](r, name) |
the same, type-asserted |
GetAllPrincipals(r) |
every principal, keyed by scheme name (a copy) |
GetSchemeName(r) |
which scheme authenticated |
The principal is whatever your validator returned. Make it a concrete type you
control, so GetPrincipalAs[*User] is meaningful.
| Situation | Status | Slug |
|---|---|---|
| no credential, or it did not validate |
401 + WWW-Authenticate
|
unauthorized |
| authenticated, but a role or scope is missing | 403 | forbidden |
Both are problem documents with a generic detail. The reason goes to the log, never to the client — a validator's error text routinely names another account, an internal host, or the shape of the authorization model.
Configure the logger, or the specifics are simply lost and the redaction rule becomes unmaintainable in practice:
security.MiddlewareConfig{Logger: r.Logger()}Do not implement SecuredRoute — or return nil:
func (r *HealthRoute) RequiredSchemes() []string { return nil }Both mean public. Prefer not implementing it: an explicit nil reads as an
oversight to the next person.
The extension registers the middleware itself, per route, at
rextension.PriorityAuth. If you are composing a chain by hand — outside Rex,
or in a test — security.SecurityMiddleware(cfg) is the plain net/http form:
mw := security.SecurityMiddleware(security.MiddlewareConfig{
SchemeRegistry: registry,
Logger: logger,
})
handler = mw(handler)It reads the matched route from the request context, so it needs to run inside the Rex router to see one.
rextension-security — authentication, authorization and CSRF for Rex · MIT · © 2026 Kryovyx