mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-10-14 04:07:46 +00:00
263 lines
7.7 KiB
Go
263 lines
7.7 KiB
Go
package traefikoidc
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// TestAjaxRequestsWith6HourExpiredTokens tests the fix for the production issue:
|
|
// After 6 hours of browser inactivity, AJAX requests should get 401 responses
|
|
// instead of redirects that lead to /unknown-session errors
|
|
func TestAjaxRequestsWith6HourExpiredTokens(t *testing.T) {
|
|
t.Log("Testing AJAX requests with 6-hour expired tokens - verifying production fix")
|
|
|
|
// Create test session manager
|
|
sm, err := NewSessionManager("test_key_for_encryption_12345678", false, "", NewLogger("debug"))
|
|
if err != nil {
|
|
t.Fatalf("Failed to create session manager: %v", err)
|
|
}
|
|
|
|
// Set up middleware
|
|
tOidc := &TraefikOidc{
|
|
next: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte("Request successful"))
|
|
}),
|
|
name: "test-ajax-expiry",
|
|
clientID: "test-client-id",
|
|
clientSecret: "test-client-secret",
|
|
sessionManager: sm,
|
|
extractClaimsFunc: extractClaims,
|
|
logger: NewLogger("debug"),
|
|
redirURLPath: "/oauth/callback",
|
|
issuerURL: "https://test-issuer.com",
|
|
initComplete: make(chan struct{}),
|
|
httpClient: &http.Client{Timeout: 10 * time.Second},
|
|
tokenHTTPClient: &http.Client{Timeout: 10 * time.Second},
|
|
tokenCache: NewTokenCache(),
|
|
tokenBlacklist: NewCache(),
|
|
refreshGracePeriod: 1 * time.Minute,
|
|
}
|
|
tOidc.tokenVerifier = tOidc
|
|
tOidc.jwtVerifier = tOidc
|
|
tOidc.tokenExchanger = tOidc
|
|
|
|
// Close the initComplete channel to bypass the waiting
|
|
close(tOidc.initComplete)
|
|
|
|
// Test different AJAX request types
|
|
testCases := []struct {
|
|
name string
|
|
headers map[string]string
|
|
expectCode int
|
|
expectJSON bool
|
|
description string
|
|
}{
|
|
{
|
|
name: "jQuery AJAX with expired tokens",
|
|
headers: map[string]string{
|
|
"X-Requested-With": "XMLHttpRequest",
|
|
"Accept": "application/json",
|
|
},
|
|
expectCode: http.StatusUnauthorized,
|
|
expectJSON: true,
|
|
description: "Should return 401 for jQuery AJAX requests",
|
|
},
|
|
{
|
|
name: "Fetch API with expired tokens",
|
|
headers: map[string]string{
|
|
"Sec-Fetch-Mode": "cors",
|
|
"Accept": "application/json",
|
|
},
|
|
expectCode: http.StatusUnauthorized,
|
|
expectJSON: true,
|
|
description: "Should return 401 for Fetch API requests",
|
|
},
|
|
{
|
|
name: "JSON API request with expired tokens",
|
|
headers: map[string]string{
|
|
"Accept": "application/json",
|
|
"Content-Type": "application/json",
|
|
},
|
|
expectCode: http.StatusUnauthorized,
|
|
expectJSON: true,
|
|
description: "Should return 401 for JSON API requests",
|
|
},
|
|
{
|
|
name: "Browser navigation with expired tokens",
|
|
headers: map[string]string{
|
|
"Accept": "text/html,application/xhtml+xml",
|
|
},
|
|
expectCode: http.StatusFound, // 302
|
|
expectJSON: false,
|
|
description: "Should redirect for normal browser navigation",
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Log(tc.description)
|
|
|
|
// Create request
|
|
req := httptest.NewRequest("GET", "/api/data", nil)
|
|
req.Header.Set("X-Forwarded-Proto", "https")
|
|
req.Header.Set("X-Forwarded-Host", "example.com")
|
|
|
|
// Add test-specific headers
|
|
for key, value := range tc.headers {
|
|
req.Header.Set(key, value)
|
|
}
|
|
|
|
// Create session with 6+ hour old tokens
|
|
session, err := tOidc.sessionManager.GetSession(req)
|
|
if err != nil {
|
|
t.Fatalf("Failed to get session: %v", err)
|
|
}
|
|
|
|
// Simulate authenticated session with expired tokens (6 hours old)
|
|
session.SetAuthenticated(true)
|
|
session.SetEmail("[email protected]")
|
|
|
|
// Set refresh token with old timestamp (7 hours ago)
|
|
session.SetRefreshToken("refresh_token_7_hours_old")
|
|
// Manually set the issued_at to 7 hours ago
|
|
session.refreshSession.Values["issued_at"] = time.Now().Add(-7 * time.Hour).Unix()
|
|
|
|
// Don't set access/ID tokens (simulating they're expired/missing)
|
|
|
|
// Save session
|
|
rr := httptest.NewRecorder()
|
|
if err := session.Save(req, rr); err != nil {
|
|
t.Fatalf("Failed to save session: %v", err)
|
|
}
|
|
|
|
// Apply cookies from session save to request
|
|
for _, cookie := range rr.Result().Cookies() {
|
|
req.AddCookie(cookie)
|
|
}
|
|
|
|
// Make request
|
|
rr = httptest.NewRecorder()
|
|
tOidc.ServeHTTP(rr, req)
|
|
|
|
// Verify response
|
|
if rr.Code != tc.expectCode {
|
|
t.Errorf("Expected status code %d, got %d", tc.expectCode, rr.Code)
|
|
t.Logf("Response body: %s", rr.Body.String())
|
|
}
|
|
|
|
// Check if JSON response for AJAX requests
|
|
if tc.expectJSON {
|
|
var jsonResp map[string]interface{}
|
|
if err := json.Unmarshal(rr.Body.Bytes(), &jsonResp); err != nil {
|
|
t.Errorf("Expected JSON response for AJAX request, got: %s", rr.Body.String())
|
|
} else {
|
|
if errorMsg, ok := jsonResp["error"].(string); ok {
|
|
t.Logf("JSON error response: %s", errorMsg)
|
|
// The error message is "Unauthorized" from sendErrorResponse
|
|
if errorMsg != "Unauthorized" && errorMsg != "Session expired" && errorMsg != "Authentication required" {
|
|
t.Errorf("Unexpected error message: %s", errorMsg)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check for problematic redirects
|
|
if rr.Code == http.StatusFound || rr.Code == http.StatusTemporaryRedirect {
|
|
location := rr.Header().Get("Location")
|
|
if location == "" {
|
|
t.Error("Redirect response missing Location header")
|
|
} else {
|
|
t.Logf("Redirect location: %s", location)
|
|
// The fix should prevent /unknown-session redirects
|
|
if tc.expectJSON {
|
|
t.Error("AJAX request should not be redirected")
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
t.Log("✅ AJAX token expiry fix verified - no more /unknown-session errors for AJAX requests")
|
|
}
|
|
|
|
// TestRefreshTokenExpiryDetection tests the refresh token expiry detection logic
|
|
func TestRefreshTokenExpiryDetection(t *testing.T) {
|
|
tOidc := &TraefikOidc{
|
|
logger: NewLogger("debug"),
|
|
}
|
|
|
|
// Create test session manager
|
|
sm, err := NewSessionManager("test_key_for_encryption_12345678", false, "", NewLogger("debug"))
|
|
if err != nil {
|
|
t.Fatalf("Failed to create session manager: %v", err)
|
|
}
|
|
req := httptest.NewRequest("GET", "/test", nil)
|
|
session, err := sm.GetSession(req)
|
|
if err != nil {
|
|
t.Fatalf("Failed to get session: %v", err)
|
|
}
|
|
|
|
testCases := []struct {
|
|
name string
|
|
setupSession func()
|
|
expectedExpired bool
|
|
}{
|
|
{
|
|
name: "Fresh refresh token",
|
|
setupSession: func() {
|
|
session.SetRefreshToken("fresh_token")
|
|
session.refreshSession.Values["issued_at"] = time.Now().Unix()
|
|
},
|
|
expectedExpired: false,
|
|
},
|
|
{
|
|
name: "5 hour old refresh token",
|
|
setupSession: func() {
|
|
session.SetRefreshToken("5_hour_old_token")
|
|
session.refreshSession.Values["issued_at"] = time.Now().Add(-5 * time.Hour).Unix()
|
|
},
|
|
expectedExpired: false,
|
|
},
|
|
{
|
|
name: "6.5 hour old refresh token",
|
|
setupSession: func() {
|
|
session.SetRefreshToken("expired_token")
|
|
session.refreshSession.Values["issued_at"] = time.Now().Add(-6*time.Hour - 30*time.Minute).Unix()
|
|
},
|
|
expectedExpired: true,
|
|
},
|
|
{
|
|
name: "24 hour old refresh token",
|
|
setupSession: func() {
|
|
session.SetRefreshToken("very_old_token")
|
|
session.refreshSession.Values["issued_at"] = time.Now().Add(-24 * time.Hour).Unix()
|
|
},
|
|
expectedExpired: true,
|
|
},
|
|
{
|
|
name: "No timestamp available",
|
|
setupSession: func() {
|
|
session.SetRefreshToken("unknown_age_token")
|
|
delete(session.refreshSession.Values, "issued_at")
|
|
},
|
|
expectedExpired: false, // Conservative: try refresh if we don't know age
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
tc.setupSession()
|
|
|
|
isExpired := tOidc.isRefreshTokenExpired(session)
|
|
|
|
if isExpired != tc.expectedExpired {
|
|
t.Errorf("Expected refresh token expired=%v, got %v", tc.expectedExpired, isExpired)
|
|
}
|
|
})
|
|
}
|
|
}
|