From cd2e93bf2badb9925d3670658d75065364ccb180 Mon Sep 17 00:00:00 2001 From: Chris Lu Date: Wed, 21 Jan 2026 13:27:33 -0800 Subject: [PATCH] fix: propagate OIDC attributes to STS session token for IAM policies (#8079) * fix: propagate OIDC attributes to STS session token * refactor: apply PR suggestions for STS session claims --- weed/iam/sts/sts_service.go | 23 +++++++- weed/iam/sts/sts_service_test.go | 93 ++++++++++++++++++++++++++++++++ 2 files changed, 115 insertions(+), 1 deletion(-) diff --git a/weed/iam/sts/sts_service.go b/weed/iam/sts/sts_service.go index f87038fc8..ffbd69e27 100644 --- a/weed/iam/sts/sts_service.go +++ b/weed/iam/sts/sts_service.go @@ -458,12 +458,33 @@ func (s *STSService) AssumeRoleWithWebIdentity(ctx context.Context, request *Ass Subject: externalIdentity.UserID, } + // Create request context from identity attributes for policy evaluation + requestContext := make(map[string]interface{}, len(externalIdentity.Attributes)+3) + + // Add generic attributes (including preferred_username, etc.) + if externalIdentity.Attributes != nil { + for k, v := range externalIdentity.Attributes { + requestContext[k] = v + } + } + + // Add standard OIDC fields if not already present + if _, ok := requestContext["email"]; !ok && externalIdentity.Email != "" { + requestContext["email"] = externalIdentity.Email + } + if _, ok := requestContext["name"]; !ok && externalIdentity.DisplayName != "" { + requestContext["name"] = externalIdentity.DisplayName + } + // Add sub as well since it's commonly used + requestContext["sub"] = externalIdentity.UserID + // Create rich JWT claims with all session information sessionClaims := NewSTSSessionClaims(sessionId, s.Config.Issuer, expiresAt). WithSessionName(request.RoleSessionName). WithRoleInfo(request.RoleArn, assumedRoleUser.Arn, assumedRoleUser.Arn). WithIdentityProvider(provider.Name(), externalIdentity.UserID, ""). - WithMaxDuration(sessionDuration) + WithMaxDuration(sessionDuration). + WithRequestContext(requestContext) // Generate self-contained JWT token with all session information jwtToken, err := s.tokenGenerator.GenerateJWTWithClaims(sessionClaims) diff --git a/weed/iam/sts/sts_service_test.go b/weed/iam/sts/sts_service_test.go index 7ab58c189..dfd93fd6c 100644 --- a/weed/iam/sts/sts_service_test.go +++ b/weed/iam/sts/sts_service_test.go @@ -660,3 +660,96 @@ func (m *MockIdentityProviderWithExpiration) ValidateToken(ctx context.Context, func timePtr(t time.Time) *time.Time { return &t } + +// TestAssumeRoleWithWebIdentity_PreservesAttributes tests that attributes from the identity provider +// are correctly propagated to the session token's request context +func TestAssumeRoleWithWebIdentity_PreservesAttributes(t *testing.T) { + service := setupTestSTSService(t) + + // Create a mock provider that returns a user with attributes + mockProvider := &MockIdentityProviderWithAttributes{ + name: "attr-provider", + attributes: map[string]string{ + "preferred_username": "my-user", + "department": "engineering", + "project": "seaweedfs", + }, + } + service.RegisterProvider(mockProvider) + + // Create a valid JWT token for the provider + token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ + "iss": "attr-provider", + "sub": "test-user-id", + "aud": "test-client", + "exp": time.Now().Add(time.Hour).Unix(), + "iat": time.Now().Unix(), + }) + tokenString, err := token.SignedString([]byte("test-signing-key")) + require.NoError(t, err) + + ctx := context.Background() + request := &AssumeRoleWithWebIdentityRequest{ + RoleArn: "arn:aws:iam::role/TestRole", + WebIdentityToken: tokenString, + RoleSessionName: "test-session", + } + + response, err := service.AssumeRoleWithWebIdentity(ctx, request) + require.NoError(t, err) + require.NotNil(t, response) + + // Validate the session token to check claims + sessionInfo, err := service.ValidateSessionToken(ctx, response.Credentials.SessionToken) + require.NoError(t, err) + + // Check that attributes are present in RequestContext + require.NotNil(t, sessionInfo.RequestContext, "RequestContext should not be nil") + assert.Equal(t, "my-user", sessionInfo.RequestContext["preferred_username"]) + assert.Equal(t, "engineering", sessionInfo.RequestContext["department"]) + assert.Equal(t, "seaweedfs", sessionInfo.RequestContext["project"]) + + // Check standard claims are also present + assert.Equal(t, "test-user-id", sessionInfo.RequestContext["sub"]) + assert.Equal(t, "test@example.com", sessionInfo.RequestContext["email"]) + assert.Equal(t, "Test User", sessionInfo.RequestContext["name"]) +} + +// MockIdentityProviderWithAttributes is a mock provider that returns configured attributes +type MockIdentityProviderWithAttributes struct { + name string + attributes map[string]string +} + +func (m *MockIdentityProviderWithAttributes) Name() string { + return m.name +} + +func (m *MockIdentityProviderWithAttributes) GetIssuer() string { + return m.name +} + +func (m *MockIdentityProviderWithAttributes) Initialize(config interface{}) error { + return nil +} + +func (m *MockIdentityProviderWithAttributes) Authenticate(ctx context.Context, token string) (*providers.ExternalIdentity, error) { + return &providers.ExternalIdentity{ + UserID: "test-user-id", + Email: "test@example.com", + DisplayName: "Test User", + Provider: m.name, + Attributes: m.attributes, + }, nil +} + +func (m *MockIdentityProviderWithAttributes) GetUserInfo(ctx context.Context, userID string) (*providers.ExternalIdentity, error) { + return nil, nil +} + +func (m *MockIdentityProviderWithAttributes) ValidateToken(ctx context.Context, token string) (*providers.TokenClaims, error) { + return &providers.TokenClaims{ + Subject: "test-user-id", + Issuer: m.name, + }, nil +}