-
Notifications
You must be signed in to change notification settings - Fork 0
Troubleshooting
That is the feature. The message names every problem:
- GET /admin/users requires scheme "jwt", which is not registered
- DELETE /users/{id} declares roles for scheme "apikey", which cannot enforce them
"not registered" — a typo in RequiredSchemes, or the scheme was never
passed to WithScheme.
"cannot enforce them" — the scheme's validator does not implement
RoleEnforcer / ScopeEnforcer, or its SupportsRoles() returns false.
Implement it on the validator; see Roles and Scopes.
Do not work around this by removing the role declaration. The declaration is correct — the enforcement is missing.
Almost always a pointer receiver on a value registration:
func (r *AdminRoute) RequiredSchemes() []string { … } // pointer receiver
app.RegisterRoute(AdminRoute{Route: rt}) // ← value: assertion fails, route is public
app.RegisterRoute(&AdminRoute{Route: rt}) // ← rightNothing reports it, because "does not implement SecuredRoute" is
indistinguishable from "deliberately public". A
custom RouteValidator with an explicit allowlist of
public paths is the only mechanism that catches this class of mistake.
Check SupportsRoles() on the validator. If it returns false — or the
validator does not implement RoleEnforcer at all — the framework now refuses to
start rather than serving the route to everyone. If it is starting, the route
is not declaring roles for the scheme you think it is: the map key must match a
name in RequiredSchemes.
The client is deliberately told nothing. Look at the log — the specifics go to
MiddlewareConfig.Logger, which the extension sets from r.Logger().
If there is no log line either, something replaced the middleware config with
one whose Logger is nil.
At LogLevelDebug the middleware records which scheme rejected and why.
A nil validator. A scheme with no validator cannot authenticate anything, so
every request to a route requiring it would fail at request time — with a nil
dereference or a permanent 401 depending on the path. Panicking at construction
puts the failure at the line that caused it.
A non-empty allowlist does not implicitly allow the application's own
origin. Browsers send Origin on unsafe same-origin requests too, so add it:
security.WithCSRFPolicy(rextension.OriginPolicy{
AllowedOrigins: []string{"https://app.example.com"}, // ← including your own
})This is invisible in a BFF layout where the SPA is on another port, and bites the moment the two are served together.
-
The cookie is not being sent.
credentials: "include"on the fetch, and the cookie must not beSecureif you are on plain HTTP —WithCSRFInsecureTransport()for local development. -
The header name does not match. Default
X-CSRF-Token. -
A form body with no header. The form field is checked instead; default
csrf_token. -
__Host-prefix without its requirements. That prefix needsSecure, noDomain, andPath=/. Onhttp://localhostthe browser silently refuses to set the cookie.
It attaches only to unsafe methods on routes authenticated by a cookie-based scheme. If your route is one and should be exempt — a webhook receiver authenticated by a signature, say — declare it:
func (r *WebhookRoute) CSRFExempt() bool { return true }Do not reach for WithoutCSRF() unless the whole application serves no
browser-facing cookie session.
SessionStore.Set is probably ignoring expiresAt, or Touch is not
implemented and returns an error. Both compile silently.
If sessions never expire instead, Set is ignoring expiresAt in the other
direction — the store is authoritative for expiry, so a store that does not
honour the deadline has no expiry at all.
Touch is not being called, or it is not extending. ValidateSession refreshes
the idle deadline on every successful validation — that is what makes an idle
timeout an idle timeout.
Note it never pushes past the absolute deadline, so a user working continuously past 12 hours is logged out by design.
By design. RotateSession carries the absolute deadline over rather than
resetting it — resetting would let an attacker with a valid session extend its
lifetime indefinitely by re-authenticating, which is the bound rotation exists
to preserve.
Use GetPrincipalForScheme:
svc, _ := security.GetPrincipalForScheme(r, "serviceToken")
usr, _ := security.GetPrincipalForScheme(r, "session")GetPrincipal returns the primary principal — the first scheme to
authenticate — which is right for a single-scheme route and ambiguous for
anything else.
The generator resolves rextension.SchemeRegistry from the container. If the
security extension is not registered, or is registered but has no schemes, there
is nothing to document.
Check the boot order is not the issue — the registry goes into the container
during security's OnInitialize, and OpenAPI reads it in OnStart.
CookieOptions.AllowInsecureTransport is set. It should be false everywhere
except http://localhost. There is no Secure field — the zero value produces
Secure, so the only way to lose it is to have asked.
They should not any more: the scheme registry is an instance in each
application's container, not a package-level global. If you see cross-talk,
something is still calling the deprecated rextension.RegisterSecuritySchemes.
rextension-security — authentication, authorization and CSRF for Rex · MIT · © 2026 Kryovyx