-
Notifications
You must be signed in to change notification settings - Fork 0
Security Contracts
These interfaces let the security extension and the OpenAPI extension cooperate
without either importing the other. rextension declares the shapes; the
security extension implements them; OpenAPI, Swagger and the router read them.
The minimum a scheme exposes so it can be documented and challenged with.
type SecuritySchemeAccessor interface {
Name() string // unique id — "bearer", "basic", "api-key"
Type() string // OpenAPI type — "http", "apiKey"
Description() string // human-readable
Challenge() string // WWW-Authenticate value — "Bearer"
}A scheme implements these only when they apply. Consumers type-assert.
// Carried in a named request parameter: an API key in a header, query or cookie.
type ParameterizedScheme interface {
ParamName() string // "X-API-Key"
Location() string // "header" | "query" | "cookie"
}
// A bearer scheme that documents its token format.
type BearerFormatProvider interface {
BearerFormat() string // "JWT"
}
// A scheme that documents which token claim carries the caller's roles.
type RoleClaimProvider interface {
RolesClaim() string // "" when the scheme exposes no roles
}if p, ok := scheme.(rextension.ParameterizedScheme); ok {
doc.In = p.Location()
doc.Name = p.ParamName()
}
Location()returns a plainstringon purpose. It used to return a named type (security.APIKeyLocation), which is why the OpenAPI generator reached it withreflect.MethodByName("Location")and formatted the result with%s— a named type cannot satisfy an interface declaringLocation() string. Returningstringmakes the interface expressible and deletes the reflection.
How a route declares what must authenticate it.
type SecuredRouteAccessor interface {
RequiredSchemes() []string // empty or nil means public
}type AdminRoute struct{ rxroute.Route }
func (r *AdminRoute) RequiredSchemes() []string { return []string{"bearer"} }Read by the security middleware to decide what to enforce, by OpenAPI to emit
the security block, and by Swagger to render the authorization panel.
type SchemeRegistry interface {
Register(schemes ...SecuritySchemeAccessor) // ignores nils and duplicate names
Schemes() []SecuritySchemeAccessor // snapshot, in registration order
Lookup(name string) (SecuritySchemeAccessor, bool)
}The registry is an instance in the DI container, not a package-level variable. The security extension registers it; consumers resolve it:
func (e *OpenAPIExtension) OnStart(ctx context.Context, r rextension.Rex) error {
var reg rextension.SchemeRegistry
if err := r.Container().Resolve(®); err != nil {
return nil // no security extension configured — document nothing
}
for _, s := range reg.Schemes() {
e.doc.AddSecurityScheme(s)
}
return nil
}Why not a global. It was one, written by a package-level
RegisterSecuritySchemesthat replaced rather than appended and had no unregister. Two Rex instances in one process clobbered each other's schemes, and state leaked between tests in the same binary. An instance in the container has the lifetime of the application that owns it.
The contracts above describe intent; RouteValidator is
what checks that the intent is satisfiable, before any listener binds. Two
checks that are only possible at that moment:
- A route requiring a scheme the application never registered. Previously a 500 on every request to that route, discoverable only by making one.
-
A route declaring roles for a scheme that cannot enforce them (no
RoleClaimProvider). Previously served to every authenticated caller with no diagnostic anywhere — an endpoint marked "admin only" that was not.
Both are now deployment failures.
-
Name()must be stable. It is the key routes use inRequiredSchemesand the key that appears in the OpenAPI document. Changing it is a breaking change for every route that names it. -
Reject on failure with a Problem document —
ProblemUnauthorizedfor "authenticate",ProblemForbiddenfor "authenticated but not permitted" — and setWWW-AuthenticatefromChallenge()on the 401. -
Never put the reason in
detail. "token expired at 14:02 for subject u_1934" tells an attacker which half of a guess was right. Log it; answer with "credentials were not accepted".
rextension — the Rex extension contract · MIT · © 2026 Kryovyx · pre-1.0, interfaces may change
Building an extension
Contracts
Reference
Ecosystem