mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-06-05 22:44:17 +00:00
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.
This commit is contained in:
@@ -0,0 +1,344 @@
|
||||
package traefikoidc
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// generateRandomString generates a random string of the specified length
|
||||
// This is used in tests to create unique identifiers
|
||||
func generateRandomString(length int) string {
|
||||
bytes := make([]byte, length/2)
|
||||
if _, err := rand.Read(bytes); err != nil {
|
||||
// In tests, fallback to a predictable string if random fails
|
||||
return "random-string-fallback"
|
||||
}
|
||||
return hex.EncodeToString(bytes)
|
||||
}
|
||||
|
||||
// Test createCaseInsensitiveStringMap function
|
||||
func TestCreateCaseInsensitiveStringMap(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
items []string
|
||||
expected map[string]struct{}
|
||||
}{
|
||||
{
|
||||
name: "Mixed case items",
|
||||
items: []string{"Admin", "USER", "manager"},
|
||||
expected: map[string]struct{}{
|
||||
"admin": {},
|
||||
"user": {},
|
||||
"manager": {},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Empty slice",
|
||||
items: []string{},
|
||||
expected: map[string]struct{}{},
|
||||
},
|
||||
{
|
||||
name: "Duplicates with different cases",
|
||||
items: []string{"Admin", "admin", "ADMIN"},
|
||||
expected: map[string]struct{}{
|
||||
"admin": {},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Nil slice",
|
||||
items: nil,
|
||||
expected: map[string]struct{}{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := createCaseInsensitiveStringMap(tt.items)
|
||||
|
||||
if len(result) != len(tt.expected) {
|
||||
t.Errorf("createCaseInsensitiveStringMap() length = %v, want %v", len(result), len(tt.expected))
|
||||
return
|
||||
}
|
||||
|
||||
for key := range tt.expected {
|
||||
if _, exists := result[key]; !exists {
|
||||
t.Errorf("createCaseInsensitiveStringMap() missing key %v", key)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Test keysFromMap function
|
||||
func TestKeysFromMap(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input map[string]struct{}
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
name: "Multiple keys",
|
||||
input: map[string]struct{}{
|
||||
"key1": {},
|
||||
"key2": {},
|
||||
"key3": {},
|
||||
},
|
||||
expected: []string{"key1", "key2", "key3"},
|
||||
},
|
||||
{
|
||||
name: "Empty map",
|
||||
input: map[string]struct{}{},
|
||||
expected: []string{},
|
||||
},
|
||||
{
|
||||
name: "Single key",
|
||||
input: map[string]struct{}{
|
||||
"onlykey": {},
|
||||
},
|
||||
expected: []string{"onlykey"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := keysFromMap(tt.input)
|
||||
|
||||
if len(result) != len(tt.expected) {
|
||||
t.Errorf("keysFromMap() length = %v, want %v", len(result), len(tt.expected))
|
||||
return
|
||||
}
|
||||
|
||||
// Convert to map for comparison since order doesn't matter
|
||||
resultMap := make(map[string]bool)
|
||||
for _, key := range result {
|
||||
resultMap[key] = true
|
||||
}
|
||||
|
||||
for _, key := range tt.expected {
|
||||
if !resultMap[key] {
|
||||
t.Errorf("keysFromMap() missing key %v", key)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Test TraefikOidc provider detection methods
|
||||
func TestTraefikOidcProviderDetection(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
providerURL string
|
||||
expectGoogle bool
|
||||
expectAzure bool
|
||||
}{
|
||||
{
|
||||
name: "Google provider",
|
||||
providerURL: "https://accounts.google.com",
|
||||
expectGoogle: true,
|
||||
expectAzure: false,
|
||||
},
|
||||
{
|
||||
name: "Azure provider",
|
||||
providerURL: "https://login.microsoftonline.com/tenant-id/v2.0",
|
||||
expectGoogle: false,
|
||||
expectAzure: true,
|
||||
},
|
||||
{
|
||||
name: "Generic provider",
|
||||
providerURL: "https://auth.example.com",
|
||||
expectGoogle: false,
|
||||
expectAzure: false,
|
||||
},
|
||||
{
|
||||
name: "Empty provider URL",
|
||||
providerURL: "",
|
||||
expectGoogle: false,
|
||||
expectAzure: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
traefik := &TraefikOidc{
|
||||
issuerURL: tt.providerURL,
|
||||
}
|
||||
|
||||
isGoogle := traefik.isGoogleProvider()
|
||||
isAzure := traefik.isAzureProvider()
|
||||
|
||||
if isGoogle != tt.expectGoogle {
|
||||
t.Errorf("isGoogleProvider() = %v, want %v", isGoogle, tt.expectGoogle)
|
||||
}
|
||||
|
||||
if isAzure != tt.expectAzure {
|
||||
t.Errorf("isAzureProvider() = %v, want %v", isAzure, tt.expectAzure)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Test buildFullURL function
|
||||
func TestBuildFullURL(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
scheme string
|
||||
host string
|
||||
path string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "Standard HTTPS URL",
|
||||
scheme: "https",
|
||||
host: "example.com",
|
||||
path: "/auth/callback",
|
||||
expected: "https://example.com/auth/callback",
|
||||
},
|
||||
{
|
||||
name: "HTTP URL",
|
||||
scheme: "http",
|
||||
host: "localhost:8080",
|
||||
path: "/test",
|
||||
expected: "http://localhost:8080/test",
|
||||
},
|
||||
{
|
||||
name: "Root path",
|
||||
scheme: "https",
|
||||
host: "api.example.com",
|
||||
path: "/",
|
||||
expected: "https://api.example.com/",
|
||||
},
|
||||
{
|
||||
name: "Empty path",
|
||||
scheme: "https",
|
||||
host: "example.com",
|
||||
path: "",
|
||||
expected: "https://example.com/",
|
||||
},
|
||||
{
|
||||
name: "Path without leading slash",
|
||||
scheme: "https",
|
||||
host: "example.com",
|
||||
path: "noSlash",
|
||||
expected: "https://example.com/noSlash",
|
||||
},
|
||||
{
|
||||
name: "Complex path with query params",
|
||||
scheme: "https",
|
||||
host: "api.example.com",
|
||||
path: "/v2/search?q=test&limit=10",
|
||||
expected: "https://api.example.com/v2/search?q=test&limit=10",
|
||||
},
|
||||
{
|
||||
name: "IPv4 address",
|
||||
scheme: "http",
|
||||
host: "192.168.1.100",
|
||||
path: "/api",
|
||||
expected: "http://192.168.1.100/api",
|
||||
},
|
||||
{
|
||||
name: "IPv6 address with brackets",
|
||||
scheme: "http",
|
||||
host: "[::1]:8080",
|
||||
path: "/test",
|
||||
expected: "http://[::1]:8080/test",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := buildFullURL(tt.scheme, tt.host, tt.path)
|
||||
if result != tt.expected {
|
||||
t.Errorf("buildFullURL() = %v, want %v", result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Test validateURL function
|
||||
func TestValidateURL(t *testing.T) {
|
||||
traefik := &TraefikOidc{
|
||||
logger: NewLogger("debug"),
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
url string
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "Valid HTTPS URL",
|
||||
url: "https://example.com/path",
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "Valid HTTP URL",
|
||||
url: "http://example.com",
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "Empty URL",
|
||||
url: "",
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "Invalid URL format",
|
||||
url: "not-a-url",
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "URL with space",
|
||||
url: "https://example .com",
|
||||
expectError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := traefik.validateURL(tt.url)
|
||||
if tt.expectError && err == nil {
|
||||
t.Errorf("validateURL(%q) expected error but got none", tt.url)
|
||||
} else if !tt.expectError && err != nil {
|
||||
t.Errorf("validateURL(%q) unexpected error: %v", tt.url, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Test TraefikOidc helper logging methods
|
||||
func TestTraefikOidcHelperMethods(t *testing.T) {
|
||||
traefik := &TraefikOidc{
|
||||
logger: NewLogger("debug"),
|
||||
}
|
||||
|
||||
// Test safe logging methods (they just delegate to logger, but increase coverage)
|
||||
traefik.safeLogDebug("test debug message")
|
||||
traefik.safeLogDebugf("test debug with %s", "param")
|
||||
traefik.safeLogError("test error message")
|
||||
traefik.safeLogErrorf("test error with %s", "param")
|
||||
traefik.safeLogInfo("test info message")
|
||||
|
||||
// These methods should not panic with nil logger either
|
||||
traefikNilLogger := &TraefikOidc{}
|
||||
traefikNilLogger.safeLogDebug("test with nil logger")
|
||||
traefikNilLogger.safeLogInfo("test info with nil logger")
|
||||
}
|
||||
|
||||
// Test createDefaultHTTPClient function
|
||||
func TestCreateDefaultHTTPClient(t *testing.T) {
|
||||
client := createDefaultHTTPClient()
|
||||
|
||||
if client == nil {
|
||||
t.Fatal("createDefaultHTTPClient() returned nil")
|
||||
}
|
||||
|
||||
if client.Timeout == 0 {
|
||||
t.Error("Expected non-zero timeout")
|
||||
}
|
||||
|
||||
// Verify it has some reasonable timeout
|
||||
expectedTimeout := 30000000000 // 30 seconds in nanoseconds
|
||||
if client.Timeout.Nanoseconds() != int64(expectedTimeout) {
|
||||
t.Logf("Client timeout: %v (expected 30s, but this may vary)", client.Timeout)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user