Files
traefikoidc/internal/providers/validation.go
T
lukaszraczylo 1b49e133da Complete rebuild of the plugin
* Fix bug affecting Azure OIDC authentication ( and most likely others )

* Fixes issue #51

* Ensure that appended roles are unique. Update the documentation.

* Improvements targetting possible memory usage spikes.

* Additional fixes and cleanup

* Refactoring code to fix the issues identified by the users.

* Modernize run

* Fieldalignment

* Multiple changes to improve performance and reduce complexity.
- Optimise the errors and recovery.
- Deduplicate code in metadata cache.
- Remove unused performance monitoring code.
- Simplify session management and settings handling.

* Fix claims issue.

* Add ability to overwrite the default scopes in the settings file

* Well.. that escalated quickly.

Completely forgot that Traefik uses outdated Yaegi and requires compatibility with 1.20 ( pre-generic Go code ).

* Bugfix #51: Ensures that user provided scopes overrides work.

* fixup! Bugfix #51: Ensures that user provided scopes overrides work.

* fixup! fixup! Bugfix #51: Ensures that user provided scopes overrides work.

* Abstract the provider logic into a separate package.

* Additional micro fixes and cleanups.

* Simplify all the things.

* fixup! Simplify all the things.

* fixup! fixup! Simplify all the things.

* fixup! fixup! fixup! Simplify all the things.

* fixup! fixup! fixup! fixup! Simplify all the things.

* ...

* Cleanup tests.

* fixup! Cleanup tests.

* fixup! fixup! fixup! Cleanup tests.

* fixup! fixup! fixup! fixup! Cleanup tests.

* fixup! fixup! fixup! fixup! fixup! Cleanup tests.

* Issue #53: Fix CSRF token handling in reverse proxy

1.  HTTPS Detection Fixed (session.go:723)
- Now uses X-Forwarded-Proto header instead of r.URL.Scheme
- Properly detects HTTPS in reverse proxy environments
2.  SameSite Cookie Attribute Fixed
- Removed automatic SameSiteStrictMode for HTTPS (would break OAuth)
- Keeps SameSiteLaxMode to allow OAuth callbacks from external domains
- Only uses Strict for AJAX requests which don't involve OAuth redirects
3.  Cookie Domain Handling Fixed
- Now respects X-Forwarded-Host header for cookie domain
- Ensures cookies are set for the public domain, not internal proxy domain
4.  EnhanceSessionSecurity Properly Integrated
- Function is now actually called during session save
- Applies security enhancements without breaking OAuth flow

Why Issue #53 Failed Before:

