Skip to content

Schemes

wiki edited this page Sep 4, 2026 · 1 revision

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.

Bearer

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.

Basic

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 func until 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. BasicValidatorFunc keeps 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.

API key

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.

Session cookie

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.

Writing your own

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.

Registering

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.

Multiple schemes on one route

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.

Clone this wiki locally