Skip to content

Introspection secret requires a scheme-less Authorization header (RFC 7235 non-conformant); make scheme/header configurable #3132

Description

@mwisner

Summary

The introspection-skip secret is validated by comparing the entire raw Authorization header value against the configured secret. This requires the header to be sent without an auth-scheme (Authorization: <secret>), which:

  1. is non-conformant with RFC 7235 §2.1 (the auth-scheme is mandatory),
  2. cannot traverse any standards-compliant Authorization-parsing intermediary (ext_authz / API gateway / proxy), and
  3. collides with the router's own JWT Bearer authentication, so there is no spec-conformant way to present the secret.

Request: make the introspection credential's scheme (and ideally header) configurable, and dispatch on the scheme so the secret never enters JWT validation.

Current behavior

AccessController.IntrospectionAccess compares the whole header value to the secret:

authHeader := r.Header.Get("Authorization")
if authHeader == "" {
    return false
}
authHeader = strings.TrimSpace(authHeader)
return subtle.ConstantTimeCompare([]byte(authHeader), []byte(a.introspectionSkipSecret)) == 1

Because the full header is compared, the value must be sent scheme-less: Authorization: <secret>.

Why this is a problem

1. RFC 7235 non-conformance. The Authorization header is Authorization = credentials, and credentials require a scheme:

RFC 7235 §2.1 — credentials = auth-scheme [ 1*SP ( token68 / #auth-param ) ]

The auth-scheme sits outside the optional brackets — it is mandatory. A scheme-less Authorization value is malformed per the grammar. (https://datatracker.ietf.org/doc/html/rfc7235#section-2.1)

2. Breaks behind standards-compliant intermediaries. Any Envoy ext_authz service, API gateway, or proxy that parses the Authorization scheme (as the RFC intends) rejects a scheme-less value before it reaches the router. In our deployment an Envoy ext_authz authorizer returns 401 on the scheme-less secret, so introspection can never reach the router where the secret would be checked.

3. Collides with the router's own JWT auth. With authentication.jwt configured (header_name: Authorization, header_value_prefix: Bearer), the secret cannot be presented as Authorization: Bearer <secret> — the JWT authenticator would pick it up and attempt to validate it as a JWT. That leaves only the scheme-less form, which is exactly the form that fails (1) and (2).

Net: there is no way to present the introspection secret in a spec-conformant Authorization header when JWT auth is also enabled.

Note: Bearer is not the right scheme to mandate here anyway — the JWT authenticator owns Bearer, and a distinct scheme cleanly separates the two credential types.

Proposed fix

Primary — configurable scheme + dispatch by scheme.

Add an introspection.secret_scheme option. When set, the router expects Authorization: <scheme> <secret>, strips the (case-insensitive) scheme, and constant-time compares the remainder. When unset, current scheme-less behavior is preserved. The prehandler dispatches on scheme, so <scheme> <secret> is never fed to JWT validation and Bearer <jwt> is never fed to the introspection check.

introspection:
  secret: ''
  secret_scheme: 'Token'   # optional auth-scheme expected on the Authorization header

Sketch:

authHeader := strings.TrimSpace(r.Header.Get("Authorization"))
if authHeader == "" {
    return false
}

presented := authHeader
if a.introspectionSecretScheme != "" {
    scheme, token, found := strings.Cut(authHeader, " ")
    if !found || !strings.EqualFold(scheme, a.introspectionSecretScheme) {
        return false // wrong/absent scheme → not an introspection credential
    }
    presented = strings.TrimSpace(token)
}

return subtle.ConstantTimeCompare([]byte(presented), []byte(a.introspectionSkipSecret)) == 1

Token is a good default choice (real precedent: GitHub used Authorization: token <PAT>); any value matching the RFC 7235 token ABNF works.

Alternative — dedicated configurable header.

Let the secret ride on its own header instead of Authorization, sidestepping the Authorization/JWT overlap entirely:

introspection:
  secret: ''
  secret_header: 'X-Introspection-Token'

Backward compatibility

Both proposals are non-breaking: with secret_scheme / secret_header unset, behavior is identical to today (scheme-less Authorization compare). Existing deployments are unaffected.

Environment

  • Reproduces on main and on router commit b9dd6b20d1d3.
  • Config: authorization.require_authentication: true, authentication.ignore_introspection: true, introspection.secret set, authentication.jwt enabled with the default Bearer prefix.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

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