mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-06-05 22:44:17 +00:00
c3f23cb99b
* Resolve issue with opaque tokens not being parsed correctly * Increase test coverage * Further improvements to test coverage and code quality * Add new providers. * fixup! Add new providers. * Cleanup. * fixup! Cleanup. * fixup! fixup! Cleanup. * fixup! fixup! fixup! Cleanup. * fixup! fixup! fixup! fixup! Cleanup. * Memory management optimisation 24 bytes per Put < 256-4096 bytes per buffer allocation avoided (10-170x difference) * Pooling cleanup.
564 lines
14 KiB
Go
564 lines
14 KiB
Go
package providers
|
|
|
|
import (
|
|
"net/url"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// TestNewConfigValidator tests the creation of a ConfigValidator
|
|
func TestNewConfigValidator(t *testing.T) {
|
|
validator := NewConfigValidator()
|
|
if validator == nil {
|
|
t.Error("expected non-nil validator")
|
|
}
|
|
}
|
|
|
|
// TestValidateIssuerURL tests the ValidateIssuerURL function
|
|
func TestValidateIssuerURL(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
issuerURL string
|
|
wantErr bool
|
|
errMsg string
|
|
}{
|
|
{
|
|
name: "valid https URL",
|
|
issuerURL: "https://accounts.google.com",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "valid http URL",
|
|
issuerURL: "http://localhost:8080",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "valid URL with path",
|
|
issuerURL: "https://login.microsoftonline.com/tenant-id/v2.0",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "empty URL",
|
|
issuerURL: "",
|
|
wantErr: true,
|
|
errMsg: "issuer URL cannot be empty",
|
|
},
|
|
{
|
|
name: "URL without scheme",
|
|
issuerURL: "accounts.google.com",
|
|
wantErr: true,
|
|
errMsg: "issuer URL must include scheme",
|
|
},
|
|
{
|
|
name: "URL with invalid scheme",
|
|
issuerURL: "ftp://example.com",
|
|
wantErr: true,
|
|
errMsg: "issuer URL scheme must be http or https",
|
|
},
|
|
{
|
|
name: "URL without host",
|
|
issuerURL: "https://",
|
|
wantErr: true,
|
|
errMsg: "issuer URL must include host",
|
|
},
|
|
{
|
|
name: "malformed URL",
|
|
issuerURL: "ht!tp://[invalid",
|
|
wantErr: true,
|
|
errMsg: "invalid issuer URL format",
|
|
},
|
|
{
|
|
name: "URL with port",
|
|
issuerURL: "https://auth.example.com:443/oauth",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "URL with query parameters",
|
|
issuerURL: "https://auth.example.com?tenant=123",
|
|
wantErr: false,
|
|
},
|
|
}
|
|
|
|
validator := NewConfigValidator()
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := validator.ValidateIssuerURL(tt.issuerURL)
|
|
|
|
if tt.wantErr {
|
|
if err == nil {
|
|
t.Error("expected error, got nil")
|
|
} else if tt.errMsg != "" && !strings.Contains(err.Error(), tt.errMsg) {
|
|
t.Errorf("expected error containing %q, got %q", tt.errMsg, err.Error())
|
|
}
|
|
} else {
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestValidateClientID tests the ValidateClientID function
|
|
func TestValidateClientID(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
clientID string
|
|
wantErr bool
|
|
errMsg string
|
|
}{
|
|
{
|
|
name: "valid client ID",
|
|
clientID: "my-application-client",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "valid UUID client ID",
|
|
clientID: "123e4567-e89b-12d3-a456-426614174000",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "empty client ID",
|
|
clientID: "",
|
|
wantErr: true,
|
|
errMsg: "client ID cannot be empty",
|
|
},
|
|
{
|
|
name: "too short client ID",
|
|
clientID: "ab",
|
|
wantErr: true,
|
|
errMsg: "client ID appears to be too short",
|
|
},
|
|
{
|
|
name: "minimum length client ID",
|
|
clientID: "abc",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "client ID with special characters",
|
|
clientID: "client-id_123.app",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "long client ID",
|
|
clientID: strings.Repeat("a", 255),
|
|
wantErr: false,
|
|
},
|
|
}
|
|
|
|
validator := NewConfigValidator()
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := validator.ValidateClientID(tt.clientID)
|
|
|
|
if tt.wantErr {
|
|
if err == nil {
|
|
t.Error("expected error, got nil")
|
|
} else if tt.errMsg != "" && !strings.Contains(err.Error(), tt.errMsg) {
|
|
t.Errorf("expected error containing %q, got %q", tt.errMsg, err.Error())
|
|
}
|
|
} else {
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestValidateScopes tests the ValidateScopes function
|
|
func TestValidateScopes(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
scopes []string
|
|
wantErr bool
|
|
errMsg string
|
|
}{
|
|
{
|
|
name: "valid scopes with openid",
|
|
scopes: []string{"openid", "email", "profile"},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "only openid scope",
|
|
scopes: []string{"openid"},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "openid with whitespace",
|
|
scopes: []string{" openid ", "email"},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "empty scopes",
|
|
scopes: []string{},
|
|
wantErr: true,
|
|
errMsg: "at least one scope must be provided",
|
|
},
|
|
{
|
|
name: "nil scopes",
|
|
scopes: nil,
|
|
wantErr: true,
|
|
errMsg: "at least one scope must be provided",
|
|
},
|
|
{
|
|
name: "missing openid scope",
|
|
scopes: []string{"email", "profile"},
|
|
wantErr: true,
|
|
errMsg: "'openid' scope is required",
|
|
},
|
|
{
|
|
name: "duplicate openid scope",
|
|
scopes: []string{"openid", "openid", "email"},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "custom scopes with openid",
|
|
scopes: []string{"openid", "api:read", "api:write"},
|
|
wantErr: false,
|
|
},
|
|
}
|
|
|
|
validator := NewConfigValidator()
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := validator.ValidateScopes(tt.scopes)
|
|
|
|
if tt.wantErr {
|
|
if err == nil {
|
|
t.Error("expected error, got nil")
|
|
} else if tt.errMsg != "" && !strings.Contains(err.Error(), tt.errMsg) {
|
|
t.Errorf("expected error containing %q, got %q", tt.errMsg, err.Error())
|
|
}
|
|
} else {
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestValidateRedirectURL tests the ValidateRedirectURL function
|
|
func TestValidateRedirectURL(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
redirectURL string
|
|
wantErr bool
|
|
errMsg string
|
|
}{
|
|
{
|
|
name: "valid https redirect URL",
|
|
redirectURL: "https://example.com/callback",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "valid http redirect URL",
|
|
redirectURL: "http://localhost:3000/auth/callback",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "empty redirect URL",
|
|
redirectURL: "",
|
|
wantErr: true,
|
|
errMsg: "redirect URL cannot be empty",
|
|
},
|
|
{
|
|
name: "redirect URL without scheme",
|
|
redirectURL: "example.com/callback",
|
|
wantErr: true,
|
|
errMsg: "redirect URL must include scheme",
|
|
},
|
|
{
|
|
name: "malformed redirect URL",
|
|
redirectURL: "ht!tp://[invalid",
|
|
wantErr: true,
|
|
errMsg: "invalid redirect URL format",
|
|
},
|
|
{
|
|
name: "redirect URL with query parameters",
|
|
redirectURL: "https://example.com/callback?state=abc",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "redirect URL with fragment",
|
|
redirectURL: "https://example.com/callback#section",
|
|
wantErr: false,
|
|
},
|
|
}
|
|
|
|
validator := NewConfigValidator()
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := validator.ValidateRedirectURL(tt.redirectURL)
|
|
|
|
if tt.wantErr {
|
|
if err == nil {
|
|
t.Error("expected error, got nil")
|
|
} else if tt.errMsg != "" && !strings.Contains(err.Error(), tt.errMsg) {
|
|
t.Errorf("expected error containing %q, got %q", tt.errMsg, err.Error())
|
|
}
|
|
} else {
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestValidateProviderSpecificConfig tests provider-specific configuration validation
|
|
func TestValidateProviderSpecificConfig(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
provider OIDCProvider
|
|
config map[string]interface{}
|
|
wantErr bool
|
|
errMsg string
|
|
}{
|
|
{
|
|
name: "valid Google config",
|
|
provider: NewGoogleProvider(),
|
|
config: map[string]interface{}{
|
|
"issuer_url": "https://accounts.google.com",
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "invalid Google config - wrong issuer",
|
|
provider: NewGoogleProvider(),
|
|
config: map[string]interface{}{
|
|
"issuer_url": "https://example.com",
|
|
},
|
|
wantErr: true,
|
|
errMsg: "google provider requires issuer URL to contain accounts.google.com",
|
|
},
|
|
{
|
|
name: "valid Azure config with tenant ID",
|
|
provider: NewAzureProvider(),
|
|
config: map[string]interface{}{
|
|
"issuer_url": "https://login.microsoftonline.com/12345678-1234-1234-1234-123456789012/v2.0",
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "invalid Azure config - wrong domain",
|
|
provider: NewAzureProvider(),
|
|
config: map[string]interface{}{
|
|
"issuer_url": "https://example.com/tenant",
|
|
},
|
|
wantErr: true,
|
|
errMsg: "azure provider requires issuer URL to contain login.microsoftonline.com",
|
|
},
|
|
{
|
|
name: "Azure config with sts.windows.net",
|
|
provider: NewAzureProvider(),
|
|
config: map[string]interface{}{
|
|
"issuer_url": "https://sts.windows.net/12345678-1234-1234-1234-123456789012",
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "Azure config without tenant ID",
|
|
provider: NewAzureProvider(),
|
|
config: map[string]interface{}{
|
|
"issuer_url": "https://login.microsoftonline.com/common",
|
|
},
|
|
wantErr: true,
|
|
errMsg: "azure issuer URL should include tenant ID",
|
|
},
|
|
{
|
|
name: "valid generic provider config",
|
|
provider: NewGenericProvider(),
|
|
config: map[string]interface{}{
|
|
"issuer_url": "https://auth.example.com",
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "empty config for generic provider",
|
|
provider: NewGenericProvider(),
|
|
config: map[string]interface{}{},
|
|
wantErr: false,
|
|
},
|
|
}
|
|
|
|
validator := NewConfigValidator()
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := validator.ValidateProviderSpecificConfig(tt.provider, tt.config)
|
|
|
|
if tt.wantErr {
|
|
if err == nil {
|
|
t.Error("expected error, got nil")
|
|
} else if tt.errMsg != "" && !strings.Contains(err.Error(), tt.errMsg) {
|
|
t.Errorf("expected error containing %q, got %q", tt.errMsg, err.Error())
|
|
}
|
|
} else {
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestValidateProviderSpecificConfig_UnknownProvider tests handling of unknown provider types
|
|
func TestValidateProviderSpecificConfig_UnknownProvider(t *testing.T) {
|
|
validator := NewConfigValidator()
|
|
|
|
// Create a mock provider with invalid type
|
|
mockProvider := &mockUnknownProvider{}
|
|
|
|
err := validator.ValidateProviderSpecificConfig(mockProvider, map[string]interface{}{})
|
|
if err == nil {
|
|
t.Error("expected error for unknown provider type")
|
|
}
|
|
if !strings.Contains(err.Error(), "unknown provider type") {
|
|
t.Errorf("expected 'unknown provider type' error, got: %v", err)
|
|
}
|
|
}
|
|
|
|
// mockUnknownProvider is a test provider with an invalid type
|
|
type mockUnknownProvider struct{}
|
|
|
|
func (m *mockUnknownProvider) GetType() ProviderType {
|
|
return ProviderType(999) // Invalid type
|
|
}
|
|
|
|
func (m *mockUnknownProvider) GetCapabilities() ProviderCapabilities {
|
|
return ProviderCapabilities{}
|
|
}
|
|
|
|
func (m *mockUnknownProvider) ValidateTokens(session Session, verifier TokenVerifier, tokenCache TokenCache, refreshGracePeriod time.Duration) (*ValidationResult, error) {
|
|
return &ValidationResult{}, nil
|
|
}
|
|
|
|
func (m *mockUnknownProvider) BuildAuthParams(baseParams url.Values, scopes []string) (*AuthParams, error) {
|
|
return &AuthParams{}, nil
|
|
}
|
|
|
|
func (m *mockUnknownProvider) HandleTokenRefresh(tokenData *TokenResult) error {
|
|
return nil
|
|
}
|
|
|
|
func (m *mockUnknownProvider) ValidateConfig() error {
|
|
return nil
|
|
}
|
|
|
|
// TestValidateGoogleConfig_EdgeCases tests edge cases for Google config validation
|
|
func TestValidateGoogleConfig_EdgeCases(t *testing.T) {
|
|
validator := NewConfigValidator()
|
|
googleProvider := NewGoogleProvider()
|
|
|
|
tests := []struct {
|
|
name string
|
|
config map[string]interface{}
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "config without issuer_url",
|
|
config: map[string]interface{}{},
|
|
wantErr: false, // Should pass as issuer_url is not present
|
|
},
|
|
{
|
|
name: "config with non-string issuer_url",
|
|
config: map[string]interface{}{
|
|
"issuer_url": 123,
|
|
},
|
|
wantErr: false, // Should pass as type assertion fails
|
|
},
|
|
{
|
|
name: "config with accounts.google.com in path",
|
|
config: map[string]interface{}{
|
|
"issuer_url": "https://example.com/accounts.google.com",
|
|
},
|
|
wantErr: false, // Should pass as it contains the required string
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := validator.ValidateProviderSpecificConfig(googleProvider, tt.config)
|
|
|
|
if tt.wantErr && err == nil {
|
|
t.Error("expected error, got nil")
|
|
} else if !tt.wantErr && err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestValidateAzureConfig_EdgeCases tests edge cases for Azure config validation
|
|
func TestValidateAzureConfig_EdgeCases(t *testing.T) {
|
|
validator := NewConfigValidator()
|
|
azureProvider := NewAzureProvider()
|
|
|
|
tests := []struct {
|
|
name string
|
|
config map[string]interface{}
|
|
wantErr bool
|
|
errMsg string
|
|
}{
|
|
{
|
|
name: "valid tenant ID format",
|
|
config: map[string]interface{}{
|
|
"issuer_url": "https://login.microsoftonline.com/a1b2c3d4-e5f6-7890-abcd-ef1234567890/v2.0",
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "tenant ID in different position",
|
|
config: map[string]interface{}{
|
|
"issuer_url": "https://login.microsoftonline.com/v2.0/a1b2c3d4-e5f6-7890-abcd-ef1234567890/oauth",
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "malformed URL for parsing",
|
|
config: map[string]interface{}{
|
|
"issuer_url": "https://login.microsoftonline.com/[invalid",
|
|
},
|
|
wantErr: true,
|
|
errMsg: "azure issuer URL should include tenant ID",
|
|
},
|
|
{
|
|
name: "config without issuer_url",
|
|
config: map[string]interface{}{},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "config with non-string issuer_url",
|
|
config: map[string]interface{}{
|
|
"issuer_url": []string{"https://login.microsoftonline.com"},
|
|
},
|
|
wantErr: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := validator.ValidateProviderSpecificConfig(azureProvider, tt.config)
|
|
|
|
if tt.wantErr {
|
|
if err == nil {
|
|
t.Error("expected error, got nil")
|
|
} else if tt.errMsg != "" && !strings.Contains(err.Error(), tt.errMsg) {
|
|
t.Errorf("expected error containing %q, got %q", tt.errMsg, err.Error())
|
|
}
|
|
} else {
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|