mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-10-14 04:07:46 +00:00
182 lines
6.8 KiB
Go
182 lines
6.8 KiB
Go
package traefikoidc
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// TestAzureOAuthCallbackScenario tests the exact scenario from issue #53
|
|
// This test ensures that cookies set during OAuth initiation are available
|
|
// during the callback from Azure AD
|
|
func TestAzureOAuthCallbackScenario(t *testing.T) {
|
|
t.Run("Azure_OAuth_Complete_Flow", func(t *testing.T) {
|
|
sessionManager, err := NewSessionManager("test-encryption-key-32-characters", false, "", NewLogger("debug"))
|
|
require.NoError(t, err)
|
|
|
|
// Step 1: User visits https://app.example.com/protected
|
|
// Traefik receives this as http://internal/protected with headers
|
|
initReq := httptest.NewRequest("GET", "http://internal/protected", nil)
|
|
initReq.Header.Set("X-Forwarded-Proto", "https")
|
|
initReq.Header.Set("X-Forwarded-Host", "app.example.com")
|
|
initReq.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)")
|
|
initReq.Host = "internal" // The actual host Traefik sees
|
|
|
|
// Get session and prepare for OAuth
|
|
session, err := sessionManager.GetSession(initReq)
|
|
require.NoError(t, err)
|
|
|
|
// Set OAuth flow data
|
|
csrfToken := "azure-csrf-state-token"
|
|
nonce := "azure-nonce-value"
|
|
codeVerifier := "pkce-code-verifier"
|
|
|
|
session.SetCSRF(csrfToken)
|
|
session.SetNonce(nonce)
|
|
session.SetCodeVerifier(codeVerifier)
|
|
session.SetIncomingPath("/protected")
|
|
session.MarkDirty()
|
|
|
|
// Save session
|
|
rec := httptest.NewRecorder()
|
|
err = session.Save(initReq, rec)
|
|
require.NoError(t, err)
|
|
|
|
// Examine the cookies that would be sent to the browser
|
|
cookies := rec.Result().Cookies()
|
|
require.NotEmpty(t, cookies, "Cookies must be set for OAuth flow")
|
|
|
|
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")
|
|
|
|
// Verify cookie attributes for Azure OAuth
|
|
assert.True(t, mainCookie.Secure, "Cookie MUST be Secure for HTTPS")
|
|
assert.Equal(t, http.SameSiteLaxMode, mainCookie.SameSite,
|
|
"MUST be Lax to allow Azure callback from different domain")
|
|
assert.Equal(t, "app.example.com", mainCookie.Domain,
|
|
"Domain must match X-Forwarded-Host for browser to send it back")
|
|
assert.Equal(t, "/", mainCookie.Path, "Path must be root")
|
|
assert.True(t, mainCookie.HttpOnly, "HttpOnly for security")
|
|
|
|
// Step 2: User is redirected to Azure AD login
|
|
// Azure AD redirects back to https://app.example.com/oidc/callback?code=xxx&state=xxx
|
|
// Traefik receives this as http://internal/oidc/callback with headers
|
|
|
|
callbackReq := httptest.NewRequest("GET",
|
|
"http://internal/oidc/callback?code=AzureAuthCode&state="+csrfToken, nil)
|
|
callbackReq.Header.Set("X-Forwarded-Proto", "https")
|
|
callbackReq.Header.Set("X-Forwarded-Host", "app.example.com")
|
|
callbackReq.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)")
|
|
callbackReq.Host = "internal"
|
|
|
|
// Browser sends cookies because:
|
|
// 1. Request is to https://app.example.com (matches cookie domain)
|
|
// 2. Cookie has Secure flag and request is HTTPS
|
|
// 3. Cookie has SameSite=Lax which allows top-level navigation from Azure
|
|
for _, cookie := range cookies {
|
|
callbackReq.AddCookie(cookie)
|
|
}
|
|
|
|
// Get session in callback
|
|
callbackSession, err := sessionManager.GetSession(callbackReq)
|
|
require.NoError(t, err)
|
|
|
|
// Verify session data is available - THIS WAS FAILING IN ISSUE #53
|
|
retrievedCSRF := callbackSession.GetCSRF()
|
|
assert.Equal(t, csrfToken, retrievedCSRF,
|
|
"CSRF token MUST be available in callback (was missing in issue #53)")
|
|
|
|
retrievedNonce := callbackSession.GetNonce()
|
|
assert.Equal(t, nonce, retrievedNonce,
|
|
"Nonce MUST be available for security validation")
|
|
|
|
retrievedCodeVerifier := callbackSession.GetCodeVerifier()
|
|
assert.Equal(t, codeVerifier, retrievedCodeVerifier,
|
|
"PKCE verifier MUST be available for token exchange")
|
|
|
|
retrievedPath := callbackSession.GetIncomingPath()
|
|
assert.Equal(t, "/protected", retrievedPath,
|
|
"Original path MUST be available for post-auth redirect")
|
|
})
|
|
|
|
t.Run("Cookie_Not_Sent_With_Wrong_Domain", func(t *testing.T) {
|
|
// This test verifies that cookies with wrong domain won't be sent
|
|
sessionManager, err := NewSessionManager("test-encryption-key-32-characters", false, "", NewLogger("debug"))
|
|
require.NoError(t, err)
|
|
|
|
// Initial request sets cookie for app.example.com
|
|
initReq := httptest.NewRequest("GET", "http://internal/protected", nil)
|
|
initReq.Header.Set("X-Forwarded-Proto", "https")
|
|
initReq.Header.Set("X-Forwarded-Host", "app.example.com")
|
|
initReq.Header.Set("User-Agent", "Mozilla/5.0")
|
|
|
|
session, err := sessionManager.GetSession(initReq)
|
|
require.NoError(t, err)
|
|
session.SetCSRF("test-csrf")
|
|
|
|
rec := httptest.NewRecorder()
|
|
err = session.Save(initReq, rec)
|
|
require.NoError(t, err)
|
|
|
|
// Callback comes to different domain
|
|
callbackReq := httptest.NewRequest("GET", "http://internal/oidc/callback", nil)
|
|
callbackReq.Header.Set("X-Forwarded-Proto", "https")
|
|
callbackReq.Header.Set("X-Forwarded-Host", "different.example.com") // Different domain!
|
|
callbackReq.Header.Set("User-Agent", "Mozilla/5.0")
|
|
|
|
// Browser wouldn't send cookies because domain doesn't match
|
|
// So we simulate that by not adding cookies
|
|
|
|
callbackSession, err := sessionManager.GetSession(callbackReq)
|
|
require.NoError(t, err)
|
|
|
|
// Session should be empty
|
|
assert.Empty(t, callbackSession.GetCSRF(),
|
|
"CSRF should be empty when cookies aren't sent due to domain mismatch")
|
|
})
|
|
|
|
t.Run("SameSite_Strict_Would_Break_OAuth", func(t *testing.T) {
|
|
// This test demonstrates why we can't use SameSite=Strict for OAuth
|
|
// With Strict, cookies wouldn't be sent when redirecting from Azure to our app
|
|
|
|
sessionManager, err := NewSessionManager("test-encryption-key-32-characters", false, "", NewLogger("debug"))
|
|
require.NoError(t, err)
|
|
|
|
// If we had SameSite=Strict (which we don't anymore), the browser would:
|
|
// 1. Set cookie when user visits app.example.com
|
|
// 2. NOT send cookie when Azure redirects back to app.example.com/callback
|
|
// This is because the request originates from login.microsoftonline.com
|
|
|
|
// Our fix ensures we use SameSite=Lax which allows top-level navigation
|
|
req := httptest.NewRequest("GET", "http://internal/test", 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)
|
|
session.SetCSRF("test")
|
|
|
|
rec := httptest.NewRecorder()
|
|
err = session.Save(req, rec)
|
|
require.NoError(t, err)
|
|
|
|
cookies := rec.Result().Cookies()
|
|
for _, cookie := range cookies {
|
|
if cookie.Name == "_oidc_raczylo_m" {
|
|
assert.Equal(t, http.SameSiteLaxMode, cookie.SameSite,
|
|
"Must use Lax, not Strict, for OAuth to work")
|
|
break
|
|
}
|
|
}
|
|
})
|
|
}
|