mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-10-14 04:07:46 +00:00
358 lines
12 KiB
Go
358 lines
12 KiB
Go
package traefikoidc
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// TestReverseProxyHTTPSDetection tests that HTTPS is properly detected in reverse proxy environments
|
|
func TestReverseProxyHTTPSDetection(t *testing.T) {
|
|
t.Run("HTTPS_Detection_With_X_Forwarded_Proto", func(t *testing.T) {
|
|
sessionManager, err := NewSessionManager("test-encryption-key-32-characters", false, "", NewLogger("debug"))
|
|
require.NoError(t, err)
|
|
|
|
// Simulate request from reverse proxy (Traefik/nginx)
|
|
// The reverse proxy terminates SSL and forwards HTTP internally
|
|
req := httptest.NewRequest("GET", "http://example.com/test", nil)
|
|
req.Header.Set("X-Forwarded-Proto", "https")
|
|
req.Header.Set("X-Forwarded-Host", "example.com")
|
|
|
|
session, err := sessionManager.GetSession(req)
|
|
require.NoError(t, err)
|
|
|
|
// Set critical session data
|
|
session.SetCSRF("important-csrf-token")
|
|
session.SetNonce("test-nonce")
|
|
|
|
// Save session
|
|
rec := httptest.NewRecorder()
|
|
err = session.Save(req, rec)
|
|
require.NoError(t, err)
|
|
|
|
// Verify cookies have Secure flag when X-Forwarded-Proto is https
|
|
cookies := rec.Result().Cookies()
|
|
var mainCookie *http.Cookie
|
|
for _, cookie := range cookies {
|
|
if cookie.Name == "_oidc_raczylo_m" {
|
|
mainCookie = cookie
|
|
break
|
|
}
|
|
}
|
|
require.NotNil(t, mainCookie, "Main session cookie should be set")
|
|
assert.True(t, mainCookie.Secure, "Cookie should have Secure flag when X-Forwarded-Proto is https")
|
|
assert.Equal(t, http.SameSiteLaxMode, mainCookie.SameSite, "Should use Lax SameSite for OAuth compatibility")
|
|
})
|
|
|
|
t.Run("HTTPS_Detection_Without_Headers", func(t *testing.T) {
|
|
sessionManager, err := NewSessionManager("test-encryption-key-32-characters", false, "", NewLogger("debug"))
|
|
require.NoError(t, err)
|
|
|
|
// Request without reverse proxy headers (direct HTTP)
|
|
req := httptest.NewRequest("GET", "http://example.com/test", nil)
|
|
|
|
session, err := sessionManager.GetSession(req)
|
|
require.NoError(t, err)
|
|
|
|
session.SetCSRF("test-csrf")
|
|
|
|
rec := httptest.NewRecorder()
|
|
err = session.Save(req, rec)
|
|
require.NoError(t, err)
|
|
|
|
// Verify cookies don't have Secure flag for plain HTTP
|
|
cookies := rec.Result().Cookies()
|
|
var mainCookie *http.Cookie
|
|
for _, cookie := range cookies {
|
|
if cookie.Name == "_oidc_raczylo_m" {
|
|
mainCookie = cookie
|
|
break
|
|
}
|
|
}
|
|
require.NotNil(t, mainCookie)
|
|
assert.False(t, mainCookie.Secure, "Cookie should not have Secure flag for HTTP")
|
|
assert.Equal(t, http.SameSiteLaxMode, mainCookie.SameSite, "Should use Lax SameSite in HTTP")
|
|
})
|
|
|
|
t.Run("HTTPS_Detection_With_ForceHTTPS", func(t *testing.T) {
|
|
// Test with forceHTTPS enabled
|
|
sessionManager, err := NewSessionManager("test-encryption-key-32-characters", true, "", NewLogger("debug"))
|
|
require.NoError(t, err)
|
|
|
|
// Even without headers, forceHTTPS should make cookies secure
|
|
req := httptest.NewRequest("GET", "http://example.com/test", nil)
|
|
|
|
session, err := sessionManager.GetSession(req)
|
|
require.NoError(t, err)
|
|
|
|
session.SetCSRF("forced-secure-csrf")
|
|
|
|
rec := httptest.NewRecorder()
|
|
err = session.Save(req, rec)
|
|
require.NoError(t, err)
|
|
|
|
cookies := rec.Result().Cookies()
|
|
var mainCookie *http.Cookie
|
|
for _, cookie := range cookies {
|
|
if cookie.Name == "_oidc_raczylo_m" {
|
|
mainCookie = cookie
|
|
break
|
|
}
|
|
}
|
|
require.NotNil(t, mainCookie)
|
|
assert.True(t, mainCookie.Secure, "Cookie should have Secure flag with forceHTTPS")
|
|
})
|
|
}
|
|
|
|
// TestCSRFPersistenceInReverseProxy tests CSRF token persistence in reverse proxy setups
|
|
func TestCSRFPersistenceInReverseProxy(t *testing.T) {
|
|
t.Run("CSRF_Persists_Through_OAuth_Flow_With_Proxy", func(t *testing.T) {
|
|
sessionManager, err := NewSessionManager("test-encryption-key-32-characters", false, "", NewLogger("debug"))
|
|
require.NoError(t, err)
|
|
|
|
// Step 1: Initial request to protected resource (HTTPS via proxy)
|
|
req1 := httptest.NewRequest("GET", "http://example.com/protected", nil)
|
|
req1.Header.Set("X-Forwarded-Proto", "https")
|
|
req1.Header.Set("X-Forwarded-Host", "example.com")
|
|
|
|
session1, err := sessionManager.GetSession(req1)
|
|
require.NoError(t, err)
|
|
|
|
// Set CSRF and other auth flow data
|
|
csrfToken := "proxy-csrf-token-12345"
|
|
session1.SetCSRF(csrfToken)
|
|
session1.SetNonce("proxy-nonce")
|
|
session1.SetIncomingPath("/protected")
|
|
|
|
// Save session (should set Secure cookie)
|
|
rec1 := httptest.NewRecorder()
|
|
err = session1.Save(req1, rec1)
|
|
require.NoError(t, err)
|
|
|
|
cookies := rec1.Result().Cookies()
|
|
|
|
// Step 2: Simulate OAuth callback (also HTTPS via proxy)
|
|
req2 := httptest.NewRequest("GET", "http://example.com/oidc/callback?code=auth-code&state="+csrfToken, nil)
|
|
req2.Header.Set("X-Forwarded-Proto", "https")
|
|
req2.Header.Set("X-Forwarded-Host", "example.com")
|
|
|
|
// Add cookies from step 1
|
|
for _, cookie := range cookies {
|
|
req2.AddCookie(cookie)
|
|
}
|
|
|
|
// Get session in callback
|
|
session2, err := sessionManager.GetSession(req2)
|
|
require.NoError(t, err)
|
|
|
|
// CSRF token should persist
|
|
retrievedCSRF := session2.GetCSRF()
|
|
assert.Equal(t, csrfToken, retrievedCSRF, "CSRF token must persist through OAuth flow in reverse proxy")
|
|
assert.Equal(t, "proxy-nonce", session2.GetNonce(), "Nonce should also persist")
|
|
assert.Equal(t, "/protected", session2.GetIncomingPath(), "Incoming path should persist")
|
|
})
|
|
|
|
t.Run("Session_Cookie_Domain_With_Proxy_Headers", func(t *testing.T) {
|
|
sessionManager, err := NewSessionManager("test-encryption-key-32-characters", false, "", NewLogger("debug"))
|
|
require.NoError(t, err)
|
|
|
|
// Test with X-Forwarded-Host header
|
|
req := httptest.NewRequest("GET", "http://internal.local/test", nil)
|
|
req.Header.Set("X-Forwarded-Proto", "https")
|
|
req.Header.Set("X-Forwarded-Host", "public.example.com")
|
|
req.Host = "internal.local" // Internal host
|
|
|
|
session, err := sessionManager.GetSession(req)
|
|
require.NoError(t, err)
|
|
|
|
session.SetCSRF("domain-test-csrf")
|
|
|
|
rec := httptest.NewRecorder()
|
|
err = session.Save(req, rec)
|
|
require.NoError(t, err)
|
|
|
|
cookies := rec.Result().Cookies()
|
|
var mainCookie *http.Cookie
|
|
for _, cookie := range cookies {
|
|
if cookie.Name == "_oidc_raczylo_m" {
|
|
mainCookie = cookie
|
|
break
|
|
}
|
|
}
|
|
require.NotNil(t, mainCookie)
|
|
|
|
// Domain should be set based on X-Forwarded-Host when present
|
|
// This ensures cookies work correctly with the public domain
|
|
assert.Equal(t, "public.example.com", mainCookie.Domain, "Cookie domain should use forwarded host")
|
|
})
|
|
}
|
|
|
|
// TestAzureOIDCWithReverseProxy simulates Azure OIDC flow behind a reverse proxy
|
|
func TestAzureOIDCWithReverseProxy(t *testing.T) {
|
|
t.Run("Azure_Provider_Detection_And_Configuration", func(t *testing.T) {
|
|
// This test verifies Azure-specific provider detection and configuration
|
|
// without making actual network calls
|
|
|
|
sessionManager, err := NewSessionManager("test-encryption-key-32-characters", false, "", NewLogger("debug"))
|
|
require.NoError(t, err)
|
|
|
|
// Step 1: Test session setup for Azure OAuth flow
|
|
req := httptest.NewRequest("GET", "http://internal/protected", nil)
|
|
req.Header.Set("X-Forwarded-Proto", "https")
|
|
req.Header.Set("X-Forwarded-Host", "app.example.com")
|
|
|
|
session, err := sessionManager.GetSession(req)
|
|
require.NoError(t, err)
|
|
|
|
// Simulate OAuth flow initialization
|
|
csrfToken := "azure-csrf-token"
|
|
nonce := "azure-nonce"
|
|
session.SetCSRF(csrfToken)
|
|
session.SetNonce(nonce)
|
|
session.SetIncomingPath("/protected")
|
|
session.MarkDirty()
|
|
|
|
// Save session with proper HTTPS detection
|
|
rec := httptest.NewRecorder()
|
|
err = session.Save(req, rec)
|
|
require.NoError(t, err)
|
|
|
|
// Verify cookies have correct security attributes for Azure
|
|
cookies := rec.Result().Cookies()
|
|
var mainCookie *http.Cookie
|
|
for _, cookie := range cookies {
|
|
if cookie.Name == "_oidc_raczylo_m" {
|
|
mainCookie = cookie
|
|
break
|
|
}
|
|
}
|
|
require.NotNil(t, mainCookie, "Main session cookie must be set")
|
|
assert.True(t, mainCookie.Secure, "Cookie must be secure for HTTPS reverse proxy")
|
|
assert.Equal(t, http.SameSiteLaxMode, mainCookie.SameSite, "Should use Lax SameSite for OAuth compatibility")
|
|
|
|
// Step 2: Simulate callback and verify session persistence
|
|
callbackReq := httptest.NewRequest("GET", "http://internal/oidc/callback?code=azure-code&state="+csrfToken, nil)
|
|
callbackReq.Header.Set("X-Forwarded-Proto", "https")
|
|
callbackReq.Header.Set("X-Forwarded-Host", "app.example.com")
|
|
|
|
// Add cookies from initial request
|
|
for _, cookie := range cookies {
|
|
callbackReq.AddCookie(cookie)
|
|
}
|
|
|
|
// Get session in callback
|
|
callbackSession, err := sessionManager.GetSession(callbackReq)
|
|
require.NoError(t, err)
|
|
|
|
// Verify session data persisted correctly
|
|
assert.Equal(t, csrfToken, callbackSession.GetCSRF(), "CSRF token must persist in Azure flow")
|
|
assert.Equal(t, nonce, callbackSession.GetNonce(), "Nonce must persist")
|
|
assert.Equal(t, "/protected", callbackSession.GetIncomingPath(), "Original path must persist")
|
|
})
|
|
|
|
t.Run("Mixed_HTTP_HTTPS_Requests", func(t *testing.T) {
|
|
sessionManager, err := NewSessionManager("test-encryption-key-32-characters", false, "", NewLogger("debug"))
|
|
require.NoError(t, err)
|
|
|
|
// Simulate a scenario where some requests come via HTTPS proxy and some don't
|
|
// This can happen in development or misconfigured environments
|
|
|
|
// Request 1: HTTPS via proxy
|
|
req1 := httptest.NewRequest("GET", "http://localhost:8080/test", nil)
|
|
req1.Header.Set("X-Forwarded-Proto", "https")
|
|
|
|
session1, err := sessionManager.GetSession(req1)
|
|
require.NoError(t, err)
|
|
session1.SetCSRF("mixed-csrf")
|
|
|
|
rec1 := httptest.NewRecorder()
|
|
err = session1.Save(req1, rec1)
|
|
require.NoError(t, err)
|
|
|
|
cookies1 := rec1.Result().Cookies()
|
|
|
|
// Request 2: Direct HTTP (no proxy headers)
|
|
req2 := httptest.NewRequest("GET", "http://localhost:8080/test", nil)
|
|
// No X-Forwarded-Proto header
|
|
|
|
// Try to use cookies from HTTPS request
|
|
for _, cookie := range cookies1 {
|
|
// Remove Secure flag to simulate browser behavior
|
|
// (browser wouldn't send secure cookie over HTTP)
|
|
if !cookie.Secure {
|
|
req2.AddCookie(cookie)
|
|
}
|
|
}
|
|
|
|
session2, err := sessionManager.GetSession(req2)
|
|
require.NoError(t, err)
|
|
|
|
// Session should be empty because secure cookies weren't sent
|
|
csrf2 := session2.GetCSRF()
|
|
assert.Empty(t, csrf2, "CSRF should be empty when secure cookies can't be sent over HTTP")
|
|
})
|
|
}
|
|
|
|
// TestEnhanceSessionSecurity verifies the security enhancement function
|
|
func TestEnhanceSessionSecurity(t *testing.T) {
|
|
t.Run("Security_Enhancement_For_AJAX_Requests", func(t *testing.T) {
|
|
sessionManager, err := NewSessionManager("test-encryption-key-32-characters", false, "", NewLogger("debug"))
|
|
require.NoError(t, err)
|
|
|
|
// AJAX request via HTTPS proxy
|
|
req := httptest.NewRequest("GET", "http://internal/api/data", nil)
|
|
req.Header.Set("X-Forwarded-Proto", "https")
|
|
req.Header.Set("X-Requested-With", "XMLHttpRequest")
|
|
|
|
session, err := sessionManager.GetSession(req)
|
|
require.NoError(t, err)
|
|
|
|
session.SetCSRF("ajax-csrf")
|
|
|
|
rec := httptest.NewRecorder()
|
|
err = session.Save(req, rec)
|
|
require.NoError(t, err)
|
|
|
|
// Check that AJAX requests get strict same-site
|
|
cookies := rec.Result().Cookies()
|
|
for _, cookie := range cookies {
|
|
if cookie.Name == "_oidc_raczylo_m" {
|
|
assert.Equal(t, http.SameSiteStrictMode, cookie.SameSite, "AJAX requests should use Strict SameSite")
|
|
break
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("Security_Enhancement_Missing_User_Agent", func(t *testing.T) {
|
|
sessionManager, err := NewSessionManager("test-encryption-key-32-characters", false, "", NewLogger("debug"))
|
|
require.NoError(t, err)
|
|
|
|
// Request without User-Agent (potential bot/attack)
|
|
req := httptest.NewRequest("GET", "http://example.com/test", nil)
|
|
req.Header.Set("X-Forwarded-Proto", "https")
|
|
// No User-Agent header
|
|
|
|
session, err := sessionManager.GetSession(req)
|
|
require.NoError(t, err)
|
|
|
|
session.SetCSRF("no-ua-csrf")
|
|
|
|
rec := httptest.NewRecorder()
|
|
err = session.Save(req, rec)
|
|
require.NoError(t, err)
|
|
|
|
// Verify reduced session timeout for suspicious requests
|
|
cookies := rec.Result().Cookies()
|
|
for _, cookie := range cookies {
|
|
if cookie.Name == "_oidc_raczylo_m" {
|
|
// Should have reduced MaxAge (half of normal)
|
|
assert.Less(t, cookie.MaxAge, int(absoluteSessionTimeout.Seconds()), "Suspicious requests should have reduced timeout")
|
|
break
|
|
}
|
|
}
|
|
})
|
|
}
|