Add support for the anchor redirects

This commit is contained in:
2025-03-18 01:57:59 +00:00
parent 4322407129
commit a3b8cbf9f3
2 changed files with 121 additions and 7 deletions
+34 -6
View File
@@ -286,13 +286,41 @@ func (t *TraefikOidc) handleCallback(rw http.ResponseWriter, req *http.Request,
http.Error(rw, "Failed to save session", http.StatusInternalServerError)
return
}
// Redirect to original path or root
redirectPath := "/"
if incomingPath := session.GetIncomingPath(); incomingPath != "" && incomingPath != t.redirURLPath {
redirectPath = incomingPath
}
// Redirect to original path or root
redirectPath := "/"
if incomingPath := session.GetIncomingPath(); incomingPath != "" && incomingPath != t.redirURLPath {
redirectPath = incomingPath
}
// For redirecting, we need to ensure URL fragments are preserved
// To do this, we'll use a small JavaScript snippet that preserves any URL fragments
// This is necessary because URL fragments are not sent to the server
rw.Header().Set("Content-Type", "text/html; charset=utf-8")
rw.WriteHeader(http.StatusOK)
fmt.Fprintf(rw, `<!DOCTYPE html>
<html>
<head>
<title>Authentication Complete</title>
<script>
// Preserve URL fragments by combining the redirectPath with any fragment in the current URL
(function() {
var redirectPath = %q;
var redirectUrl = new URL(redirectPath, window.location.href);
// If we have a hash in the current URL, and the redirect path doesn't already have one,
// append the hash to the redirect URL to preserve anchors
if (window.location.hash && !redirectPath.includes('#')) {
redirectUrl.hash = window.location.hash;
}
window.location.replace(redirectUrl.toString());
})();
</script>
</head>
<body>
<p>Authentication successful. Redirecting...</p>
</body>
</html>`, redirectPath)
http.Redirect(rw, req, redirectPath, http.StatusFound)
}
+87 -1
View File
@@ -513,7 +513,7 @@ func TestHandleCallback(t *testing.T) {
session.SetCSRF("test-csrf-token")
session.SetNonce("test-nonce")
},
expectedStatus: http.StatusFound,
expectedStatus: http.StatusOK, // Changed from StatusFound since we now return HTML instead of redirect
},
{
name: "Missing Code",
@@ -2056,6 +2056,92 @@ func TestExchangeCodeForToken(t *testing.T) {
}
}
// TestHandleCallback_PreservesURLFragments tests that URL fragments (anchors) are preserved during the authentication callback process.
func TestHandleCallback_PreservesURLFragments(t *testing.T) {
ts := &TestSuite{t: t}
ts.Setup()
// Create a new instance for this specific test
logger := NewLogger("info")
sessionManager, _ := NewSessionManager("test-secret-key-that-is-at-least-32-bytes", false, logger)
tOidc := &TraefikOidc{
allowedUserDomains: map[string]struct{}{"example.com": {}},
logger: logger,
tokenVerifier: ts.tOidc.tokenVerifier,
jwtVerifier: ts.tOidc.jwtVerifier,
sessionManager: sessionManager,
redirURLPath: "/callback",
extractClaimsFunc: func(tokenString string) (map[string]interface{}, error) {
return map[string]interface{}{
"email": "user@example.com",
"nonce": "test-nonce",
}, nil
},
exchangeCodeForTokenFunc: func(code string, redirectURL string, codeVerifier string) (*TokenResponse, error) {
return &TokenResponse{
IDToken: ts.token,
RefreshToken: "test-refresh-token",
}, nil
},
}
// Create a request with the callback URL
req := httptest.NewRequest("GET", "/callback?code=test-code&state=test-csrf-token", nil)
rr := httptest.NewRecorder()
// Create session with an incoming path that contains a URL fragment
session, err := sessionManager.GetSession(req)
if err != nil {
t.Fatalf("Failed to get session: %v", err)
}
// Set up the session with necessary values and an incoming path with a fragment
session.SetCSRF("test-csrf-token")
session.SetNonce("test-nonce")
session.SetIncomingPath("/dashboard?param=value") // The fragment will be client-side only
if err := session.Save(req, rr); err != nil {
t.Fatalf("Failed to save session: %v", err)
}
// Copy cookies to the request
for _, cookie := range rr.Result().Cookies() {
req.AddCookie(cookie)
}
// Reset response recorder
rr = httptest.NewRecorder()
// Call handleCallback
tOidc.handleCallback(rr, req, "http://example.com/callback")
// The response should be OK (200) since we're returning HTML, not a redirect
if rr.Code != http.StatusOK {
t.Errorf("Expected status 200, got %d", rr.Code)
}
// Verify that the response is HTML and contains our JavaScript for preserving fragments
contentType := rr.Header().Get("Content-Type")
if !strings.Contains(contentType, "text/html") {
t.Errorf("Expected Content-Type to contain 'text/html', got %s", contentType)
}
// Verify the response contains the redirect path and JavaScript for preserving fragments
body := rr.Body.String()
if !strings.Contains(body, "/dashboard?param=value") {
t.Errorf("Response body doesn't contain the original redirect path")
}
if !strings.Contains(body, "window.location.hash") {
t.Errorf("Response doesn't contain JavaScript logic to preserve URL fragments")
}
if !strings.Contains(body, "redirectUrl.hash = window.location.hash") {
t.Errorf("Response doesn't contain logic to copy the fragment from current URL")
}
}
// TestDefaultInitiateAuthentication_PreservesQueryParameters tests that defaultInitiateAuthentication preserves query parameters in the incoming path.
func TestDefaultInitiateAuthentication_PreservesQueryParameters(t *testing.T) {
ts := &TestSuite{t: t}