-
Notifications
You must be signed in to change notification settings - Fork 0
Schemes
A scheme knows how to pull a credential out of a request and validate it. A validator knows how to check that credential against whatever the application uses for identity.
type SecurityScheme interface {
Name() string // unique — routes reference it by this
Type() string // OpenAPI type: "http", "apiKey"
Description() string
Authenticate(r *http.Request) (principal interface{}, err error)
Challenge() string // WWW-Authenticate value on a 401
}Four are built in. A nil validator panics at construction in all four — a
scheme that cannot authenticate anything would otherwise fail at request time,
far from the mistake.
scheme := security.NewBearerScheme("jwt", myTokenValidator).
SetBearerFormat("JWT").
SetRolesClaim("realm_access.roles").
SetDescription("Access token issued by the identity provider")type TokenValidator interface {
ValidateToken(token string) (principal interface{}, err error)
}An interface rather than a function type, because a validator that holds key material or a JWKS cache owns that state privately. For the small case:
security.NewBearerScheme("jwt", security.TokenValidatorFunc(
func(token string) (interface{}, error) { … },
))SetRolesClaim is documentation only — it becomes x-roles-claim on the
OpenAPI security scheme so UI tooling can surface it. It does not configure
enforcement; that is the validator's job. See Roles and Scopes.
security.NewBasicScheme("basic", "Restricted", security.BasicValidatorFunc(
func(username, password string) (interface{}, error) { … },
))type BasicValidator interface {
ValidateCredentials(username, password string) (principal interface{}, err error)
}This took a bare
funcuntil recently, alone among the four schemes — which meant a validator holding a user store had to close over it, and the signature said nothing about the dependency.BasicValidatorFunckeeps the old shape.
Use a constant-time comparison and a real password hash in the validator. Basic sends the credential on every request, so it belongs behind TLS and generally behind an internal listener rather than on a public API.
security.NewAPIKeyScheme("apikey", "X-API-Key", security.APIKeyHeader, myKeyValidator)| Location | Constant | |
|---|---|---|
| header | security.APIKeyHeader |
the normal choice |
| query | security.APIKeyQuery |
⚠ ends up in access logs, proxy logs and Referer
|
| cookie | security.APIKeyCookie |
a BFF session cookie |
type KeyValidator interface {
ValidateKey(key string) (principal interface{}, err error)
}Location() returns a plain string; LocationKind() returns the typed
APIKeyLocation when you want it. The plain form exists so the scheme satisfies
rextension.ParameterizedScheme — a named string type cannot, which is why the
OpenAPI generator once had to reach the method by reflection.
For a Backend-For-Frontend, where the browser only ever holds an opaque session identifier:
validator := security.NewSessionStoreValidator(redisStore,
security.WithIdleTimeout(30*time.Minute),
security.WithAbsoluteTimeout(12*time.Hour),
)
scheme := security.NewSessionCookieScheme("session", "session_id", validator).
WithCookieOptions(security.CookieOptions{
MaxAge: int((12 * time.Hour).Seconds()),
SameSite: http.SameSiteLaxMode,
HttpOnly: true,
// AllowInsecureTransport left false → Secure
})It maps to OpenAPI apiKey / in: cookie, which is the specification-correct
representation. Challenge() returns "" on purpose: a BFF redirects to a
login page on 401 rather than issuing a WWW-Authenticate challenge.
Full lifecycle — issuing, rotating, revoking, timeouts — in Sessions.
This scheme implements CookieScheme, which is what makes its routes
CSRF-exposed. See CSRF.
type HMACScheme struct{ secret []byte }
func (s *HMACScheme) Name() string { return "hmac" }
func (s *HMACScheme) Type() string { return "apiKey" }
func (s *HMACScheme) Description() string { return "Request signature" }
func (s *HMACScheme) Challenge() string { return "" }
func (s *HMACScheme) Authenticate(r *http.Request) (interface{}, error) {
sig := r.Header.Get("X-Signature")
if sig == "" {
return nil, errors.New("no signature")
}
if !hmac.Equal([]byte(sig), s.expected(r)) {
return nil, errors.New("signature mismatch")
}
return &ServicePrincipal{ID: r.Header.Get("X-Service-Id")}, nil
}Optional capabilities, each read by something:
| Implement | And | Gets you |
|---|---|---|
ParamName() string, Location() string
|
— | correct OpenAPI in/name
|
BearerFormat() string |
— |
bearerFormat in the document |
RolesClaim() string |
— |
x-roles-claim in the document |
CookieName() string |
(CookieScheme) |
CSRF protection on its routes |
ValidateRoles + SupportsRoles() bool
|
(RoleEnforcer) |
role enforcement, checked at startup |
ValidateScopes + SupportsScopes() bool
|
(ScopeEnforcer) |
scope enforcement, checked at startup |
Error text from Authenticate never reaches the client — it is logged. Say
what you like in it.
security.NewConfig(
security.WithScheme(bearer),
security.WithScheme(apiKey),
security.WithScheme(sessionCookie),
)Names must be unique and stable. They are the key routes use in
RequiredSchemes and the key that appears in the OpenAPI document; changing one
is a breaking change for every route that names it.
The first registration of a name wins; later ones are ignored. Silent replacement is what the old global registry did, and it is how an application ended up authenticating against a scheme it had not configured.
func (r *InternalRoute) RequiredSchemes() []string {
return []string{"serviceToken", "session"}
}All of them must authenticate. Each produces its own principal, and each is kept:
svc, _ := security.GetPrincipalForScheme(r, "serviceToken")
usr, _ := security.GetPrincipalForScheme(r, "session")GetPrincipal returns the primary principal — the first scheme to
authenticate — which is right for the single-scheme case and ambiguous for
anything else.
Only one principal used to be kept, so a route requiring both a service token and a user session lost one of the two identities.
rextension-security — authentication, authorization and CSRF for Rex · MIT · © 2026 Kryovyx