-
Notifications
You must be signed in to change notification settings - Fork 0
Description
Given we're using kin-openapi (openapi3filter + router FindRoute) the cleanest “OpenAPI-aware auth” is:
- Only enforce auth when the OpenAPI operation actually declares
security(or inherits globalspec.Security). - Use
openapi3filter.Options.AuthenticationFuncso security is evaluated per operation (and per scheme) the same way the validator evaluates parameters/bodies.
e.g newSpecValidator() middleware.
Let openapi3filter drive auth via AuthenticationFunc
Key behavior: AuthenticationFunc is invoked only when security is required for the matched operation.
package server
import (
"context"
"errors"
"net/http"
"strings"
"github.com/getkin/kin-openapi/openapi3"
"github.com/getkin/kin-openapi/openapi3filter"
)
var errUnauthorized = errors.New("unauthorized")
// Example: plug your real token validation in here.
func validateBearerToken(ctx context.Context, token string) error {
// TODO: verify JWT / OIDC, introspect, call Gen3 authz, etc.
if token == "" {
return errUnauthorized
}
return nil
}
func openapiAuthFunc(spec *openapi3.T) openapi3filter.AuthenticationFunc {
return func(ctx context.Context, input *openapi3filter.AuthenticationInput) error {
// openapi3filter calls this when the operation requires security.
// input.SecuritySchemeName tells you which scheme is being evaluated.
// input.SecurityScheme is the resolved scheme definition.
// input.Scopes are required OAuth scopes (if applicable).
s := input.SecurityScheme
switch s.Type {
case "http":
// Common: bearer JWT
if strings.EqualFold(s.Scheme, "bearer") {
h := input.RequestValidationInput.Request.Header.Get("Authorization")
if h == "" {
return errUnauthorized
}
parts := strings.SplitN(h, " ", 2)
if len(parts) != 2 || !strings.EqualFold(parts[0], "Bearer") {
return errUnauthorized
}
return validateBearerToken(ctx, parts[1])
}
// Basic, etc. handle if you use it.
return errUnauthorized
case "apiKey":
// Respect where the scheme says the key lives.
switch s.In {
case "header":
key := input.RequestValidationInput.Request.Header.Get(s.Name)
if key == "" {
return errUnauthorized
}
// TODO validate key
return nil
case "query":
key := input.RequestValidationInput.Request.URL.Query().Get(s.Name)
if key == "" {
return errUnauthorized
}
// TODO validate key
return nil
case "cookie":
c, _ := input.RequestValidationInput.Request.Cookie(s.Name)
if c == nil || c.Value == "" {
return errUnauthorized
}
// TODO validate key
return nil
default:
return errUnauthorized
}
case "oauth2", "openIdConnect":
// Typically: bearer token + scope checks
h := input.RequestValidationInput.Request.Header.Get("Authorization")
if h == "" {
return errUnauthorized
}
parts := strings.SplitN(h, " ", 2)
if len(parts) != 2 || !strings.EqualFold(parts[0], "Bearer") {
return errUnauthorized
}
if err := validateBearerToken(ctx, parts[1]); err != nil {
return errUnauthorized
}
// TODO: enforce input.Scopes if you use them.
return nil
}
return errUnauthorized
}
}
// Small helper to convert auth errors into proper HTTP responses.
func writeAuthError(w http.ResponseWriter) {
// Bearer is the usual default; adjust if you only use apiKey.
w.Header().Set("WWW-Authenticate", `Bearer realm="drs"`)
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
}Then, in validator middleware where we call openapi3filter.ValidateRequest, pass:
opts := &openapi3filter.Options{
AuthenticationFunc: openapiAuthFunc(spec),
}
if err := openapi3filter.ValidateRequest(ctx, validationInput, opts); err != nil {
// If err looks like auth failure, return 401; otherwise 400.
// You can also inspect err type, but simplest is to special-case errUnauthorized.
if errors.Is(err, errUnauthorized) {
writeAuthError(c.Writer) // or w in net/http
c.Abort()
return
}
// ...your existing 400 handling for validation problems...
}Why this is “OpenAPI-aware”
Because OpenAPI drives the decision:
- If operation has
security: [](explicitly public), auth func is not required. - If operation has one or more security requirements, the validator calls your auth func for the relevant schemes and handles “OR of requirement objects / AND within a requirement” properly.
Integration tip “better-middleware” branch
We currently do:
FindRoute(r)- build
RequestValidationInput - call
ValidateRequest(...)
…then this is a minimal diff:
- Keep your route matching and request validation exactly as-is
- Add
openapi3filter.Options{ AuthenticationFunc: ... } - Map “auth failures” to 401 (not 400)
Trace back to openapi schema spec
It “knows” from the matched OpenAPI operation’s security field (and, if that’s not set, from the spec’s top-level security).
Once you call FindRoute(...), you have an operation. The rule is:
-
Operation-level
securityoverrides global. -
If
operation.Securityis nil, inheritspec.Security. -
If the effective
securityis:- missing / nil or empty ⇒ no auth required
- present and non-empty ⇒ auth required
In kin-openapi types, security is a list of “security requirement objects” (OpenAPI semantics):
- The list is OR (any one requirement object may satisfy)
- Each requirement object is AND across schemes listed inside it
Concretely (what your handler can do)
After FindRoute:
sec := route.Operation.Security
if sec == nil {
sec = spec.Security
}
authRequired := sec != nil && len(*sec) > 0That’s the “does this operation require security at all?” check.
The better way: let openapi3filter decide
If you pass an openapi3filter.Options{ AuthenticationFunc: ... } into ValidateRequest, the validator will:
-
Compute the effective
securityfor the operation -
If it’s not required, it will not call your
AuthenticationFunc -
If it is required, it will call your
AuthenticationFuncwith:- the specific scheme being evaluated
- the required scopes (if any)
So your handler doesn’t need to guess—the OpenAPI validator drives auth only when security says so.