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
27 changes: 27 additions & 0 deletions pkg/authz/authorizers/cedar/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,26 @@ func (a *Authorizer) resolveClaims(identity *auth.Identity) (jwt.MapClaims, erro
}
parsedClaims, err := parseUpstreamJWTClaims(upstreamToken)
if err != nil {
// Distinguish "not JWT-shaped" (opaque OAuth 2.0 access token —
// Google's ya29.*, GitHub's gho_*, etc.) from "JWT-shaped but
// malformed/tampered" (a JWT with three segments that fails to
// parse). Only fall back for the former; preserve the deny for
// the latter so a tampered upstream JWT cannot bypass policy.
//
// The embedded auth server already mirrors the upstream OIDC
// sub/email/name claims into its issued AS token (see
// pkg/authserver/server/session/session.go). For opaque-token
// providers, falling back to identity.Claims preserves identity
// for policies referencing standard OIDC claims; policies that
// reference upstream-only claims (groups, hd, custom namespaced
// claims) will see those attributes as absent and must be
// authored defensively (`has(claim_groups) && ...`).
if !looksLikeJWT(upstreamToken) {
slog.Warn("upstream token is not a JWT; falling back to request-token claims for Cedar evaluation",
"provider", a.primaryUpstreamProvider)
a.logClaimKeys("token-fallback", jwt.MapClaims(identity.Claims))
return jwt.MapClaims(identity.Claims), nil
}
return nil, fmt.Errorf("failed to parse upstream token for provider %q: %w",
a.primaryUpstreamProvider, err)
}
Expand All @@ -504,6 +524,13 @@ func (a *Authorizer) resolveClaims(identity *auth.Identity) (jwt.MapClaims, erro
return claims, nil
}

// looksLikeJWT returns true when the token has the three-segment shape of a
// JOSE-compact-serialized JWT (`header.payload.signature`). It does not
// validate the contents; the parser handles that.
func looksLikeJWT(tokenStr string) bool {
return strings.Count(tokenStr, ".") == 2
}

// logClaimKeys emits a rate-limited DEBUG log listing the JWT claim keys
// available for Cedar policy evaluation.
func (a *Authorizer) logClaimKeys(source string, claims jwt.MapClaims) {
Expand Down
41 changes: 40 additions & 1 deletion pkg/authz/authorizers/cedar/core_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1296,7 +1296,7 @@ func TestAuthorizeWithJWTClaims_UpstreamProvider(t *testing.T) {
errContains: "upstream token for provider",
},
{
name: "upstream_token_opaque_not_parseable",
name: "upstream_token_opaque_falls_back_to_request_claims_denied",
identity: &auth.Identity{
PrincipalInfo: auth.PrincipalInfo{
Subject: "thv-user",
Expand All @@ -1306,6 +1306,45 @@ func TestAuthorizeWithJWTClaims_UpstreamProvider(t *testing.T) {
providerName: "opaque-token-cannot-be-parsed",
},
},
// Opaque upstream tokens (Google's ya29.*, GitHub's gho_*, etc.)
// trigger the fallback to identity.Claims. Here the request-token
// sub does not match the policy, so authorization is correctly
// denied based on policy evaluation rather than a parse-time error.
wantAuthorize: false,
},
{
name: "upstream_token_opaque_falls_back_to_request_claims_permitted",
identity: &auth.Identity{
PrincipalInfo: auth.PrincipalInfo{
Subject: "upstream-user",
Claims: map[string]any{"sub": "upstream-user"},
},
UpstreamTokens: map[string]string{
providerName: "opaque-token-cannot-be-parsed",
},
},
// When the upstream token is not a JWT, Cedar evaluates against
// the request-token claims. The embedded auth server already
// mirrors the upstream OIDC sub/email/name into its issued token,
// so a policy referencing claim_sub still matches the user.
wantAuthorize: true,
},
{
name: "upstream_token_jwt_shaped_but_malformed_still_errors",
identity: &auth.Identity{
PrincipalInfo: auth.PrincipalInfo{
Subject: "thv-user",
Claims: map[string]any{"sub": "thv-user"},
},
UpstreamTokens: map[string]string{
// Three-segment shape (looks like a JWT) but the segments are
// not valid base64-encoded JSON — i.e. a tampered or
// corrupted JWT. The fallback path MUST NOT trigger here:
// silently degrading a tampered upstream JWT to fallback
// claims would be a security regression.
providerName: "not-base64.not-base64.not-base64",
},
},
wantErr: true,
errContains: "failed to parse upstream token",
},
Expand Down