// Package errors implements the unsupported feature enforcement layer for KeyCape. // Every request passes through the Registry middleware before reaching any handler. // If a registered feature is detected the middleware writes a ProfileError JSON // response, emits an EventUnsupportedFeature telemetry event, and short-circuits // the handler chain. Adding a new unsupported feature requires only a call to // Register — no handler changes are needed. package errors import ( "net/http" "strings" "time" profileerrors "keycape/internal/errors" "keycape/internal/server/telemetry" ) // UnsupportedFeature describes a profile boundary that KeyCape enforces. type UnsupportedFeature struct { // Name is a stable string identifier used in telemetry and error payloads. Name string // ErrorType is the profile error category emitted when this feature is triggered. ErrorType profileerrors.ErrorType // Description is a human-readable explanation of why the feature is blocked. Description string // Detector reports whether the given request triggers this feature. Detector func(r *http.Request) bool } // Registry holds all known unsupported features and exposes middleware that // enforces them on every incoming request. type Registry struct { features []UnsupportedFeature } // NewRegistry returns an empty Registry. Use Register to add features and // DefaultRegistry to obtain one pre-populated with the spec-mandated set. func NewRegistry() *Registry { return &Registry{} } // Register appends a feature to the registry. Registered features are checked // in insertion order; the first match wins. func (reg *Registry) Register(f UnsupportedFeature) { reg.features = append(reg.features, f) } // Middleware returns an http.Handler that evaluates all registered features // for every request before delegating to next. // // If a feature is triggered: // - A ProfileError JSON response is written with an appropriate HTTP status. // - An EventUnsupportedFeature telemetry event is emitted via the Emitter // stored in the request context (a NoopEmitter is used when none is set). // - next is NOT called. // // If no feature matches, next is called normally. func (reg *Registry) Middleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { for _, f := range reg.features { if f.Detector(r) { pe := &profileerrors.ProfileError{ Error: f.ErrorType, Description: f.Description, Feature: f.Name, } pe.Write(w, httpStatusFor(f.ErrorType)) em := telemetry.EmitterFromContext(r.Context()) em.Emit(r.Context(), telemetry.Event{ Timestamp: time.Now().UTC(), EventType: telemetry.EventUnsupportedFeature, Feature: f.Name, ErrorType: string(f.ErrorType), Endpoint: r.URL.Path, Result: "failure", Environment: "", TraceID: "", ClientID: r.URL.Query().Get("client_id"), }) return } } next.ServeHTTP(w, r) }) } // httpStatusFor maps an ErrorType to its canonical HTTP status code. func httpStatusFor(et profileerrors.ErrorType) int { switch et { case profileerrors.ErrInvalidProfileUsage: return http.StatusBadRequest case profileerrors.ErrRejectedForSafety: return http.StatusForbidden case profileerrors.ErrKeycloakModeOnly: return http.StatusNotImplemented default: // ErrFeatureNotSupported return http.StatusNotImplemented } } // --------------------------------------------------------------------------- // Default feature set (spec §4 — normative). // --------------------------------------------------------------------------- // DefaultRegistry returns a Registry pre-populated with all spec-mandated // unsupported features. No handler changes are required to enforce new entries. func DefaultRegistry() *Registry { reg := NewRegistry() // 1. Dynamic client registration (RFC 7591) — not in the profile. reg.Register(UnsupportedFeature{ Name: "dynamic_client_registration", ErrorType: profileerrors.ErrFeatureNotSupported, Description: "Dynamic client registration is not part of the NetKingdom IAM Profile. Register clients statically in KeyCape configuration.", Detector: func(r *http.Request) bool { return (r.Method == http.MethodPost && strings.HasSuffix(r.URL.Path, "/connect/register")) || strings.Contains(r.URL.Path, "registration") }, }) // 2. Implicit flow — blocked for security. reg.Register(UnsupportedFeature{ Name: "implicit_flow", ErrorType: profileerrors.ErrRejectedForSafety, Description: "The implicit flow (response_type=token or id_token) is rejected. Use the authorization code flow with PKCE.", Detector: func(r *http.Request) bool { rt := r.URL.Query().Get("response_type") if rt == "" { return false } // Blocked when response_type contains "token" or "id_token" but NOT when it is exactly "code". // "code token" (hybrid) is also blocked. return rt == "token" || rt == "id_token" || strings.Contains(rt, "token") && rt != "code" }, }) // 3. Wildcard redirect_uri — blocked for security. reg.Register(UnsupportedFeature{ Name: "wildcard_redirect_uri", ErrorType: profileerrors.ErrRejectedForSafety, Description: "Wildcard redirect URIs are not permitted. Register exact redirect URIs in the client configuration.", Detector: func(r *http.Request) bool { return strings.Contains(r.URL.Query().Get("redirect_uri"), "*") }, }) // 4. Identity brokering — available only in Keycloak mode. reg.Register(UnsupportedFeature{ Name: "identity_broker", ErrorType: profileerrors.ErrKeycloakModeOnly, Description: "Identity brokering is available only in expanded (Keycloak) mode.", Detector: func(r *http.Request) bool { return strings.Contains(r.URL.Path, "/broker/") }, }) // 5. PKCE plain method — blocked for security (must use S256). // Registered BEFORE missing_pkce so a plain-method request is reported // as pkce_plain_method, not missing_pkce. reg.Register(UnsupportedFeature{ Name: "pkce_plain_method", ErrorType: profileerrors.ErrRejectedForSafety, Description: "PKCE plain code challenge method is not allowed. Use S256.", Detector: func(r *http.Request) bool { return r.URL.Query().Get("code_challenge_method") == "plain" }, }) // 6. Missing PKCE on /authorize — invalid profile usage. reg.Register(UnsupportedFeature{ Name: "missing_pkce", ErrorType: profileerrors.ErrInvalidProfileUsage, Description: "Requests to /authorize must include a code_challenge (PKCE S256 required).", Detector: func(r *http.Request) bool { return strings.HasSuffix(r.URL.Path, "/authorize") && r.URL.Query().Get("code_challenge") == "" }, }) // 7. Unknown grant type on /token. reg.Register(UnsupportedFeature{ Name: "unknown_grant_type", ErrorType: profileerrors.ErrFeatureNotSupported, Description: "Only authorization_code and refresh_token grant types are supported.", Detector: func(r *http.Request) bool { if r.Method != http.MethodPost || !strings.HasSuffix(r.URL.Path, "/token") { return false } gt := r.URL.Query().Get("grant_type") if gt == "" { // Also check form body if already parsed — callers may pre-parse. gt = r.FormValue("grant_type") } if gt == "" { return false // no grant_type present; let the handler decide } return gt != "authorization_code" && gt != "refresh_token" }, }) return reg }