distributed_sts_test.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340
  1. package sts
  2. import (
  3. "context"
  4. "testing"
  5. "time"
  6. "github.com/stretchr/testify/assert"
  7. "github.com/stretchr/testify/require"
  8. )
  9. // TestDistributedSTSService verifies that multiple STS instances with identical configurations
  10. // behave consistently across distributed environments
  11. func TestDistributedSTSService(t *testing.T) {
  12. ctx := context.Background()
  13. // Common configuration for all instances
  14. commonConfig := &STSConfig{
  15. TokenDuration: FlexibleDuration{time.Hour},
  16. MaxSessionLength: FlexibleDuration{12 * time.Hour},
  17. Issuer: "distributed-sts-test",
  18. SigningKey: []byte("test-signing-key-32-characters-long"),
  19. Providers: []*ProviderConfig{
  20. {
  21. Name: "keycloak-oidc",
  22. Type: "oidc",
  23. Enabled: true,
  24. Config: map[string]interface{}{
  25. "issuer": "http://keycloak:8080/realms/seaweedfs-test",
  26. "clientId": "seaweedfs-s3",
  27. "jwksUri": "http://keycloak:8080/realms/seaweedfs-test/protocol/openid-connect/certs",
  28. },
  29. },
  30. {
  31. Name: "disabled-ldap",
  32. Type: "oidc", // Use OIDC as placeholder since LDAP isn't implemented
  33. Enabled: false,
  34. Config: map[string]interface{}{
  35. "issuer": "ldap://company.com",
  36. "clientId": "ldap-client",
  37. },
  38. },
  39. },
  40. }
  41. // Create multiple STS instances simulating distributed deployment
  42. instance1 := NewSTSService()
  43. instance2 := NewSTSService()
  44. instance3 := NewSTSService()
  45. // Initialize all instances with identical configuration
  46. err := instance1.Initialize(commonConfig)
  47. require.NoError(t, err, "Instance 1 should initialize successfully")
  48. err = instance2.Initialize(commonConfig)
  49. require.NoError(t, err, "Instance 2 should initialize successfully")
  50. err = instance3.Initialize(commonConfig)
  51. require.NoError(t, err, "Instance 3 should initialize successfully")
  52. // Manually register mock providers for testing (not available in production)
  53. mockProviderConfig := map[string]interface{}{
  54. "issuer": "http://localhost:9999",
  55. "clientId": "test-client",
  56. }
  57. mockProvider1, err := createMockOIDCProvider("test-mock-provider", mockProviderConfig)
  58. require.NoError(t, err)
  59. mockProvider2, err := createMockOIDCProvider("test-mock-provider", mockProviderConfig)
  60. require.NoError(t, err)
  61. mockProvider3, err := createMockOIDCProvider("test-mock-provider", mockProviderConfig)
  62. require.NoError(t, err)
  63. instance1.RegisterProvider(mockProvider1)
  64. instance2.RegisterProvider(mockProvider2)
  65. instance3.RegisterProvider(mockProvider3)
  66. // Verify all instances have identical provider configurations
  67. t.Run("provider_consistency", func(t *testing.T) {
  68. // All instances should have same number of providers
  69. assert.Len(t, instance1.providers, 2, "Instance 1 should have 2 enabled providers")
  70. assert.Len(t, instance2.providers, 2, "Instance 2 should have 2 enabled providers")
  71. assert.Len(t, instance3.providers, 2, "Instance 3 should have 2 enabled providers")
  72. // All instances should have same provider names
  73. instance1Names := instance1.getProviderNames()
  74. instance2Names := instance2.getProviderNames()
  75. instance3Names := instance3.getProviderNames()
  76. assert.ElementsMatch(t, instance1Names, instance2Names, "Instance 1 and 2 should have same providers")
  77. assert.ElementsMatch(t, instance2Names, instance3Names, "Instance 2 and 3 should have same providers")
  78. // Verify specific providers exist on all instances
  79. expectedProviders := []string{"keycloak-oidc", "test-mock-provider"}
  80. assert.ElementsMatch(t, instance1Names, expectedProviders, "Instance 1 should have expected providers")
  81. assert.ElementsMatch(t, instance2Names, expectedProviders, "Instance 2 should have expected providers")
  82. assert.ElementsMatch(t, instance3Names, expectedProviders, "Instance 3 should have expected providers")
  83. // Verify disabled providers are not loaded
  84. assert.NotContains(t, instance1Names, "disabled-ldap", "Disabled providers should not be loaded")
  85. assert.NotContains(t, instance2Names, "disabled-ldap", "Disabled providers should not be loaded")
  86. assert.NotContains(t, instance3Names, "disabled-ldap", "Disabled providers should not be loaded")
  87. })
  88. // Test token generation consistency across instances
  89. t.Run("token_generation_consistency", func(t *testing.T) {
  90. sessionId := "test-session-123"
  91. expiresAt := time.Now().Add(time.Hour)
  92. // Generate tokens from different instances
  93. token1, err1 := instance1.tokenGenerator.GenerateSessionToken(sessionId, expiresAt)
  94. token2, err2 := instance2.tokenGenerator.GenerateSessionToken(sessionId, expiresAt)
  95. token3, err3 := instance3.tokenGenerator.GenerateSessionToken(sessionId, expiresAt)
  96. require.NoError(t, err1, "Instance 1 token generation should succeed")
  97. require.NoError(t, err2, "Instance 2 token generation should succeed")
  98. require.NoError(t, err3, "Instance 3 token generation should succeed")
  99. // All tokens should be different (due to timestamp variations)
  100. // But they should all be valid JWTs with same signing key
  101. assert.NotEmpty(t, token1)
  102. assert.NotEmpty(t, token2)
  103. assert.NotEmpty(t, token3)
  104. })
  105. // Test token validation consistency - any instance should validate tokens from any other instance
  106. t.Run("cross_instance_token_validation", func(t *testing.T) {
  107. sessionId := "cross-validation-session"
  108. expiresAt := time.Now().Add(time.Hour)
  109. // Generate token on instance 1
  110. token, err := instance1.tokenGenerator.GenerateSessionToken(sessionId, expiresAt)
  111. require.NoError(t, err)
  112. // Validate on all instances
  113. claims1, err1 := instance1.tokenGenerator.ValidateSessionToken(token)
  114. claims2, err2 := instance2.tokenGenerator.ValidateSessionToken(token)
  115. claims3, err3 := instance3.tokenGenerator.ValidateSessionToken(token)
  116. require.NoError(t, err1, "Instance 1 should validate token from instance 1")
  117. require.NoError(t, err2, "Instance 2 should validate token from instance 1")
  118. require.NoError(t, err3, "Instance 3 should validate token from instance 1")
  119. // All instances should extract same session ID
  120. assert.Equal(t, sessionId, claims1.SessionId)
  121. assert.Equal(t, sessionId, claims2.SessionId)
  122. assert.Equal(t, sessionId, claims3.SessionId)
  123. assert.Equal(t, claims1.SessionId, claims2.SessionId)
  124. assert.Equal(t, claims2.SessionId, claims3.SessionId)
  125. })
  126. // Test provider access consistency
  127. t.Run("provider_access_consistency", func(t *testing.T) {
  128. // All instances should be able to access the same providers
  129. provider1, exists1 := instance1.providers["test-mock-provider"]
  130. provider2, exists2 := instance2.providers["test-mock-provider"]
  131. provider3, exists3 := instance3.providers["test-mock-provider"]
  132. assert.True(t, exists1, "Instance 1 should have test-mock-provider")
  133. assert.True(t, exists2, "Instance 2 should have test-mock-provider")
  134. assert.True(t, exists3, "Instance 3 should have test-mock-provider")
  135. assert.Equal(t, provider1.Name(), provider2.Name())
  136. assert.Equal(t, provider2.Name(), provider3.Name())
  137. // Test authentication with the mock provider on all instances
  138. testToken := "valid_test_token"
  139. identity1, err1 := provider1.Authenticate(ctx, testToken)
  140. identity2, err2 := provider2.Authenticate(ctx, testToken)
  141. identity3, err3 := provider3.Authenticate(ctx, testToken)
  142. require.NoError(t, err1, "Instance 1 provider should authenticate successfully")
  143. require.NoError(t, err2, "Instance 2 provider should authenticate successfully")
  144. require.NoError(t, err3, "Instance 3 provider should authenticate successfully")
  145. // All instances should return identical identity information
  146. assert.Equal(t, identity1.UserID, identity2.UserID)
  147. assert.Equal(t, identity2.UserID, identity3.UserID)
  148. assert.Equal(t, identity1.Email, identity2.Email)
  149. assert.Equal(t, identity2.Email, identity3.Email)
  150. assert.Equal(t, identity1.Provider, identity2.Provider)
  151. assert.Equal(t, identity2.Provider, identity3.Provider)
  152. })
  153. }
  154. // TestSTSConfigurationValidation tests configuration validation for distributed deployments
  155. func TestSTSConfigurationValidation(t *testing.T) {
  156. t.Run("consistent_signing_keys_required", func(t *testing.T) {
  157. // Different signing keys should result in incompatible token validation
  158. config1 := &STSConfig{
  159. TokenDuration: FlexibleDuration{time.Hour},
  160. MaxSessionLength: FlexibleDuration{12 * time.Hour},
  161. Issuer: "test-sts",
  162. SigningKey: []byte("signing-key-1-32-characters-long"),
  163. }
  164. config2 := &STSConfig{
  165. TokenDuration: FlexibleDuration{time.Hour},
  166. MaxSessionLength: FlexibleDuration{12 * time.Hour},
  167. Issuer: "test-sts",
  168. SigningKey: []byte("signing-key-2-32-characters-long"), // Different key!
  169. }
  170. instance1 := NewSTSService()
  171. instance2 := NewSTSService()
  172. err1 := instance1.Initialize(config1)
  173. err2 := instance2.Initialize(config2)
  174. require.NoError(t, err1)
  175. require.NoError(t, err2)
  176. // Generate token on instance 1
  177. sessionId := "test-session"
  178. expiresAt := time.Now().Add(time.Hour)
  179. token, err := instance1.tokenGenerator.GenerateSessionToken(sessionId, expiresAt)
  180. require.NoError(t, err)
  181. // Instance 1 should validate its own token
  182. _, err = instance1.tokenGenerator.ValidateSessionToken(token)
  183. assert.NoError(t, err, "Instance 1 should validate its own token")
  184. // Instance 2 should reject token from instance 1 (different signing key)
  185. _, err = instance2.tokenGenerator.ValidateSessionToken(token)
  186. assert.Error(t, err, "Instance 2 should reject token with different signing key")
  187. })
  188. t.Run("consistent_issuer_required", func(t *testing.T) {
  189. // Different issuers should result in incompatible tokens
  190. commonSigningKey := []byte("shared-signing-key-32-characters-lo")
  191. config1 := &STSConfig{
  192. TokenDuration: FlexibleDuration{time.Hour},
  193. MaxSessionLength: FlexibleDuration{12 * time.Hour},
  194. Issuer: "sts-instance-1",
  195. SigningKey: commonSigningKey,
  196. }
  197. config2 := &STSConfig{
  198. TokenDuration: FlexibleDuration{time.Hour},
  199. MaxSessionLength: FlexibleDuration{12 * time.Hour},
  200. Issuer: "sts-instance-2", // Different issuer!
  201. SigningKey: commonSigningKey,
  202. }
  203. instance1 := NewSTSService()
  204. instance2 := NewSTSService()
  205. err1 := instance1.Initialize(config1)
  206. err2 := instance2.Initialize(config2)
  207. require.NoError(t, err1)
  208. require.NoError(t, err2)
  209. // Generate token on instance 1
  210. sessionId := "test-session"
  211. expiresAt := time.Now().Add(time.Hour)
  212. token, err := instance1.tokenGenerator.GenerateSessionToken(sessionId, expiresAt)
  213. require.NoError(t, err)
  214. // Instance 2 should reject token due to issuer mismatch
  215. // (Even though signing key is the same, issuer validation will fail)
  216. _, err = instance2.tokenGenerator.ValidateSessionToken(token)
  217. assert.Error(t, err, "Instance 2 should reject token with different issuer")
  218. })
  219. }
  220. // TestProviderFactoryDistributed tests the provider factory in distributed scenarios
  221. func TestProviderFactoryDistributed(t *testing.T) {
  222. factory := NewProviderFactory()
  223. // Simulate configuration that would be identical across all instances
  224. configs := []*ProviderConfig{
  225. {
  226. Name: "production-keycloak",
  227. Type: "oidc",
  228. Enabled: true,
  229. Config: map[string]interface{}{
  230. "issuer": "https://keycloak.company.com/realms/seaweedfs",
  231. "clientId": "seaweedfs-prod",
  232. "clientSecret": "super-secret-key",
  233. "jwksUri": "https://keycloak.company.com/realms/seaweedfs/protocol/openid-connect/certs",
  234. "scopes": []string{"openid", "profile", "email", "roles"},
  235. },
  236. },
  237. {
  238. Name: "backup-oidc",
  239. Type: "oidc",
  240. Enabled: false, // Disabled by default
  241. Config: map[string]interface{}{
  242. "issuer": "https://backup-oidc.company.com",
  243. "clientId": "seaweedfs-backup",
  244. },
  245. },
  246. }
  247. // Create providers multiple times (simulating multiple instances)
  248. providers1, err1 := factory.LoadProvidersFromConfig(configs)
  249. providers2, err2 := factory.LoadProvidersFromConfig(configs)
  250. providers3, err3 := factory.LoadProvidersFromConfig(configs)
  251. require.NoError(t, err1, "First load should succeed")
  252. require.NoError(t, err2, "Second load should succeed")
  253. require.NoError(t, err3, "Third load should succeed")
  254. // All instances should have same provider counts
  255. assert.Len(t, providers1, 1, "First instance should have 1 enabled provider")
  256. assert.Len(t, providers2, 1, "Second instance should have 1 enabled provider")
  257. assert.Len(t, providers3, 1, "Third instance should have 1 enabled provider")
  258. // All instances should have same provider names
  259. names1 := make([]string, 0, len(providers1))
  260. names2 := make([]string, 0, len(providers2))
  261. names3 := make([]string, 0, len(providers3))
  262. for name := range providers1 {
  263. names1 = append(names1, name)
  264. }
  265. for name := range providers2 {
  266. names2 = append(names2, name)
  267. }
  268. for name := range providers3 {
  269. names3 = append(names3, name)
  270. }
  271. assert.ElementsMatch(t, names1, names2, "Instance 1 and 2 should have same provider names")
  272. assert.ElementsMatch(t, names2, names3, "Instance 2 and 3 should have same provider names")
  273. // Verify specific providers
  274. expectedProviders := []string{"production-keycloak"}
  275. assert.ElementsMatch(t, names1, expectedProviders, "Should have expected enabled providers")
  276. // Verify disabled providers are not included
  277. assert.NotContains(t, names1, "backup-oidc", "Disabled providers should not be loaded")
  278. assert.NotContains(t, names2, "backup-oidc", "Disabled providers should not be loaded")
  279. assert.NotContains(t, names3, "backup-oidc", "Disabled providers should not be loaded")
  280. }