Skip to content

feature/security-handler #4

@bwalsh

Description

@bwalsh

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 global spec.Security).
  • Use openapi3filter.Options.AuthenticationFunc so 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:

  1. FindRoute(r)
  2. build RequestValidationInput
  3. 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:

  1. Operation-level security overrides global.

  2. If operation.Security is nil, inherit spec.Security.

  3. If the effective security is:

    • missing / nil or emptyno auth required
    • present and non-emptyauth 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) > 0

That’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 security for the operation

  • If it’s not required, it will not call your AuthenticationFunc

  • If it is required, it will call your AuthenticationFunc with:

    • 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions