You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							193 lines
						
					
					
						
							6.0 KiB
						
					
					
				
			
		
		
		
			
			
			
		
		
	
	
							193 lines
						
					
					
						
							6.0 KiB
						
					
					
				
								package sts
							 | 
						|
								
							 | 
						|
								import (
							 | 
						|
									"context"
							 | 
						|
									"fmt"
							 | 
						|
									"strings"
							 | 
						|
									"testing"
							 | 
						|
									"time"
							 | 
						|
								
							 | 
						|
									"github.com/golang-jwt/jwt/v5"
							 | 
						|
									"github.com/seaweedfs/seaweedfs/weed/iam/providers"
							 | 
						|
									"github.com/stretchr/testify/assert"
							 | 
						|
									"github.com/stretchr/testify/require"
							 | 
						|
								)
							 | 
						|
								
							 | 
						|
								// TestSecurityIssuerToProviderMapping tests the security fix that ensures JWT tokens
							 | 
						|
								// with specific issuer claims can only be validated by the provider registered for that issuer
							 | 
						|
								func TestSecurityIssuerToProviderMapping(t *testing.T) {
							 | 
						|
									ctx := context.Background()
							 | 
						|
								
							 | 
						|
									// Create STS service with two mock providers
							 | 
						|
									service := NewSTSService()
							 | 
						|
									config := &STSConfig{
							 | 
						|
										TokenDuration:    FlexibleDuration{time.Hour},
							 | 
						|
										MaxSessionLength: FlexibleDuration{time.Hour * 12},
							 | 
						|
										Issuer:           "test-sts",
							 | 
						|
										SigningKey:       []byte("test-signing-key-32-characters-long"),
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									err := service.Initialize(config)
							 | 
						|
									require.NoError(t, err)
							 | 
						|
								
							 | 
						|
									// Set up mock trust policy validator
							 | 
						|
									mockValidator := &MockTrustPolicyValidator{}
							 | 
						|
									service.SetTrustPolicyValidator(mockValidator)
							 | 
						|
								
							 | 
						|
									// Create two mock providers with different issuers
							 | 
						|
									providerA := &MockIdentityProviderWithIssuer{
							 | 
						|
										name:   "provider-a",
							 | 
						|
										issuer: "https://provider-a.com",
							 | 
						|
										validTokens: map[string]bool{
							 | 
						|
											"token-for-provider-a": true,
							 | 
						|
										},
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									providerB := &MockIdentityProviderWithIssuer{
							 | 
						|
										name:   "provider-b",
							 | 
						|
										issuer: "https://provider-b.com",
							 | 
						|
										validTokens: map[string]bool{
							 | 
						|
											"token-for-provider-b": true,
							 | 
						|
										},
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Register both providers
							 | 
						|
									err = service.RegisterProvider(providerA)
							 | 
						|
									require.NoError(t, err)
							 | 
						|
									err = service.RegisterProvider(providerB)
							 | 
						|
									require.NoError(t, err)
							 | 
						|
								
							 | 
						|
									// Create JWT tokens with specific issuer claims
							 | 
						|
									tokenForProviderA := createTestJWT(t, "https://provider-a.com", "user-a")
							 | 
						|
									tokenForProviderB := createTestJWT(t, "https://provider-b.com", "user-b")
							 | 
						|
								
							 | 
						|
									t.Run("jwt_token_with_issuer_a_only_validated_by_provider_a", func(t *testing.T) {
							 | 
						|
										// This should succeed - token has issuer A and provider A is registered
							 | 
						|
										identity, provider, err := service.validateWebIdentityToken(ctx, tokenForProviderA)
							 | 
						|
										assert.NoError(t, err)
							 | 
						|
										assert.NotNil(t, identity)
							 | 
						|
										assert.Equal(t, "provider-a", provider.Name())
							 | 
						|
									})
							 | 
						|
								
							 | 
						|
									t.Run("jwt_token_with_issuer_b_only_validated_by_provider_b", func(t *testing.T) {
							 | 
						|
										// This should succeed - token has issuer B and provider B is registered
							 | 
						|
										identity, provider, err := service.validateWebIdentityToken(ctx, tokenForProviderB)
							 | 
						|
										assert.NoError(t, err)
							 | 
						|
										assert.NotNil(t, identity)
							 | 
						|
										assert.Equal(t, "provider-b", provider.Name())
							 | 
						|
									})
							 | 
						|
								
							 | 
						|
									t.Run("jwt_token_with_unregistered_issuer_fails", func(t *testing.T) {
							 | 
						|
										// Create token with unregistered issuer
							 | 
						|
										tokenWithUnknownIssuer := createTestJWT(t, "https://unknown-issuer.com", "user-x")
							 | 
						|
								
							 | 
						|
										// This should fail - no provider registered for this issuer
							 | 
						|
										identity, provider, err := service.validateWebIdentityToken(ctx, tokenWithUnknownIssuer)
							 | 
						|
										assert.Error(t, err)
							 | 
						|
										assert.Nil(t, identity)
							 | 
						|
										assert.Nil(t, provider)
							 | 
						|
										assert.Contains(t, err.Error(), "no identity provider registered for issuer: https://unknown-issuer.com")
							 | 
						|
									})
							 | 
						|
								
							 | 
						|
									t.Run("non_jwt_tokens_are_rejected", func(t *testing.T) {
							 | 
						|
										// Non-JWT tokens should be rejected - no fallback mechanism exists for security
							 | 
						|
										identity, provider, err := service.validateWebIdentityToken(ctx, "token-for-provider-a")
							 | 
						|
										assert.Error(t, err)
							 | 
						|
										assert.Nil(t, identity)
							 | 
						|
										assert.Nil(t, provider)
							 | 
						|
										assert.Contains(t, err.Error(), "web identity token must be a valid JWT token")
							 | 
						|
									})
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// createTestJWT creates a test JWT token with the specified issuer and subject
							 | 
						|
								func createTestJWT(t *testing.T, issuer, subject string) string {
							 | 
						|
									token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
							 | 
						|
										"iss": issuer,
							 | 
						|
										"sub": subject,
							 | 
						|
										"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)
							 | 
						|
									return tokenString
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// MockIdentityProviderWithIssuer is a mock provider that supports issuer mapping
							 | 
						|
								type MockIdentityProviderWithIssuer struct {
							 | 
						|
									name        string
							 | 
						|
									issuer      string
							 | 
						|
									validTokens map[string]bool
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								func (m *MockIdentityProviderWithIssuer) Name() string {
							 | 
						|
									return m.name
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								func (m *MockIdentityProviderWithIssuer) GetIssuer() string {
							 | 
						|
									return m.issuer
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								func (m *MockIdentityProviderWithIssuer) Initialize(config interface{}) error {
							 | 
						|
									return nil
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								func (m *MockIdentityProviderWithIssuer) Authenticate(ctx context.Context, token string) (*providers.ExternalIdentity, error) {
							 | 
						|
									// For JWT tokens, parse and validate the token format
							 | 
						|
									if len(token) > 50 && strings.Contains(token, ".") {
							 | 
						|
										// This looks like a JWT - parse it to get the subject
							 | 
						|
										parsedToken, _, err := new(jwt.Parser).ParseUnverified(token, jwt.MapClaims{})
							 | 
						|
										if err != nil {
							 | 
						|
											return nil, fmt.Errorf("invalid JWT token")
							 | 
						|
										}
							 | 
						|
								
							 | 
						|
										claims, ok := parsedToken.Claims.(jwt.MapClaims)
							 | 
						|
										if !ok {
							 | 
						|
											return nil, fmt.Errorf("invalid claims")
							 | 
						|
										}
							 | 
						|
								
							 | 
						|
										issuer, _ := claims["iss"].(string)
							 | 
						|
										subject, _ := claims["sub"].(string)
							 | 
						|
								
							 | 
						|
										// Verify the issuer matches what we expect
							 | 
						|
										if issuer != m.issuer {
							 | 
						|
											return nil, fmt.Errorf("token issuer %s does not match provider issuer %s", issuer, m.issuer)
							 | 
						|
										}
							 | 
						|
								
							 | 
						|
										return &providers.ExternalIdentity{
							 | 
						|
											UserID:   subject,
							 | 
						|
											Email:    subject + "@" + m.name + ".com",
							 | 
						|
											Provider: m.name,
							 | 
						|
										}, nil
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// For non-JWT tokens, check our simple token list
							 | 
						|
									if m.validTokens[token] {
							 | 
						|
										return &providers.ExternalIdentity{
							 | 
						|
											UserID:   "test-user",
							 | 
						|
											Email:    "test@" + m.name + ".com",
							 | 
						|
											Provider: m.name,
							 | 
						|
										}, nil
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									return nil, fmt.Errorf("invalid token")
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								func (m *MockIdentityProviderWithIssuer) GetUserInfo(ctx context.Context, userID string) (*providers.ExternalIdentity, error) {
							 | 
						|
									return &providers.ExternalIdentity{
							 | 
						|
										UserID:   userID,
							 | 
						|
										Email:    userID + "@" + m.name + ".com",
							 | 
						|
										Provider: m.name,
							 | 
						|
									}, nil
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								func (m *MockIdentityProviderWithIssuer) ValidateToken(ctx context.Context, token string) (*providers.TokenClaims, error) {
							 | 
						|
									if m.validTokens[token] {
							 | 
						|
										return &providers.TokenClaims{
							 | 
						|
											Subject: "test-user",
							 | 
						|
											Issuer:  m.issuer,
							 | 
						|
										}, nil
							 | 
						|
									}
							 | 
						|
									return nil, fmt.Errorf("invalid token")
							 | 
						|
								}
							 |