Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 82 additions & 12 deletions internal/api/handlers/v0/auth/oidc.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,29 +210,99 @@ func (h *OIDCHandler) validateExtraClaims(claims *OIDCClaims) error {
return nil // No extra validation required
}

// Parse extra claims configuration
var extraClaimsRules []map[string]any
if err := json.Unmarshal([]byte(h.config.OIDCExtraClaims), &extraClaimsRules); err != nil {
return fmt.Errorf("invalid extra claims configuration: %w", err)
extraClaimsRules, err := h.parseExtraClaimsRules()
if err != nil {
return fmt.Errorf("failed to parse extra claims rules: %w", err)
}

// Validate each rule
for _, rule := range extraClaimsRules {
for key, expectedValue := range rule {
actualValue, exists := claims.ExtraClaims[key]
if !exists {
return fmt.Errorf("claim validation failed: required claim %s not found", key)
}
if err := h.validateSingleRule(claims, rule); err != nil {
return fmt.Errorf("claim validation failed for rule %v: %w", rule, err)
}
}

if actualValue != expectedValue {
return fmt.Errorf("claim validation failed: %s expected %v, got %v", key, expectedValue, actualValue)
}
return nil
}

// parseExtraClaimsRules parses the extra claims configuration from JSON
func (h *OIDCHandler) parseExtraClaimsRules() ([]map[string]any, error) {
var extraClaimsRules []map[string]any
if err := json.Unmarshal([]byte(h.config.OIDCExtraClaims), &extraClaimsRules); err != nil {
return nil, fmt.Errorf("invalid extra claims configuration: %w", err)
}
return extraClaimsRules, nil
}

// validateSingleRule validates a single claim rule against the OIDC claims
func (h *OIDCHandler) validateSingleRule(claims *OIDCClaims, rule map[string]any) error {
for key, expectedValue := range rule {
actualValue, exists := claims.ExtraClaims[key]
if !exists {
return fmt.Errorf("claim validation failed: required claim %s not found", key)
}

if err := h.compareClaimValues(key, actualValue, expectedValue); err != nil {
return err
}
}
return nil
}

// compareClaimValues compares actual and expected claim values with support for arrays
func (h *OIDCHandler) compareClaimValues(key string, actualValue, expectedValue any) error {
actualArray, actualIsArray := actualValue.([]any)
if actualIsArray {
return h.compareArrayClaimValues(key, actualArray, expectedValue)
}

// Compare scalar values
if actualValue != expectedValue {
return fmt.Errorf("claim validation failed: %s expected %v, got %v", key, expectedValue, actualValue)
}

return nil
}

// compareArrayClaimValues handles comparison when actual value is an array
func (h *OIDCHandler) compareArrayClaimValues(key string, actualArray []any, expectedValue any) error {
expectedArray, expectedIsArray := expectedValue.([]any)
if expectedIsArray {
return h.compareArrayToArray(key, actualArray, expectedArray)
}

return h.compareArrayToScalar(key, actualArray, expectedValue)
}

// compareArrayToArray compares two arrays looking for any matching values
func (h *OIDCHandler) compareArrayToArray(key string, actualArray, expectedArray []any) error {
for _, actVal := range actualArray {
for _, expVal := range expectedArray {
if actVal == expVal {
return nil // Found a match
}
}
}
return fmt.Errorf("claim validation failed: %s no matching values found between actual %v and expected %v", key, actualArray, expectedArray)
}

// compareArrayToScalar compares an array to a scalar value
func (h *OIDCHandler) compareArrayToScalar(key string, actualArray []any, expectedValue any) error {
if len(actualArray) == 1 {
// Normalize single-element arrays to scalars and compare
return h.compareClaimValues(key, actualArray[0], expectedValue)
}

// Check if expected value exists in the array
for _, item := range actualArray {
if item == expectedValue {
return nil // Found a match
}
}

return fmt.Errorf("claim validation failed: %s expected %v to be in array %v", key, expectedValue, actualArray)
}

// buildPermissions builds permissions based on OIDC claims and configuration
func (h *OIDCHandler) buildPermissions(_ *OIDCClaims) []auth.Permission {
var permissions []auth.Permission
Expand Down
180 changes: 180 additions & 0 deletions internal/api/handlers/v0/auth/oidc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading