From 5472061231b2fa47be2e543c65bbea1a3fbc048e Mon Sep 17 00:00:00 2001 From: Chris Lu Date: Wed, 21 Jan 2026 18:36:24 -0800 Subject: [PATCH] Fix: Populate Claims from STS session RequestContext for policy variable substitution (#8082) * Fix: Populate Claims from STS session RequestContext for policy variable substitution When using STS temporary credentials (from AssumeRoleWithWebIdentity) with AWS Signature V4 authentication, JWT claims like preferred_username were not available for bucket policy variable substitution (e.g., ${jwt:preferred_username}). Root Cause: - STS session tokens store user claims in the req_ctx field (added in PR #8079) - validateSTSSessionToken() created Identity but didn't populate Claims field - authorizeWithIAM() created IAMIdentity but didn't copy Claims - Policy engine couldn't resolve ${jwt:*} variables without claims Changes: 1. auth_signature_v4.go: Extract claims from sessionInfo.RequestContext and populate Identity.Claims in validateSTSSessionToken() 2. auth_credentials.go: Copy Claims when creating IAMIdentity in authorizeWithIAM() 3. auth_sts_identity_test.go: Add TestSTSIdentityClaimsPopulation to verify claims are properly populated from RequestContext This enables bucket policies with JWT claim variables to work correctly with STS temporary credentials obtained via AssumeRoleWithWebIdentity. Fixes #8037 * Refactor: Idiomatic map population for STS claims --- weed/s3api/auth_credentials.go | 1 + weed/s3api/auth_signature_v4.go | 9 ++++ weed/s3api/auth_sts_identity_test.go | 65 ++++++++++++++++++++++++++++ 3 files changed, 75 insertions(+) diff --git a/weed/s3api/auth_credentials.go b/weed/s3api/auth_credentials.go index 9db792809..441e8f002 100644 --- a/weed/s3api/auth_credentials.go +++ b/weed/s3api/auth_credentials.go @@ -1408,6 +1408,7 @@ func (iam *IdentityAccessManagement) authorizeWithIAM(r *http.Request, identity Name: identity.Name, Account: identity.Account, PolicyNames: identity.PolicyNames, + Claims: identity.Claims, // Copy claims for policy variable substitution } // Determine authorization path and configure identity diff --git a/weed/s3api/auth_signature_v4.go b/weed/s3api/auth_signature_v4.go index 7ebdefd1c..6c537dc70 100644 --- a/weed/s3api/auth_signature_v4.go +++ b/weed/s3api/auth_signature_v4.go @@ -382,6 +382,14 @@ func (iam *IdentityAccessManagement) validateSTSSessionToken(r *http.Request, se Expiration: sessionInfo.ExpiresAt.Unix(), } + // Create claims map from request context + // The request context contains user information from the original OIDC token + // that was used in AssumeRoleWithWebIdentity (e.g., preferred_username, email, etc.) + claims := make(map[string]interface{}, len(sessionInfo.RequestContext)) + for k, v := range sessionInfo.RequestContext { + claims[k] = v + } + // Create an identity for the STS session // The identity represents the assumed role user identity := &Identity{ @@ -390,6 +398,7 @@ func (iam *IdentityAccessManagement) validateSTSSessionToken(r *http.Request, se Credentials: []*Credential{cred}, PrincipalArn: sessionInfo.Principal, PolicyNames: sessionInfo.Policies, // Populate PolicyNames for IAM authorization + Claims: claims, // Populate Claims for policy variable substitution } glog.V(2).Infof("Successfully validated STS session token for principal: %s, assumed role user: %s", diff --git a/weed/s3api/auth_sts_identity_test.go b/weed/s3api/auth_sts_identity_test.go index 5120bbccf..808f04e67 100644 --- a/weed/s3api/auth_sts_identity_test.go +++ b/weed/s3api/auth_sts_identity_test.go @@ -304,6 +304,71 @@ func TestValidateSTSSessionTokenIntegration(t *testing.T) { t.Log("✓ Integration test passed: STS identity properly configured for IAM authorization") } +// TestSTSIdentityClaimsPopulation tests that Claims are properly populated from RequestContext +// This is critical for policy variable substitution like ${jwt:preferred_username} +func TestSTSIdentityClaimsPopulation(t *testing.T) { + // Setup STS service + stsService, config := setupTestSTSService(t) + + // Create IAM with STS integration + iam := NewIdentityAccessManagementWithStore(&S3ApiServerOption{}, "memory") + s3iam := &S3IAMIntegration{ + stsService: stsService, + } + iam.SetIAMIntegration(s3iam) + + // Create a mock HTTP request + req, err := http.NewRequest("PUT", "/test-bucket/test-object.txt", nil) + require.NoError(t, err) + + // Generate session token with RequestContext containing user claims + sessionId := "claims-test-session" + expiresAt := time.Now().Add(time.Hour) + sessionClaims := sts.NewSTSSessionClaims(sessionId, config.Issuer, expiresAt). + WithSessionName("claims-test"). + WithRoleInfo("arn:aws:iam::role/S3UserRole", "arn:aws:sts::assumed-role/S3UserRole/claims-test", "arn:aws:sts::assumed-role/S3UserRole/claims-test") + + // Add RequestContext with user claims (simulating AssumeRoleWithWebIdentity) + sessionClaims.RequestContext = map[string]interface{}{ + "preferred_username": "f2wbnp", + "email": "user@example.com", + "name": "Test User", + "groups": []string{"developers", "users"}, + } + + sessionClaims.Policies = []string{"S3UserPolicy"} + + tokenGen := sts.NewTokenGenerator(config.SigningKey, config.Issuer) + sessionToken, err := tokenGen.GenerateJWTWithClaims(sessionClaims) + require.NoError(t, err) + + // Validate session token + sessionInfo, err := stsService.ValidateSessionToken(context.Background(), sessionToken) + require.NoError(t, err) + require.NotNil(t, sessionInfo) + require.NotNil(t, sessionInfo.RequestContext, "RequestContext should be populated") + + // Verify RequestContext has the claims + assert.Equal(t, "f2wbnp", sessionInfo.RequestContext["preferred_username"]) + assert.Equal(t, "user@example.com", sessionInfo.RequestContext["email"]) + + // Validate session token and check identity creation + identity, _, errCode := iam.validateSTSSessionToken(req, sessionToken, sessionInfo.Credentials.AccessKeyId) + require.Equal(t, s3err.ErrNone, errCode) + require.NotNil(t, identity) + + // Verify Claims are populated from RequestContext + assert.NotNil(t, identity.Claims, "Claims should be populated") + assert.Equal(t, "f2wbnp", identity.Claims["preferred_username"], "preferred_username should be in Claims") + assert.Equal(t, "user@example.com", identity.Claims["email"], "email should be in Claims") + assert.Equal(t, "Test User", identity.Claims["name"], "name should be in Claims") + + // Verify PolicyNames are also populated + assert.Equal(t, []string{"S3UserPolicy"}, identity.PolicyNames) + + t.Log("✓ Claims properly populated from RequestContext for policy variable substitution") +} + // Helper functions for tests func setupTestSTSService(t *testing.T) (*sts.STSService, *sts.STSConfig) {