Bug description
When an opaque access token is validated through RFC 7662 introspection, ToolHive drops non-standard claims from the introspection response. Consequently, Cedar policies cannot reference claims such as roles, even though the introspection endpoint returns them.
Reproduction
Given an introspection response containing:
{
"active": true,
"sub": "user-123",
"roles": ["admin"]
}
and a Cedar policy:
permit(
principal,
action == Action::"call_tool",
resource == Tool::"example_admin-tool"
) when {
principal has claim_roles &&
principal.claim_roles.contains("admin")
};
the tool is excluded from tools/list.
The gateway logs show:
Client::"user-123" does not have the attribute claim_roles
Expected behavior
Custom fields returned by a successful RFC 7662 introspection response should be exposed to Cedar using the existing claim_ prefix, consistent with claims obtained from JWT access tokens.
In this example, roles should become principal.claim_roles
Actual behavior
Only the explicitly modeled fields survive introspection:
exp
sub
aud
scope
iss
All other fields are discarded.
Environment (if relevant)
- ToolHive version: vmcp:v0.49.0
Suggested direction
Decode the introspection response into a generic map, or implement custom unmarshalling, so extension fields are retained alongside the standard RFC 7662 fields.
This would make Cedar claim behavior consistent between JWT and introspected opaque access tokens.
Bug description
When an opaque access token is validated through RFC 7662 introspection, ToolHive drops non-standard claims from the introspection response. Consequently, Cedar policies cannot reference claims such as
roles, even though the introspection endpoint returns them.Reproduction
Given an introspection response containing:
{ "active": true, "sub": "user-123", "roles": ["admin"] }and a Cedar policy:
the tool is excluded from tools/list.
The gateway logs show:
Client::"user-123" does not have the attribute
claim_rolesExpected behavior
Custom fields returned by a successful RFC 7662 introspection response should be exposed to Cedar using the existing claim_ prefix, consistent with claims obtained from JWT access tokens.
In this example,
rolesshould becomeprincipal.claim_rolesActual behavior
Only the explicitly modeled fields survive introspection:
expsubaudscopeissAll other fields are discarded.
Environment (if relevant)
Suggested direction
Decode the introspection response into a generic map, or implement custom unmarshalling, so extension fields are retained alongside the standard RFC 7662 fields.
This would make Cedar claim behavior consistent between JWT and introspected opaque access tokens.