Summary
The --oidc-scopes flag on thv run is documented (and named) to suggest it enforces required scopes on incoming JWT tokens. In practice it only advertises scopes in the OAuth2 discovery document (RFC 9728 /.well-known/oauth-protected-resource) and in the WWW-Authenticate response header. It does not validate that incoming tokens carry those scopes.
Expected behaviour
When a proxy is started with --oidc-scopes required-scope, any JWT that does not contain required-scope in its scope claim should be rejected with 401 Unauthorized (or 403 Forbidden).
Actual behaviour
validateClaims in pkg/auth/token.go only checks issuer, audience, and expiry. The v.scopes field is never consulted during token validation — it is only used to populate:
- The
scope field in the WWW-Authenticate response header (buildWWWAuthenticate)
- The
scopes_supported field in the OAuth2 resource metadata document (NewAuthInfoHandler)
Any token with valid issuer, audience, and expiry is accepted regardless of its scope claim.
Impact
A downstream application that depends on thv enforcing OAuth scopes for access control is silently unprotected. Tokens issued without the required scope are accepted as if they were fully authorized.
Relevant code
pkg/auth/token.go — validateClaims function:
func (v *TokenValidator) validateClaims(claims jwt.MapClaims) error {
// Validates: issuer, audience, expiry only
// v.scopes is never checked here
...
return nil
}
Suggested fix
Add scope claim validation to validateClaims:
if len(v.scopes) > 0 {
scopeClaim, _ := claims["scope"].(string)
tokenScopes := strings.Fields(scopeClaim)
for _, required := range v.scopes {
found := slices.Contains(tokenScopes, required)
if !found {
return ErrInsufficientScope
}
}
}
Discovery context
Found while implementing DAST adversarial tests for scope validation in the enterprise platform. The test (missing_scope in the token variation matrix) cannot be automated end-to-end because the proxy accepts all valid OIDC tokens regardless of scope.
Summary
The
--oidc-scopesflag onthv runis documented (and named) to suggest it enforces required scopes on incoming JWT tokens. In practice it only advertises scopes in the OAuth2 discovery document (RFC 9728/.well-known/oauth-protected-resource) and in theWWW-Authenticateresponse header. It does not validate that incoming tokens carry those scopes.Expected behaviour
When a proxy is started with
--oidc-scopes required-scope, any JWT that does not containrequired-scopein itsscopeclaim should be rejected with401 Unauthorized(or403 Forbidden).Actual behaviour
validateClaimsinpkg/auth/token.goonly checks issuer, audience, and expiry. Thev.scopesfield is never consulted during token validation — it is only used to populate:scopefield in theWWW-Authenticateresponse header (buildWWWAuthenticate)scopes_supportedfield in the OAuth2 resource metadata document (NewAuthInfoHandler)Any token with valid issuer, audience, and expiry is accepted regardless of its
scopeclaim.Impact
A downstream application that depends on thv enforcing OAuth scopes for access control is silently unprotected. Tokens issued without the required scope are accepted as if they were fully authorized.
Relevant code
pkg/auth/token.go—validateClaimsfunction:Suggested fix
Add scope claim validation to
validateClaims:Discovery context
Found while implementing DAST adversarial tests for scope validation in the enterprise platform. The test (
missing_scopein the token variation matrix) cannot be automated end-to-end because the proxy accepts all valid OIDC tokens regardless of scope.