mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-06-05 22:44:17 +00:00
e64fc7f730
* Add redis support for distributed caching * Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi. * fixup! Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi. * fixup! fixup! Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi. * fixup! fixup! fixup! Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi. * fixup! fixup! fixup! fixup! Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi. * fixup! fixup! fixup! fixup! fixup! Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi. * ... and another all nighter. * fixup! ... and another all nighter. * fixup! fixup! ... and another all nighter. * fixup! fixup! fixup! ... and another all nighter. * Resolve issue #85 by adding ability to set custom claims in JWT tokens * Remove redundant validation in auth middleware ( issue #89 ) * Add ability to set cookie prefix for session cookies ( #87 ) * fixup! Add ability to set cookie prefix for session cookies ( #87 ) * Add ability to set cookie max age - issue #91 * Potential fix for code scanning alert no. 10: Size computation for allocation may overflow Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * fixup! Merge main into 0.8.0-redis: resolve conflicts --------- Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
381 lines
11 KiB
Go
381 lines
11 KiB
Go
package traefikoidc
|
|
|
|
import (
|
|
"net/http/httptest"
|
|
"sync"
|
|
"testing"
|
|
)
|
|
|
|
// testWriter is an io.Writer that writes to test log
|
|
// lint:ignore U1000 Kept for potential future use
|
|
/*
|
|
type testWriter struct {
|
|
t *testing.T
|
|
}
|
|
|
|
func (w *testWriter) Write(p []byte) (n int, err error) {
|
|
w.t.Log(string(p))
|
|
return len(p), nil
|
|
}
|
|
*/
|
|
|
|
// Test helper adapters for the new test files
|
|
|
|
// resetGlobalState resets all global singletons to prevent test interference
|
|
// nolint:unused // Kept for potential future use in integration tests
|
|
/*
|
|
func resetGlobalState() {
|
|
// Reset global task registry first to stop all background tasks
|
|
ResetGlobalTaskRegistry()
|
|
|
|
// Give tasks a moment to stop
|
|
time.Sleep(10 * time.Millisecond)
|
|
|
|
// Reset and cleanup replay cache - this should work now that tasks are stopped
|
|
cleanupReplayCache()
|
|
|
|
// Reset memory pools
|
|
memoryPoolMutex.Lock()
|
|
globalMemoryPools = nil
|
|
memoryPoolOnce = sync.Once{}
|
|
memoryPoolMutex.Unlock()
|
|
|
|
// The universal cache manager is a singleton that persists across tests
|
|
// Don't reset it as it causes issues
|
|
}
|
|
*/
|
|
|
|
// testCleanup provides comprehensive cleanup for tests to prevent goroutine leaks
|
|
type testCleanup struct {
|
|
t *testing.T
|
|
caches []CacheInterface
|
|
servers []*httptest.Server
|
|
oidcs []*TraefikOidc
|
|
mu sync.Mutex
|
|
}
|
|
|
|
// newTestCleanup creates a new test cleanup helper that automatically registers cleanup
|
|
func newTestCleanup(t *testing.T) *testCleanup {
|
|
tc := &testCleanup{
|
|
t: t,
|
|
caches: make([]CacheInterface, 0),
|
|
servers: make([]*httptest.Server, 0),
|
|
oidcs: make([]*TraefikOidc, 0),
|
|
}
|
|
|
|
// Register cleanup to run even if test panics
|
|
t.Cleanup(func() {
|
|
tc.cleanupAll()
|
|
})
|
|
|
|
return tc
|
|
}
|
|
|
|
// addCache registers a cache for cleanup
|
|
func (tc *testCleanup) addCache(c CacheInterface) CacheInterface {
|
|
tc.mu.Lock()
|
|
defer tc.mu.Unlock()
|
|
tc.caches = append(tc.caches, c)
|
|
return c
|
|
}
|
|
|
|
// addTokenCache registers a token cache for cleanup
|
|
func (tc *testCleanup) addTokenCache(c *TokenCache) *TokenCache {
|
|
tc.mu.Lock()
|
|
defer tc.mu.Unlock()
|
|
// TokenCache cleanup is handled by the global manager
|
|
// No need to manually close as it's a singleton
|
|
return c
|
|
}
|
|
|
|
// addOIDC registers a TraefikOidc instance for cleanup
|
|
//
|
|
//lint:ignore U1000 Kept for potential future use
|
|
func (tc *testCleanup) addOIDC(o *TraefikOidc) *TraefikOidc {
|
|
tc.mu.Lock()
|
|
defer tc.mu.Unlock()
|
|
tc.oidcs = append(tc.oidcs, o)
|
|
return o
|
|
}
|
|
|
|
// cleanupAll cleans up all registered resources
|
|
func (tc *testCleanup) cleanupAll() {
|
|
tc.mu.Lock()
|
|
defer tc.mu.Unlock()
|
|
|
|
// Close all caches
|
|
for _, c := range tc.caches {
|
|
if c != nil {
|
|
c.Close()
|
|
}
|
|
}
|
|
|
|
// Close all servers
|
|
for _, s := range tc.servers {
|
|
if s != nil {
|
|
s.Close()
|
|
}
|
|
}
|
|
|
|
// Close all OIDC instances
|
|
for _, o := range tc.oidcs {
|
|
if o != nil {
|
|
// Close caches within the OIDC instance
|
|
if o.tokenCache != nil && o.tokenCache.cache != nil {
|
|
o.tokenCache.cache.Close()
|
|
}
|
|
if o.tokenBlacklist != nil {
|
|
o.tokenBlacklist.Close()
|
|
}
|
|
// Call Close if it exists
|
|
o.Close()
|
|
}
|
|
}
|
|
|
|
// Reset global state - commented out as resetGlobalState is unused
|
|
// resetGlobalState()
|
|
}
|
|
|
|
// createTestConfig creates a config with all required fields populated for testing
|
|
// nolint:unused // Kept for potential future use in integration tests
|
|
/*
|
|
func createTestConfig() *Config {
|
|
config := CreateConfig()
|
|
config.ProviderURL = "https://test-provider.com"
|
|
config.ClientID = "test-client-id"
|
|
config.ClientSecret = "test-client-secret"
|
|
config.SessionEncryptionKey = "test-encryption-key-32-characters"
|
|
config.CallbackURL = "/oauth2/callback"
|
|
return config
|
|
}
|
|
*/
|
|
|
|
// setupTestOIDCMiddleware creates a test OIDC middleware instance with mock servers
|
|
// nolint:unused // Kept for potential future use in integration tests
|
|
/*
|
|
func setupTestOIDCMiddleware(t *testing.T, config *Config) (*TraefikOidc, *httptest.Server) {
|
|
// Reset global state to ensure test isolation
|
|
resetGlobalState()
|
|
|
|
// Create mock OIDC server
|
|
var serverURL string
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/.well-known/openid-configuration":
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
|
"issuer": serverURL,
|
|
"authorization_endpoint": serverURL + "/auth",
|
|
"token_endpoint": serverURL + "/token",
|
|
"userinfo_endpoint": serverURL + "/userinfo",
|
|
"jwks_uri": serverURL + "/keys",
|
|
"revocation_endpoint": serverURL + "/revoke",
|
|
})
|
|
case "/keys":
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.Write([]byte(`{
|
|
"keys": [{
|
|
"kty": "RSA",
|
|
"kid": "test-key-id",
|
|
"use": "sig",
|
|
"n": "test-n-value",
|
|
"e": "AQAB"
|
|
}]
|
|
}`))
|
|
case "/token":
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.Write([]byte(`{
|
|
"access_token": "test-access-token",
|
|
"id_token": "` + ValidIDToken + `",
|
|
"refresh_token": "test-refresh-token",
|
|
"token_type": "bearer",
|
|
"expires_in": 3600
|
|
}`))
|
|
default:
|
|
w.WriteHeader(http.StatusNotFound)
|
|
}
|
|
}))
|
|
serverURL = server.URL
|
|
|
|
// Create middleware bypassing validation like main tests do
|
|
// Create a logger that outputs to test log
|
|
logger := &Logger{
|
|
logError: log.New(&testWriter{t}, "ERROR: ", 0),
|
|
logInfo: log.New(&testWriter{t}, "INFO: ", 0),
|
|
logDebug: log.New(&testWriter{t}, "DEBUG: ", 0),
|
|
}
|
|
sessionManager, _ := NewSessionManager(config.SessionEncryptionKey, false, "", "", logger)
|
|
|
|
// Create next handler
|
|
nextHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
})
|
|
|
|
// Set default paths
|
|
callbackPath := config.CallbackURL
|
|
if callbackPath == "" {
|
|
callbackPath = "/oauth2/callback"
|
|
}
|
|
logoutPath := config.LogoutURL
|
|
if logoutPath == "" {
|
|
logoutPath = callbackPath + "/logout"
|
|
}
|
|
|
|
// Set default post logout redirect URI to match the actual implementation
|
|
postLogoutRedirectURI := config.PostLogoutRedirectURI
|
|
if postLogoutRedirectURI == "" {
|
|
postLogoutRedirectURI = "/" // Default to root path like the actual implementation
|
|
}
|
|
|
|
// Use test URLs that won't be blocked by validation
|
|
testIssuerURL := "https://test-provider.example.com"
|
|
testAuthURL := testIssuerURL + "/auth"
|
|
testTokenURL := testIssuerURL + "/token"
|
|
testJWKSURL := testIssuerURL + "/keys"
|
|
|
|
// Create WaitGroup for background goroutines
|
|
var wg sync.WaitGroup
|
|
|
|
// Create context with cancel for proper cleanup
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
// Create TraefikOidc instance directly
|
|
oidc := &TraefikOidc{
|
|
next: nextHandler,
|
|
issuerURL: testIssuerURL,
|
|
clientID: config.ClientID,
|
|
audience: config.ClientID,
|
|
clientSecret: config.ClientSecret,
|
|
redirURLPath: callbackPath,
|
|
logoutURLPath: logoutPath,
|
|
postLogoutRedirectURI: postLogoutRedirectURI,
|
|
limiter: rate.NewLimiter(rate.Every(time.Second), 10),
|
|
tokenBlacklist: NewCache(),
|
|
tokenCache: NewTokenCache(),
|
|
logger: logger,
|
|
excludedURLs: make(map[string]struct{}),
|
|
httpClient: &http.Client{},
|
|
authURL: testAuthURL,
|
|
tokenURL: testTokenURL,
|
|
jwksURL: testJWKSURL,
|
|
initComplete: make(chan struct{}),
|
|
sessionManager: sessionManager,
|
|
extractClaimsFunc: extractClaims,
|
|
enablePKCE: config.EnablePKCE,
|
|
refreshGracePeriod: time.Duration(config.RefreshGracePeriodSeconds) * time.Second,
|
|
revocationURL: config.RevocationURL,
|
|
endSessionURL: config.OIDCEndSessionURL,
|
|
scopes: config.Scopes,
|
|
forceHTTPS: config.ForceHTTPS,
|
|
allowedUserDomains: make(map[string]struct{}),
|
|
jwkCache: NewJWKCache(),
|
|
metadataCache: NewMetadataCache(nil),
|
|
ctx: ctx,
|
|
cancelFunc: cancel,
|
|
goroutineWG: &wg,
|
|
providerURL: serverURL,
|
|
}
|
|
|
|
// Process excluded URLs
|
|
for _, url := range config.ExcludedURLs {
|
|
oidc.excludedURLs[url] = struct{}{}
|
|
}
|
|
|
|
// Set default excluded URLs
|
|
oidc.excludedURLs["/favicon"] = struct{}{}
|
|
oidc.excludedURLs["/favicon.ico"] = struct{}{}
|
|
|
|
// Close init channel
|
|
close(oidc.initComplete)
|
|
|
|
// Set verifiers
|
|
oidc.tokenVerifier = oidc
|
|
oidc.jwtVerifier = oidc
|
|
oidc.tokenExchanger = oidc // Set tokenExchanger to self
|
|
|
|
// Set default refresh grace period if not set or negative
|
|
if config.RefreshGracePeriodSeconds <= 0 {
|
|
oidc.refreshGracePeriod = 60 * time.Second
|
|
}
|
|
|
|
// Set authentication initiation function
|
|
oidc.initiateAuthenticationFunc = func(rw http.ResponseWriter, req *http.Request, session *SessionData, redirectURL string) {
|
|
// Generate CSRF token and nonce
|
|
csrfToken := uuid.NewString()
|
|
nonce := uuid.NewString()
|
|
|
|
// Store in session
|
|
session.SetCSRF(csrfToken)
|
|
session.SetNonce(nonce)
|
|
|
|
// Store the original path
|
|
session.SetIncomingPath(req.URL.RequestURI())
|
|
|
|
// Handle PKCE if enabled
|
|
var codeChallenge string
|
|
if oidc.enablePKCE {
|
|
verifier, _ := generateCodeVerifier()
|
|
session.SetCodeVerifier(verifier)
|
|
codeChallenge = deriveCodeChallenge(verifier)
|
|
}
|
|
|
|
// Save session
|
|
session.Save(req, rw)
|
|
|
|
// Build auth URL
|
|
authURL := oidc.buildAuthURL(redirectURL, csrfToken, nonce, codeChallenge)
|
|
|
|
// Redirect
|
|
http.Redirect(rw, req, authURL, http.StatusFound)
|
|
}
|
|
|
|
// Set scopes if not set
|
|
if len(oidc.scopes) == 0 {
|
|
oidc.scopes = []string{"openid", "profile", "email"}
|
|
}
|
|
|
|
return oidc, server
|
|
}
|
|
*/
|
|
|
|
// createMockJWT creates a mock JWT token for testing - adapter for existing tests
|
|
// nolint:unused // Kept for potential future use in integration tests
|
|
/*
|
|
func createMockJWT(t *testing.T, sub, email string) string {
|
|
return ValidIDToken
|
|
}
|
|
*/
|
|
|
|
// createTestSession creates a properly initialized SessionData for testing
|
|
func createTestSession() *SessionData {
|
|
// Create a minimal session manager for testing
|
|
logger := newNoOpLogger()
|
|
sessionManager, _ := NewSessionManager("test-encryption-key-32-characters", false, "", "", 0, logger)
|
|
|
|
// Create a test request
|
|
req := httptest.NewRequest("GET", "/", nil)
|
|
|
|
// Get a session from the manager
|
|
session, _ := sessionManager.GetSession(req)
|
|
return session
|
|
}
|
|
|
|
// injectSessionIntoRequest saves the session and adds the resulting cookies to the request
|
|
// nolint:unused // Kept for potential future use in integration tests
|
|
/*
|
|
func injectSessionIntoRequest(t *testing.T, req *http.Request, session *SessionData) {
|
|
// Create a response recorder to capture cookies
|
|
rec := httptest.NewRecorder()
|
|
|
|
// Save the session (this sets cookies)
|
|
if err := session.Save(req, rec); err != nil {
|
|
t.Fatalf("Failed to save session: %v", err)
|
|
}
|
|
|
|
// Add the cookies to the request
|
|
for _, cookie := range rec.Result().Cookies() {
|
|
req.AddCookie(cookie)
|
|
}
|
|
}
|
|
*/
|