Fix: Propagate OIDC claims for dynamic IAM policies (#8060)
Fix: Propagate OIDC claims to IAM identity for dynamic policy variables Fixes #8037. Ensures additional OIDC claims (like preferred_username) are preserved in ExternalIdentity attributes and propagated to IAM tokens, enabling substitution in dynamic policies.
This commit is contained in:
@@ -237,6 +237,34 @@ func (p *OIDCProvider) Authenticate(ctx context.Context, token string) (*provide
|
|||||||
attributes["roles"] = strings.Join(roles, ",")
|
attributes["roles"] = strings.Join(roles, ",")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Store all additional claims as attributes
|
||||||
|
processedClaims := map[string]struct{}{
|
||||||
|
// user / business claims already handled elsewhere
|
||||||
|
"sub": {},
|
||||||
|
"email": {},
|
||||||
|
"name": {},
|
||||||
|
"groups": {},
|
||||||
|
"roles": {},
|
||||||
|
// standard structural OIDC/JWT claims that should not be exposed as attributes
|
||||||
|
"iss": {},
|
||||||
|
"aud": {},
|
||||||
|
"exp": {},
|
||||||
|
"iat": {},
|
||||||
|
"nbf": {},
|
||||||
|
"jti": {},
|
||||||
|
}
|
||||||
|
for key, value := range claims.Claims {
|
||||||
|
if _, isProcessed := processedClaims[key]; !isProcessed {
|
||||||
|
if strValue, ok := value.(string); ok {
|
||||||
|
attributes[key] = strValue
|
||||||
|
} else if jsonValue, err := json.Marshal(value); err == nil {
|
||||||
|
attributes[key] = string(jsonValue)
|
||||||
|
} else {
|
||||||
|
glog.Warningf("failed to marshal claim %q to JSON for OIDC attributes: %v", key, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
identity := &providers.ExternalIdentity{
|
identity := &providers.ExternalIdentity{
|
||||||
UserID: claims.Subject,
|
UserID: claims.Subject,
|
||||||
Email: email,
|
Email: email,
|
||||||
|
|||||||
@@ -248,6 +248,60 @@ func TestOIDCProviderAuthentication(t *testing.T) {
|
|||||||
assert.Contains(t, identity.Groups, "developers")
|
assert.Contains(t, identity.Groups, "developers")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("successful authentication with additional attributes", func(t *testing.T) {
|
||||||
|
token := createTestJWT(t, privateKey, jwt.MapClaims{
|
||||||
|
"iss": server.URL,
|
||||||
|
"aud": "test-client",
|
||||||
|
"sub": "user123",
|
||||||
|
"exp": time.Now().Add(time.Hour).Unix(),
|
||||||
|
"iat": time.Now().Unix(),
|
||||||
|
"email": "user@example.com",
|
||||||
|
"name": "Test User",
|
||||||
|
"groups": []string{"users"},
|
||||||
|
"preferred_username": "myusername", // Extra claim
|
||||||
|
"department": "engineering", // Extra claim
|
||||||
|
"custom_number": 42, // Non-string claim
|
||||||
|
"custom_avg": 98.6, // Non-string claim
|
||||||
|
"custom_object": map[string]interface{}{"nested": "value"}, // Nested object claim
|
||||||
|
})
|
||||||
|
|
||||||
|
identity, err := provider.Authenticate(context.Background(), token)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, identity)
|
||||||
|
|
||||||
|
// Check standard fields
|
||||||
|
assert.Equal(t, "user123", identity.UserID)
|
||||||
|
|
||||||
|
// Check attributes
|
||||||
|
val, exists := identity.Attributes["preferred_username"]
|
||||||
|
assert.True(t, exists, "preferred_username should be in attributes")
|
||||||
|
assert.Equal(t, "myusername", val)
|
||||||
|
|
||||||
|
val, exists = identity.Attributes["department"]
|
||||||
|
assert.True(t, exists, "department should be in attributes")
|
||||||
|
assert.Equal(t, "engineering", val)
|
||||||
|
|
||||||
|
// Test non-string claims (should be JSON marshaled)
|
||||||
|
val, exists = identity.Attributes["custom_number"]
|
||||||
|
assert.True(t, exists, "custom_number should be in attributes")
|
||||||
|
assert.Equal(t, "42", val)
|
||||||
|
|
||||||
|
val, exists = identity.Attributes["custom_avg"]
|
||||||
|
assert.True(t, exists, "custom_avg should be in attributes")
|
||||||
|
assert.Contains(t, val, "98.6") // JSON number formatting might vary
|
||||||
|
|
||||||
|
val, exists = identity.Attributes["custom_object"]
|
||||||
|
assert.True(t, exists, "custom_object should be in attributes")
|
||||||
|
assert.Contains(t, val, "\"nested\":\"value\"")
|
||||||
|
|
||||||
|
// Verify structural JWT claims are excluded from attributes
|
||||||
|
excludedClaims := []string{"iss", "aud", "exp", "iat"}
|
||||||
|
for _, claim := range excludedClaims {
|
||||||
|
_, exists := identity.Attributes[claim]
|
||||||
|
assert.False(t, exists, "standard claim %s should not be in attributes", claim)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("authentication with invalid token", func(t *testing.T) {
|
t.Run("authentication with invalid token", func(t *testing.T) {
|
||||||
_, err := provider.Authenticate(context.Background(), "invalid-token")
|
_, err := provider.Authenticate(context.Background(), "invalid-token")
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
|
|||||||
@@ -133,20 +133,53 @@ func (s3iam *S3IAMIntegration) AuthenticateJWT(ctx context.Context, r *http.Requ
|
|||||||
return nil, s3err.ErrAccessDenied
|
return nil, s3err.ErrAccessDenied
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create claims map and populate with standard claims and attributes
|
||||||
|
claims := make(map[string]interface{}, len(identity.Attributes)+5)
|
||||||
|
|
||||||
|
// Add all attributes from the identity to the claims
|
||||||
|
// This makes attributes like "preferred_username" available for policy substitution
|
||||||
|
for k, v := range identity.Attributes {
|
||||||
|
claims[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add standard OIDC fields to claims so they are available as variables
|
||||||
|
// This ensures ${jwt:email}, ${jwt:name}, etc. work as documented in the wiki
|
||||||
|
if identity.Email != "" {
|
||||||
|
claims["email"] = identity.Email
|
||||||
|
}
|
||||||
|
if identity.DisplayName != "" {
|
||||||
|
claims["name"] = identity.DisplayName
|
||||||
|
}
|
||||||
|
if len(identity.Groups) > 0 {
|
||||||
|
claims["groups"] = identity.Groups
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set critical claims explicitly, overwriting any from attributes to ensure correctness.
|
||||||
|
claims["sub"] = identity.UserID
|
||||||
|
claims["role"] = identity.RoleArn
|
||||||
|
|
||||||
|
// Use real email address if available
|
||||||
|
emailAddress := identity.UserID + "@oidc.local"
|
||||||
|
if identity.Email != "" {
|
||||||
|
emailAddress = identity.Email
|
||||||
|
}
|
||||||
|
|
||||||
|
displayName := identity.UserID
|
||||||
|
if identity.DisplayName != "" {
|
||||||
|
displayName = identity.DisplayName
|
||||||
|
}
|
||||||
|
|
||||||
// Return IAM identity for OIDC token
|
// Return IAM identity for OIDC token
|
||||||
return &IAMIdentity{
|
return &IAMIdentity{
|
||||||
Name: identity.UserID,
|
Name: identity.UserID,
|
||||||
Principal: identity.RoleArn,
|
Principal: identity.RoleArn,
|
||||||
SessionToken: sessionToken,
|
SessionToken: sessionToken,
|
||||||
Account: &Account{
|
Account: &Account{
|
||||||
DisplayName: identity.UserID,
|
DisplayName: displayName,
|
||||||
EmailAddress: identity.UserID + "@oidc.local",
|
EmailAddress: emailAddress,
|
||||||
Id: identity.UserID,
|
Id: identity.UserID,
|
||||||
},
|
},
|
||||||
Claims: map[string]interface{}{
|
Claims: claims,
|
||||||
"sub": identity.UserID,
|
|
||||||
"role": identity.RoleArn,
|
|
||||||
},
|
|
||||||
}, s3err.ErrNone
|
}, s3err.ErrNone
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -655,9 +688,13 @@ func (enhanced *EnhancedS3ApiServer) AuthorizeRequest(r *http.Request, identity
|
|||||||
|
|
||||||
// OIDCIdentity represents an identity validated through OIDC
|
// OIDCIdentity represents an identity validated through OIDC
|
||||||
type OIDCIdentity struct {
|
type OIDCIdentity struct {
|
||||||
UserID string
|
UserID string
|
||||||
RoleArn string
|
RoleArn string
|
||||||
Provider string
|
Provider string
|
||||||
|
Email string
|
||||||
|
DisplayName string
|
||||||
|
Groups []string
|
||||||
|
Attributes map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
// validateExternalOIDCToken validates an external OIDC token using the STS service's secure issuer-based lookup
|
// validateExternalOIDCToken validates an external OIDC token using the STS service's secure issuer-based lookup
|
||||||
@@ -714,9 +751,13 @@ func (s3iam *S3IAMIntegration) validateExternalOIDCToken(ctx context.Context, to
|
|||||||
roleArn := s3iam.selectPrimaryRole(cleanRoles, externalIdentity)
|
roleArn := s3iam.selectPrimaryRole(cleanRoles, externalIdentity)
|
||||||
|
|
||||||
return &OIDCIdentity{
|
return &OIDCIdentity{
|
||||||
UserID: externalIdentity.UserID,
|
UserID: externalIdentity.UserID,
|
||||||
RoleArn: roleArn,
|
RoleArn: roleArn,
|
||||||
Provider: fmt.Sprintf("%T", provider), // Use provider type as identifier
|
Provider: fmt.Sprintf("%T", provider), // Use provider type as identifier
|
||||||
|
Email: externalIdentity.Email,
|
||||||
|
DisplayName: externalIdentity.DisplayName,
|
||||||
|
Groups: externalIdentity.Groups,
|
||||||
|
Attributes: externalIdentity.Attributes,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user