1. Cookies were not marked Secure in HTTPS environments (browser wouldn't send them back)
2. If they had been Secure with SameSite=Strict, Azure callbacks would still fail
3. Cookie domain might have been wrong (internal vs public domain)

Why It Works Now:

1. Cookies are properly marked Secure for HTTPS
2. Uses SameSite=Lax to allow OAuth provider callbacks
3. Cookie domain uses public domain from X-Forwarded-Host
4. CSRF token persists through the entire OAuth flow

* Next set of enhancements together with memory usage improvements.

* Memory leak fixes and optimisations.

* CSRF and Cookie Domain fixes

* fixup! CSRF and Cookie Domain fixes

* Metadata cache leak fix + profiling

* fixup! Metadata cache leak fix + profiling

* Memory leaks hunting, part 1337.

* Further pursue of perfection.

* fixup! Further pursue of perfection.

* fixup! fixup! Further pursue of perfection.

* fixup! fixup! fixup! Further pursue of perfection.

* fixup! fixup! fixup! fixup! Further pursue of perfection.

* fixup! fixup! fixup! fixup! fixup! Further pursue of perfection.

* fixup! fixup! fixup! fixup! fixup! fixup! Further pursue of perfection.

* fixup! fixup! fixup! fixup! fixup! fixup! fixup! Further pursue of perfection.

* fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! Further pursue of perfection.

* fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! Further pursue of perfection.

* Clear race conditions

* fixup! Clear race conditions

* Weekend fun with memory leaks

* Splitting code into multiple files with reasonable testing coverage.

```
ok      github.com/lukaszraczylo/traefikoidc    117.017s        coverage: 72.6% of statements
ok      github.com/lukaszraczylo/traefikoidc/auth       0.505s  coverage: 87.1% of statements
ok      github.com/lukaszraczylo/traefikoidc/circuit_breaker    0.283s  coverage: 99.0% of statements
        github.com/lukaszraczylo/traefikoidc/config             coverage: 0.0% of statements
ok      github.com/lukaszraczylo/traefikoidc/handlers   0.349s  coverage: 98.2% of statements
ok      github.com/lukaszraczylo/traefikoidc/internal/providers (cached)        coverage: 94.3% of statements
ok      github.com/lukaszraczylo/traefikoidc/middleware 0.808s  coverage: 78.0% of statements
ok      github.com/lukaszraczylo/traefikoidc/recovery   0.653s  coverage: 100.0% of statements
ok      github.com/lukaszraczylo/traefikoidc/session/chunking   (cached)        coverage: 87.8% of statements
ok      github.com/lukaszraczylo/traefikoidc/session/core       (cached)        coverage: 85.6% of statements
ok      github.com/lukaszraczylo/traefikoidc/session/crypto     (cached)        coverage: 81.8% of statements
ok      github.com/lukaszraczylo/traefikoidc/session/storage    (cached)        coverage: 93.5% of statements
ok      github.com/lukaszraczylo/traefikoidc/session/validators (cached)        coverage: 98.8% of statements
````

* fixup! Splitting code into multiple files with reasonable testing coverage.

* fixup! fixup! Splitting code into multiple files with reasonable testing coverage.

* Weekend fun with further optimisations.

* fixup! Weekend fun with further optimisations.

* fixup! fixup! Weekend fun with further optimisations.

* fixup! fixup! fixup! Weekend fun with further optimisations.

* fixup! fixup! fixup! fixup! Weekend fun with further optimisations.

* fixup! fixup! fixup! fixup! fixup! Weekend fun with further optimisations.

* Pre-release cleanup.

* Enhance test coverage.

* fixup! Enhance test coverage.

* fixup! fixup! Enhance test coverage.

* fixup! fixup! fixup! Enhance test coverage.
2025-09-18 11:01:30 +01:00

152 lines
4.1 KiB
Go

package providers
import (
"fmt"
"net/url"
"strings"
)
// ConfigValidator provides common configuration validation utilities for providers.
type ConfigValidator struct{}
// NewConfigValidator creates a new configuration validator.
func NewConfigValidator() *ConfigValidator {
return &ConfigValidator{}
}
// ValidateIssuerURL validates that an issuer URL is properly formatted and accessible.
func (v *ConfigValidator) ValidateIssuerURL(issuerURL string) error {
if issuerURL == "" {
return fmt.Errorf("issuer URL cannot be empty")
}
parsedURL, err := url.Parse(issuerURL)
if err != nil {
return fmt.Errorf("invalid issuer URL format: %w", err)
}
if parsedURL.Scheme == "" {
return fmt.Errorf("issuer URL must include scheme (http/https)")
}
if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" {
return fmt.Errorf("issuer URL scheme must be http or https")
}
if parsedURL.Host == "" {
return fmt.Errorf("issuer URL must include host")
}
return nil
}
// ValidateClientID validates that a client ID is properly formatted.
func (v *ConfigValidator) ValidateClientID(clientID string) error {
if clientID == "" {
return fmt.Errorf("client ID cannot be empty")
}
if len(clientID) < 3 {
return fmt.Errorf("client ID appears to be too short")
}
return nil
}
// ValidateScopes validates that the provided scopes are reasonable.
func (v *ConfigValidator) ValidateScopes(scopes []string) error {
if len(scopes) == 0 {
return fmt.Errorf("at least one scope must be provided")
}
hasOpenIDScope := false
for _, scope := range scopes {
if strings.TrimSpace(scope) == "openid" {
hasOpenIDScope = true
break
}
}
if !hasOpenIDScope {
return fmt.Errorf("'openid' scope is required for OIDC authentication")
}
return nil
}
// ValidateRedirectURL validates that a redirect URL is properly formatted.
func (v *ConfigValidator) ValidateRedirectURL(redirectURL string) error {
if redirectURL == "" {
return fmt.Errorf("redirect URL cannot be empty")
}
parsedURL, err := url.Parse(redirectURL)
if err != nil {
return fmt.Errorf("invalid redirect URL format: %w", err)
}
if parsedURL.Scheme == "" {
return fmt.Errorf("redirect URL must include scheme (http/https)")
}
return nil
}
// ValidateProviderSpecificConfig performs provider-specific validation.
func (v *ConfigValidator) ValidateProviderSpecificConfig(provider OIDCProvider, config map[string]interface{}) error {
switch provider.GetType() {
case ProviderTypeGoogle:
return v.validateGoogleConfig(config)
case ProviderTypeAzure:
return v.validateAzureConfig(config)
case ProviderTypeGeneric:
return v.validateGenericConfig(config)
default:
return fmt.Errorf("unknown provider type: %d", provider.GetType())
}
}
// validateGoogleConfig validates Google-specific configuration.
func (v *ConfigValidator) validateGoogleConfig(config map[string]interface{}) error {
if issuerURL, ok := config["issuer_url"].(string); ok {
if !strings.Contains(issuerURL, "accounts.google.com") {
return fmt.Errorf("google provider requires issuer URL to contain accounts.google.com")
}
}
return nil
}
// validateAzureConfig validates Azure-specific configuration.
func (v *ConfigValidator) validateAzureConfig(config map[string]interface{}) error {
if issuerURL, ok := config["issuer_url"].(string); ok {
if !strings.Contains(issuerURL, "login.microsoftonline.com") && !strings.Contains(issuerURL, "sts.windows.net") {
return fmt.Errorf("azure provider requires issuer URL to contain login.microsoftonline.com or sts.windows.net")
}
}
if issuerURL, ok := config["issuer_url"].(string); ok {
parsedURL, err := url.Parse(issuerURL)
if err == nil {
pathParts := strings.Split(parsedURL.Path, "/")
hasTenantID := false
for _, part := range pathParts {
if len(part) == 36 && strings.Count(part, "-") == 4 {
hasTenantID = true
break
}
}
if !hasTenantID {
return fmt.Errorf("azure issuer URL should include tenant ID")
}
}
}
return nil
}
// validateGenericConfig validates generic OIDC provider configuration.
func (v *ConfigValidator) validateGenericConfig(config map[string]interface{}) error {
return nil
}