Files
traefikoidc/ajax_token_expiry_test.go
T

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)
}
})
}
}