diff --git a/internal/api/handlers/v0/auth/oidc.go b/internal/api/handlers/v0/auth/oidc.go index 0cef699b3..ef258fc1b 100644 --- a/internal/api/handlers/v0/auth/oidc.go +++ b/internal/api/handlers/v0/auth/oidc.go @@ -224,6 +224,50 @@ func (h *OIDCHandler) validateExtraClaims(claims *OIDCClaims) error { return fmt.Errorf("claim validation failed: required claim %s not found", key) } + //Handle array values + if actualArray, ok := actualValue.([]any); ok { + // Check if expected value is also an array + if expectedArray, ok := expectedValue.([]any); ok { + // Both are arrays - check if any actual value exists in expected array + found := false + for _, actVal := range actualArray { + for _, expVal := range expectedArray { + if actVal == expVal { + found = true + break + } + } + if found { + break + } + } + if !found { + return fmt.Errorf("claim validation failed: %s no matching values found between actual %v and expected %v", key, actualArray, expectedArray) + } + continue + } + + // Expected is scalar, actual is array + if len(actualArray) == 1 { + // Normalize single-element arrays to scalars + actualValue = actualArray[0] + } else if len(actualArray) > 1 { + // Check if expected value exists in the array + found := false + for _, item := range actualArray { + if item == expectedValue { + found = true + break + } + } + if !found { + return fmt.Errorf("claim validation failed: %s expected %v to be in array %v", key, expectedValue, actualArray) + } + continue + } + } + + // Compare values if both are scalars if actualValue != expectedValue { return fmt.Errorf("claim validation failed: %s expected %v, got %v", key, expectedValue, actualValue) } diff --git a/internal/api/handlers/v0/auth/oidc_test.go b/internal/api/handlers/v0/auth/oidc_test.go index 4ad8acb1d..24efde5a2 100644 --- a/internal/api/handlers/v0/auth/oidc_test.go +++ b/internal/api/handlers/v0/auth/oidc_test.go @@ -81,6 +81,186 @@ func TestOIDCHandler_ExchangeToken(t *testing.T) { token: "invalid-domain-token", expectedError: true, }, + { + name: "successful validation with extra claim 'client_id'", + config: &config.Config{ + OIDCEnabled: true, + OIDCIssuer: "https://cigna.oktapreview.com", + OIDCClientID: "api://glbcore", + OIDCExtraClaims: `[{"client_id":"matched_client_id_value"}]`, + OIDCPublishPerms: "*", + JWTPrivateKey: "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", + }, + mockValidator: &MockGenericOIDCValidator{ + validateFunc: func(_ context.Context, _ string) (*auth.OIDCClaims, error) { + return &auth.OIDCClaims{ + Subject: "user-subject-123", + ExtraClaims: map[string]any{ + "email": "user@cigna.com", + "client_id": "matched_client_id_value", + }, + }, nil + }, + }, + token: "valid-okta-token", + expectedError: false, + }, + { + name: "failed validation with wrong extra claim 'client_id'", + config: &config.Config{ + OIDCEnabled: true, + OIDCIssuer: "https://cigna.oktapreview.com", + OIDCClientID: "api://glbcore", + OIDCExtraClaims: `[{"client_id":"client_id_value"}]`, + OIDCPublishPerms: "*", + JWTPrivateKey: "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", + }, + mockValidator: &MockGenericOIDCValidator{ + validateFunc: func(_ context.Context, _ string) (*auth.OIDCClaims, error) { + return &auth.OIDCClaims{ + Subject: "user-subject-123", + ExtraClaims: map[string]any{ + "email": "user@cigna.com", + "client_id": "wrong_client_id_value", + }, + }, nil + }, + }, + token: "invalid-client-id-token", + expectedError: true, + }, + { + name: "successful validation with array claim - scalar expected value in array", + config: &config.Config{ + OIDCEnabled: true, + OIDCIssuer: "https://accounts.google.com", + OIDCClientID: "test-client-id", + OIDCExtraClaims: `[{"groups":"admin"}]`, + OIDCPublishPerms: "*", + JWTPrivateKey: "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", + }, + mockValidator: &MockGenericOIDCValidator{ + validateFunc: func(_ context.Context, _ string) (*auth.OIDCClaims, error) { + return &auth.OIDCClaims{ + ExtraClaims: map[string]any{ + "groups": []any{"admin", "users", "developers"}, + }, + }, nil + }, + }, + token: "valid-array-claim-token", + expectedError: false, + }, + { + name: "failed validation with array claim - scalar not in array", + config: &config.Config{ + OIDCEnabled: true, + OIDCIssuer: "https://accounts.google.com", + OIDCClientID: "test-client-id", + OIDCExtraClaims: `[{"groups":"super-admin"}]`, + OIDCPublishPerms: "*", + JWTPrivateKey: "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", + }, + mockValidator: &MockGenericOIDCValidator{ + validateFunc: func(_ context.Context, _ string) (*auth.OIDCClaims, error) { + return &auth.OIDCClaims{ + ExtraClaims: map[string]any{ + "groups": []any{"admin", "users", "developers"}, + }, + }, nil + }, + }, + token: "invalid-array-claim-token", + expectedError: true, + }, + { + name: "successful validation with array to array comparison - overlapping values", + config: &config.Config{ + OIDCEnabled: true, + OIDCIssuer: "https://accounts.google.com", + OIDCClientID: "test-client-id", + OIDCExtraClaims: `[{"roles":["admin","moderator"]}]`, + OIDCPublishPerms: "*", + JWTPrivateKey: "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", + }, + mockValidator: &MockGenericOIDCValidator{ + validateFunc: func(_ context.Context, _ string) (*auth.OIDCClaims, error) { + return &auth.OIDCClaims{ + ExtraClaims: map[string]any{ + "roles": []any{"admin", "users"}, + }, + }, nil + }, + }, + token: "valid-array-array-token", + expectedError: false, + }, + { + name: "failed validation with array to array comparison - no overlapping values", + config: &config.Config{ + OIDCEnabled: true, + OIDCIssuer: "https://accounts.google.com", + OIDCClientID: "test-client-id", + OIDCExtraClaims: `[{"roles":["super-admin","owner"]}]`, + OIDCPublishPerms: "*", + JWTPrivateKey: "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", + }, + mockValidator: &MockGenericOIDCValidator{ + validateFunc: func(_ context.Context, _ string) (*auth.OIDCClaims, error) { + return &auth.OIDCClaims{ + ExtraClaims: map[string]any{ + "roles": []any{"admin", "users"}, + }, + }, nil + }, + }, + token: "invalid-array-array-token", + expectedError: true, + }, + { + name: "successful validation with single-element array normalization", + config: &config.Config{ + OIDCEnabled: true, + OIDCIssuer: "https://accounts.google.com", + OIDCClientID: "test-client-id", + OIDCExtraClaims: `[{"department":"engineering"}]`, + OIDCPublishPerms: "*", + JWTPrivateKey: "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", + }, + mockValidator: &MockGenericOIDCValidator{ + validateFunc: func(_ context.Context, _ string) (*auth.OIDCClaims, error) { + return &auth.OIDCClaims{ + ExtraClaims: map[string]any{ + "department": []any{"engineering"}, // Single element array + }, + }, nil + }, + }, + token: "valid-single-array-token", + expectedError: false, + }, + { + name: "failed validation with missing claim", + config: &config.Config{ + OIDCEnabled: true, + OIDCIssuer: "https://accounts.google.com", + OIDCClientID: "test-client-id", + OIDCExtraClaims: `[{"required_claim":"expected_value"}]`, + OIDCPublishPerms: "*", + JWTPrivateKey: "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", + }, + mockValidator: &MockGenericOIDCValidator{ + validateFunc: func(_ context.Context, _ string) (*auth.OIDCClaims, error) { + return &auth.OIDCClaims{ + ExtraClaims: map[string]any{ + "other_claim": "some_value", + }, + }, nil + }, + }, + token: "missing-claim-token", + expectedError: true, + }, } for _, tt := range tests {