mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-10-17 04:37:48 +00:00
December 2025 Improvements - Azure AD, Internal Networks, Startup Race Condition (#100)
* Allow internal IPs for OIDC configuration via extra flag. Addresses issue #97 * Allow for internal IPs in OIDC configuration. Addresses issue #97. * feat: Add allowPrivateIPAddresses config option for internal networks Adds a new configuration option `allowPrivateIPAddresses` that allows OIDC provider URLs to use private IP addresses (10.x.x.x, 172.16-31.x.x, 192.168.x.x). This is useful for internal deployments where Keycloak or other OIDC providers run on private networks without DNS resolution. Security considerations: - Loopback addresses (127.0.0.1, localhost, ::1) remain blocked - Link-local addresses (169.254.x.x) remain blocked - Default is false (secure by default) Fixes #97 * feat: Support non-email user identifiers for Azure AD Add userIdentifierClaim configuration option to support Azure AD users without email addresses. This allows using alternative JWT claims like "sub", "oid", "upn", or "preferred_username" for user identification. - Default behavior uses "email" claim (backward compatible) - Falls back to "sub" claim if configured claim is missing - allowedUsers matches against the configured claim value - allowedUserDomains only applies when using email-based identification Fixes #95 * Race condition on traefik pod startup When the plugin initializes and calls GetMetadataWithRecovery(): 1. Checks cache first (if metadata is cached, returns immediately) 2. Creates a retry executor with startup-optimized settings (10 attempts, 1s delays) 3. Attempts to fetch metadata from the OIDC provider 4. If the fetch fails with a retryable error (connection refused, EOF, TLS/certificate errors, Traefik default cert), it waits and retries 5. After 10 attempts or on a non-retryable error, returns the error This allows the plugin to handle the race condition where: - Traefik initializes the plugin before routes are established - Traefik serves its default certificate before loading real ones - The OIDC provider pod isn't fully ready yet Fixes issue #90 * Race condition on traefik pod startup When the plugin initializes and calls GetMetadataWithRecovery(): 1. Checks cache first (if metadata is cached, returns immediately) 2. Creates a retry executor with startup-optimized settings (10 attempts, 1s delays) 3. Attempts to fetch metadata from the OIDC provider 4. If the fetch fails with a retryable error (connection refused, EOF, TLS/certificate errors, Traefik default cert), it waits and retries 5. After 10 attempts or on a non-retryable error, returns the error This allows the plugin to handle the race condition where: - Traefik initializes the plugin before routes are established - Traefik serves its default certificate before loading real ones - The OIDC provider pod isn't fully ready yet Fixes issue #90 * Headers too big and 431 responses Added new option `minimalHeaders` to reduce the size of forwarded headers from the auth middleware to backend services. - When minimalHeaders: false (default): All headers are forwarded as before - X-Forwarded-User (always set) - X-Auth-Request-Redirect - X-Auth-Request-User - X-Auth-Request-Token (the large ID token) - X-User-Groups, X-User-Roles (if configured) - When minimalHeaders: true: Reduces header overhead - X-Forwarded-User (always set) - X-User-Groups, X-User-Roles (still forwarded if configured) - Custom templated headers (still processed) - Skipped: X-Auth-Request-Token, X-Auth-Request-User, X-Auth-Request-Redirect Fixes issues #64 and #86
This commit is contained in:
@@ -545,3 +545,168 @@ func createTestSessionManager(t *testing.T) *SessionManager {
|
||||
}
|
||||
return sm
|
||||
}
|
||||
|
||||
// TestMinimalHeaders tests the minimalHeaders configuration option
|
||||
// This addresses GitHub issue #64 - Request Header Fields Too Large
|
||||
func TestMinimalHeaders(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
minimalHeaders bool
|
||||
expectForwardedUser bool
|
||||
expectAuthRequestUser bool
|
||||
expectAuthRequestRedirect bool
|
||||
}{
|
||||
{
|
||||
name: "minimalHeaders=false (default) forwards all headers",
|
||||
minimalHeaders: false,
|
||||
expectForwardedUser: true,
|
||||
expectAuthRequestUser: true,
|
||||
expectAuthRequestRedirect: true,
|
||||
},
|
||||
{
|
||||
name: "minimalHeaders=true only forwards X-Forwarded-User",
|
||||
minimalHeaders: true,
|
||||
expectForwardedUser: true,
|
||||
expectAuthRequestUser: false,
|
||||
expectAuthRequestRedirect: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Track which headers were set
|
||||
var capturedHeaders http.Header
|
||||
|
||||
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
capturedHeaders = r.Header.Clone()
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
sessionManager := createTestSessionManager(t)
|
||||
oidc := &TraefikOidc{
|
||||
next: next,
|
||||
logger: NewLogger("debug"),
|
||||
initComplete: make(chan struct{}),
|
||||
sessionManager: sessionManager,
|
||||
firstRequestReceived: true,
|
||||
metadataRefreshStarted: true,
|
||||
issuerURL: "https://provider.example.com",
|
||||
minimalHeaders: tt.minimalHeaders,
|
||||
extractClaimsFunc: func(token string) (map[string]interface{}, error) {
|
||||
return map[string]interface{}{
|
||||
"email": "[email protected]",
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
close(oidc.initComplete)
|
||||
|
||||
// Create request and get session properly through session manager
|
||||
req := httptest.NewRequest("GET", "/protected", nil)
|
||||
rw := httptest.NewRecorder()
|
||||
|
||||
session, err := sessionManager.GetSession(req)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get session: %v", err)
|
||||
}
|
||||
|
||||
// Set up session data
|
||||
session.SetEmail("[email protected]")
|
||||
session.SetAuthenticated(true)
|
||||
|
||||
// Call processAuthorizedRequest directly
|
||||
oidc.processAuthorizedRequest(rw, req, session, "https://example.com/callback")
|
||||
|
||||
// Verify X-Forwarded-User is always set
|
||||
if tt.expectForwardedUser {
|
||||
if capturedHeaders.Get("X-Forwarded-User") != "[email protected]" {
|
||||
t.Errorf("expected X-Forwarded-User to be set, got %q", capturedHeaders.Get("X-Forwarded-User"))
|
||||
}
|
||||
}
|
||||
|
||||
// Verify X-Auth-Request-User
|
||||
hasAuthRequestUser := capturedHeaders.Get("X-Auth-Request-User") != ""
|
||||
if tt.expectAuthRequestUser && !hasAuthRequestUser {
|
||||
t.Error("expected X-Auth-Request-User to be set")
|
||||
}
|
||||
if !tt.expectAuthRequestUser && hasAuthRequestUser {
|
||||
t.Errorf("expected X-Auth-Request-User to NOT be set when minimalHeaders=true, got %q", capturedHeaders.Get("X-Auth-Request-User"))
|
||||
}
|
||||
|
||||
// Verify X-Auth-Request-Redirect
|
||||
hasAuthRequestRedirect := capturedHeaders.Get("X-Auth-Request-Redirect") != ""
|
||||
if tt.expectAuthRequestRedirect && !hasAuthRequestRedirect {
|
||||
t.Error("expected X-Auth-Request-Redirect to be set")
|
||||
}
|
||||
if !tt.expectAuthRequestRedirect && hasAuthRequestRedirect {
|
||||
t.Errorf("expected X-Auth-Request-Redirect to NOT be set when minimalHeaders=true, got %q", capturedHeaders.Get("X-Auth-Request-Redirect"))
|
||||
}
|
||||
|
||||
// Note: X-Auth-Request-Token is only set if session.GetIDToken() returns non-empty.
|
||||
// Token storage has validation that may reject test tokens, so we verify the flag
|
||||
// logic through the other headers. The important behavior is that when
|
||||
// minimalHeaders=true, the token header would NOT be set even if a token existed.
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestMinimalHeaders_TokenHeaderNotSet verifies that the X-Auth-Request-Token header
|
||||
// is NOT set when minimalHeaders is enabled, even if a token exists.
|
||||
func TestMinimalHeaders_TokenHeaderNotSet(t *testing.T) {
|
||||
var capturedHeaders http.Header
|
||||
|
||||
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
capturedHeaders = r.Header.Clone()
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
sessionManager := createTestSessionManager(t)
|
||||
oidc := &TraefikOidc{
|
||||
next: next,
|
||||
logger: NewLogger("debug"),
|
||||
initComplete: make(chan struct{}),
|
||||
sessionManager: sessionManager,
|
||||
firstRequestReceived: true,
|
||||
metadataRefreshStarted: true,
|
||||
issuerURL: "https://provider.example.com",
|
||||
minimalHeaders: true, // Enable minimal headers
|
||||
extractClaimsFunc: func(token string) (map[string]interface{}, error) {
|
||||
return map[string]interface{}{
|
||||
"email": "[email protected]",
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
close(oidc.initComplete)
|
||||
|
||||
req := httptest.NewRequest("GET", "/protected", nil)
|
||||
rw := httptest.NewRecorder()
|
||||
|
||||
session, err := sessionManager.GetSession(req)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get session: %v", err)
|
||||
}
|
||||
|
||||
session.SetEmail("[email protected]")
|
||||
session.SetAuthenticated(true)
|
||||
|
||||
oidc.processAuthorizedRequest(rw, req, session, "https://example.com/callback")
|
||||
|
||||
// Verify X-Forwarded-User is set (always should be)
|
||||
if capturedHeaders.Get("X-Forwarded-User") != "[email protected]" {
|
||||
t.Errorf("expected X-Forwarded-User to be set, got %q", capturedHeaders.Get("X-Forwarded-User"))
|
||||
}
|
||||
|
||||
// The key verification: X-Auth-Request-Token should NOT be set with minimalHeaders=true
|
||||
if capturedHeaders.Get("X-Auth-Request-Token") != "" {
|
||||
t.Error("expected X-Auth-Request-Token to NOT be set with minimalHeaders=true")
|
||||
}
|
||||
|
||||
// X-Auth-Request-User should also NOT be set with minimalHeaders=true
|
||||
if capturedHeaders.Get("X-Auth-Request-User") != "" {
|
||||
t.Error("expected X-Auth-Request-User to NOT be set with minimalHeaders=true")
|
||||
}
|
||||
|
||||
// X-Auth-Request-Redirect should also NOT be set with minimalHeaders=true
|
||||
if capturedHeaders.Get("X-Auth-Request-Redirect") != "" {
|
||||
t.Error("expected X-Auth-Request-Redirect to NOT be set with minimalHeaders=true")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user