Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions pkg/authz/authorizers/cedar/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -616,10 +616,20 @@ func (a *Authorizer) resolveClaims(identity *auth.Identity) (jwt.MapClaims, erro
// Embedded auth server path: the upstream IDP token is the primary claim source.
upstreamToken, tokenFound := identity.UpstreamTokens[a.primaryUpstreamProvider]
if !tokenFound || upstreamToken == "" {
// The upstream token must be present if the authorizer is configured to use it.
// Missing token means the session has no upstream credential; deny.
return nil, fmt.Errorf("upstream token for provider %q not found in identity",
a.primaryUpstreamProvider)
// RFC 8693 delegated tokens (and any token without an upstream session)
// have no UpstreamTokens entry — that binding only exists for an
// identity established via the upstream browser flow. Falling back to
// the request token's claims keeps delegation usable when Cedar is
// configured with a primaryUpstreamProvider (auto-derived from the
// sole upstream), rather than denying every tool before policy
// evaluation. This mirrors the existing opaque-token fallback branch
// below. See #6424.
a.claimKeyLog.Do(func() {
slog.Warn("upstream token not found for provider; falling back to request-token claims for Cedar evaluation",
"provider", a.primaryUpstreamProvider)
})
a.logClaimKeys("delegated-fallback", requestClaims)
return requestClaims, nil
}

upstreamClaims, err := parseUpstreamJWTClaims(upstreamToken)
Expand Down
33 changes: 29 additions & 4 deletions pkg/authz/authorizers/cedar/core_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1404,8 +1404,12 @@ func TestAuthorizeWithJWTClaims_UpstreamProvider(t *testing.T) {
},
UpstreamTokens: map[string]string{},
},
wantErr: true,
errContains: "upstream token for provider",
// Delegated tokens (RFC 8693) have no UpstreamTokens entry; the
// authorizer now falls back to request-token claims instead of
// erroring. Here the fallback sub does not match the policy, so
// authorization is denied by policy evaluation, not by a missing-
// token error. See #6424.
wantAuthorize: false,
},
{
name: "upstream_token_opaque_falls_back_to_request_claims_denied",
Expand Down Expand Up @@ -1469,8 +1473,29 @@ func TestAuthorizeWithJWTClaims_UpstreamProvider(t *testing.T) {
},
UpstreamTokens: nil,
},
wantErr: true,
errContains: "upstream token for provider",
// Same fallback as above: a nil UpstreamTokens map (no tsid, as in
// a delegated token minted without an IDP session link) falls back
// to request claims rather than erroring. Policy evaluation then
// denies because sub mismatch.
wantAuthorize: false,
},
{
name: "delegated_token_missing_upstream_falls_back_and_permits_via_request_claims",
identity: &auth.Identity{
PrincipalInfo: auth.PrincipalInfo{
Subject: "upstream-user",
Claims: map[string]any{
"sub": "upstream-user",
"act": map[string]interface{}{"sub": "spiffe://toolhive.dev/ns/agents/sa/delegate"},
},
},
UpstreamTokens: nil,
},
// RFC 8693 delegated token has no upstream session (no tsid) and
// thus no UpstreamTokens entry. The fallback to request claims
// carries the delegated subject through, so a policy targeting the
// user still permits. See #6424.
wantAuthorize: true,
},
{
name: "upstream_token_has_no_sub_claim",
Expand Down