This is an automated email from the ASF dual-hosted git repository.

more pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/knox.git


The following commit(s) were added to refs/heads/master by this push:
     new 9def208d2 KNOX-3186 - istio external authorizer support for 
SSOCookieProvider (#1081)
9def208d2 is described below

commit 9def208d2bdf36d940d720ffef58da4d1c7534e4
Author: Sandeep MorĂ© <[email protected]>
AuthorDate: Fri Sep 5 16:53:23 2025 -0400

    KNOX-3186 - istio external authorizer support for SSOCookieProvider (#1081)
---
 build.xml                                          |   1 -
 .../provider/federation/jwt/JWTMessages.java       |  12 +
 .../jwt/filter/SSOCookieFederationFilter.java      | 100 ++++++-
 .../provider/federation/SSOCookieProviderTest.java | 333 +++++++++++++++++++++
 4 files changed, 441 insertions(+), 5 deletions(-)

diff --git a/build.xml b/build.xml
index fc08f2285..23a8772a2 100644
--- a/build.xml
+++ b/build.xml
@@ -423,7 +423,6 @@ Release build file for the Apache Knox Gateway
     <target name="start-debug-gateway" description="Start test gateway server 
enabling remote debugging.">
         <exec executable="java" 
dir="${install.dir}/${gateway-artifact}-${gateway-version}">
             <arg 
value="-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005"/>
-            <arg 
value="--add-exports=java.naming/com.sun.jndi.ldap=ALL-UNNAMED"/> 
             <arg value="-jar"/>
             <arg value="bin/gateway.jar"/>
         </exec>
diff --git 
a/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/JWTMessages.java
 
b/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/JWTMessages.java
index b5ba653f3..c8c4f3781 100644
--- 
a/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/JWTMessages.java
+++ 
b/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/JWTMessages.java
@@ -126,4 +126,16 @@ public interface JWTMessages {
 
   @Message(level = MessageLevel.ERROR, text = "Invalid URL ignored. Not a 
valid JWKS url {0}")
   void invalidJwksUrl(String jwksUrl);
+
+  @Message(level = MessageLevel.ERROR, text = "Original redirect URL is not in 
the whitelist {0}")
+  void invalidOriginalUrlDomain(String originalMainDomain);
+
+  @Message( level = MessageLevel.INFO, text = "Using original url: {0}, set by 
the header: {1}" )
+  void originalHeaderURLForwarding( String originalUrl, String header);
+
+  @Message( level = MessageLevel.INFO, text = "SSO is configured to use 
originalURL from request header: {0}" )
+  void usingOriginalUrlFromHeader( String originalUrl);
+
+  @Message(level = MessageLevel.ERROR, text = "Malformed original url passed: 
{0}")
+  void malformedOriginalUrlDomain(String originalMainDomain);
 }
diff --git 
a/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/SSOCookieFederationFilter.java
 
b/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/SSOCookieFederationFilter.java
index 314fda9e2..7f3af11d3 100644
--- 
a/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/SSOCookieFederationFilter.java
+++ 
b/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/SSOCookieFederationFilter.java
@@ -47,6 +47,7 @@ import java.net.MalformedURLException;
 import java.net.URL;
 import java.nio.charset.StandardCharsets;
 import java.text.ParseException;
+import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
@@ -67,6 +68,25 @@ public class SSOCookieFederationFilter extends 
AbstractJWTFilter {
   public static final String X_FORWARDED_PORT = "X-Forwarded-Port";
   public static final String X_FORWARDED_PROTO = "X-Forwarded-Proto";
 
+  /* Overwrite original from header */
+  /* Feature flag to turn the original url from header for SSO ON */
+  public static final String SHOULD_USE_ORIGINAL_URL_FROM_HEADER = 
"sso.use.original.url.from.header";
+  public static final String X_ORIGINAL_URL = "X-Original-URL";
+  /* Users can choose to use custom header names */
+  public static final String X_ORIGINAL_URL_HEADER_NAME = 
"sso.original.url.from.header.name";
+  private static final boolean DEFAULT_SHOULD_USE_ORIGINAL_URL_FROM_HEADER = 
false;
+  /* Should we check for domain in configured whitelist? */
+  public static final String VERIFY_ORIGINAL_URL_FROM_HEADER_DOMAIN = 
"sso.original.url.from.header.verify.domain";
+  /*
+  * This is ONLY needed when you want tighter access,
+  * we already have `knoxsso.redirect.whitelist.regex` property
+  * that checks for redirect URL. If you add domains to whitelist here
+  * make sure they are added there as well.
+  */
+  private static final boolean DEFAULT_VERIFY_ORIGINAL_URL_FROM_HEADER_DOMAIN 
= false;
+  /* Param that specifies the whitelist for original url header domains, 
domains are comma seperated list */
+  public static final String VERIFY_ORIGINAL_URL_FROM_HEADER_DOMAIN_WHITELIST 
= "sso.original.url.from.header.domain.whitelist";
+
   private static final String ORIGINAL_URL_QUERY_PARAM = "originalUrl=";
   public static final String DEFAULT_SSO_COOKIE_NAME = "hadoop-jwt";
 
@@ -76,7 +96,12 @@ public class SSOCookieFederationFilter extends 
AbstractJWTFilter {
   private String cookieName;
   private String authenticationProviderUrl;
   private String gatewayPath;
-  private Set<String> unAuthenticatedPaths = new HashSet<>(20);
+  private final Set<String> unAuthenticatedPaths = new HashSet<>(20);
+
+  private boolean shouldUseOriginalUrlFromHeader = 
DEFAULT_SHOULD_USE_ORIGINAL_URL_FROM_HEADER;
+  private boolean verifyOriginalUrlFromHeaderDomain = 
DEFAULT_VERIFY_ORIGINAL_URL_FROM_HEADER_DOMAIN;
+  private final List<String> verifyOriginalUrlFromHeaderDomainWhitelist = new 
ArrayList<>();
+  private String originalUrlHeaderName;
 
   @Override
   public void init( FilterConfig filterConfig ) throws ServletException {
@@ -121,6 +146,46 @@ public class SSOCookieFederationFilter extends 
AbstractJWTFilter {
       LOGGER.configuredIdleTimeout(idleTimeoutSeconds, topologyName);
     }
 
+    /* Support to overwrite originalUrl by providing an option to pick it up 
from the request header value */
+    final String shouldUseOriginalUrlFromHeaderFilterParam = 
filterConfig.getInitParameter(SHOULD_USE_ORIGINAL_URL_FROM_HEADER);
+    if (shouldUseOriginalUrlFromHeaderFilterParam != null) {
+      shouldUseOriginalUrlFromHeader = 
Boolean.parseBoolean(shouldUseOriginalUrlFromHeaderFilterParam);
+    } else {
+      shouldUseOriginalUrlFromHeader = 
DEFAULT_SHOULD_USE_ORIGINAL_URL_FROM_HEADER;
+    }
+
+    /*
+    * If the feature to use update orignalurl for SSO to use headers is on 
populate
+    * required fields, else don't bother
+    */
+    if(shouldUseOriginalUrlFromHeader) {
+      originalUrlHeaderName = 
filterConfig.getInitParameter(X_ORIGINAL_URL_HEADER_NAME);
+      if (originalUrlHeaderName == null) {
+        originalUrlHeaderName = X_ORIGINAL_URL;
+      }
+
+      final String verifyOriginalUrlFromHeaderDomainFilterParam = 
filterConfig.getInitParameter(VERIFY_ORIGINAL_URL_FROM_HEADER_DOMAIN);
+      if (verifyOriginalUrlFromHeaderDomainFilterParam != null) {
+        verifyOriginalUrlFromHeaderDomain = 
Boolean.parseBoolean(verifyOriginalUrlFromHeaderDomainFilterParam);
+      } else {
+        verifyOriginalUrlFromHeaderDomain = 
DEFAULT_VERIFY_ORIGINAL_URL_FROM_HEADER_DOMAIN;
+      }
+
+      /* populate the whitelisted domains */
+      final String verifyOriginalUrlDomainWhitelistParam = 
filterConfig.getInitParameter(VERIFY_ORIGINAL_URL_FROM_HEADER_DOMAIN_WHITELIST);
+      if (verifyOriginalUrlFromHeaderDomain && 
verifyOriginalUrlDomainWhitelistParam != null) {
+        final String[] domains = 
verifyOriginalUrlDomainWhitelistParam.split(",");
+        for (final String domain : domains) {
+          final String trimmedDomain = domain.trim();
+          if (!trimmedDomain.isEmpty()) {
+            verifyOriginalUrlFromHeaderDomainWhitelist.add(trimmedDomain);
+          }
+        }
+      }
+    }
+
+
+
     configureExpectedParameters(filterConfig);
   }
 
@@ -265,9 +330,36 @@ public class SSOCookieFederationFilter extends 
AbstractJWTFilter {
     if (providerURL.contains("?")) {
       delimiter = "&";
     }
-    return providerURL + delimiter
-        + ORIGINAL_URL_QUERY_PARAM
-        + request.getRequestURL().append(getOriginalQueryString(request));
+
+    if(shouldUseOriginalUrlFromHeader && 
(request.getHeader(originalUrlHeaderName) != null) && 
!request.getHeader(originalUrlHeaderName).trim().isEmpty()) {
+      final String originalUrlFromHeader = 
request.getHeader(originalUrlHeaderName);
+      LOGGER.usingOriginalUrlFromHeader(originalUrlFromHeader);
+      /* verify if the original request domain and the domain in the header 
matches */
+      if(verifyOriginalUrlFromHeaderDomain) {
+        try {
+          final URL originalUrl = new URL(originalUrlFromHeader);
+          final String originalDomain = originalUrl.getHost();
+          if 
(!verifyOriginalUrlFromHeaderDomainWhitelist.contains(originalDomain)) {
+            LOGGER.invalidOriginalUrlDomain(originalDomain);
+            throw new IllegalArgumentException("Original URL domain '" + 
originalDomain +
+                                             "' is not in the allowed 
whitelist");
+          }
+        } catch (final MalformedURLException e) {
+          LOGGER.malformedOriginalUrlDomain(originalUrlFromHeader);
+          throw new IllegalArgumentException("Invalid original URL format: " + 
originalUrlFromHeader, e);
+        }
+      }
+
+      LOGGER.originalHeaderURLForwarding(originalUrlFromHeader, 
originalUrlHeaderName);
+      return providerURL + delimiter
+              + ORIGINAL_URL_QUERY_PARAM
+              + originalUrlFromHeader;
+    } else {
+      return providerURL + delimiter
+              + ORIGINAL_URL_QUERY_PARAM
+              + 
request.getRequestURL().append(getOriginalQueryString(request));
+    }
+
   }
 
   public String deriveDefaultAuthenticationProviderUrl(HttpServletRequest 
request) {
diff --git 
a/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/SSOCookieProviderTest.java
 
b/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/SSOCookieProviderTest.java
index f5f1a6e5d..90c056f2e 100644
--- 
a/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/SSOCookieProviderTest.java
+++ 
b/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/SSOCookieProviderTest.java
@@ -395,6 +395,339 @@ public class SSOCookieProviderTest extends 
AbstractJWTFilterTest {
     
Assert.assertTrue(errorResponse.endsWith(SSOCookieFederationFilter.IDLE_TIMEOUT_POSTFIX));
   }
 
+  /**
+   * Tests for the new original URL from header functionality
+   */
+
+  @Test
+  public void testOriginalUrlFromHeaderFeatureDisabledByDefault() throws 
Exception {
+    Properties props = getProperties();
+    handler.init(new TestFilterConfig(props));
+
+    HttpServletRequest request = 
EasyMock.createNiceMock(HttpServletRequest.class);
+    EasyMock.expect(request.getRequestURL()).andReturn(new 
StringBuffer(SERVICE_URL)).anyTimes();
+    EasyMock.expect(request.getQueryString()).andReturn(null);
+    
EasyMock.expect(request.getHeader("X-Original-URL")).andReturn("https://external.example.com/app";).anyTimes();
+    EasyMock.replay(request);
+
+    String loginURL = ((TestSSOCookieFederationProvider) 
handler).constructLoginURL(request);
+    Assert.assertNotNull("LoginURL should not be null.", loginURL);
+    // Should use the request URL, not the header value, since feature is 
disabled by default
+    Assert.assertEquals("https://localhost:8443/authserver?originalUrl="; + 
SERVICE_URL, loginURL);
+  }
+
+  @Test
+  public void testOriginalUrlFromHeaderFeatureExplicitlyDisabled() throws 
Exception {
+    Properties props = getProperties();
+    props.put(SSOCookieFederationFilter.SHOULD_USE_ORIGINAL_URL_FROM_HEADER, 
"false");
+    handler.init(new TestFilterConfig(props));
+
+    HttpServletRequest request = 
EasyMock.createNiceMock(HttpServletRequest.class);
+    EasyMock.expect(request.getRequestURL()).andReturn(new 
StringBuffer(SERVICE_URL)).anyTimes();
+    EasyMock.expect(request.getQueryString()).andReturn(null);
+    
EasyMock.expect(request.getHeader("X-Original-URL")).andReturn("https://external.example.com/app";).anyTimes();
+    EasyMock.replay(request);
+
+    String loginURL = ((TestSSOCookieFederationProvider) 
handler).constructLoginURL(request);
+    Assert.assertNotNull("LoginURL should not be null.", loginURL);
+    // Should use the request URL, not the header value
+    Assert.assertEquals("https://localhost:8443/authserver?originalUrl="; + 
SERVICE_URL, loginURL);
+  }
+
+  @Test
+  public void testOriginalUrlFromHeaderFeatureEnabledNoHeader() throws 
Exception {
+    Properties props = getProperties();
+    props.put(SSOCookieFederationFilter.SHOULD_USE_ORIGINAL_URL_FROM_HEADER, 
"true");
+    
props.put(SSOCookieFederationFilter.VERIFY_ORIGINAL_URL_FROM_HEADER_DOMAIN, 
"false");
+    handler.init(new TestFilterConfig(props));
+
+    HttpServletRequest request = 
EasyMock.createNiceMock(HttpServletRequest.class);
+    EasyMock.expect(request.getRequestURL()).andReturn(new 
StringBuffer(SERVICE_URL)).anyTimes();
+    EasyMock.expect(request.getQueryString()).andReturn(null);
+    
EasyMock.expect(request.getHeader("X-Original-URL")).andReturn(null).anyTimes();
+    EasyMock.replay(request);
+
+    String loginURL = ((TestSSOCookieFederationProvider) 
handler).constructLoginURL(request);
+    Assert.assertNotNull("LoginURL should not be null.", loginURL);
+    // Should use the request URL since no header is present
+    Assert.assertEquals("https://localhost:8443/authserver?originalUrl="; + 
SERVICE_URL, loginURL);
+  }
+
+  @Test
+  public void testOriginalUrlFromHeaderFeatureEnabledWithValidHeader() throws 
Exception {
+    Properties props = getProperties();
+    props.put(SSOCookieFederationFilter.SHOULD_USE_ORIGINAL_URL_FROM_HEADER, 
"true");
+    
props.put(SSOCookieFederationFilter.VERIFY_ORIGINAL_URL_FROM_HEADER_DOMAIN, 
"false"); // Disable domain verification
+    handler.init(new TestFilterConfig(props));
+
+    String originalUrl = "https://external.example.com/app";;
+    HttpServletRequest request = 
EasyMock.createNiceMock(HttpServletRequest.class);
+    EasyMock.expect(request.getRequestURL()).andReturn(new 
StringBuffer(SERVICE_URL)).anyTimes();
+    EasyMock.expect(request.getQueryString()).andReturn(null);
+    
EasyMock.expect(request.getHeader("X-Original-URL")).andReturn(originalUrl).anyTimes();
+    EasyMock.replay(request);
+
+    String loginURL = ((TestSSOCookieFederationProvider) 
handler).constructLoginURL(request);
+    Assert.assertNotNull("LoginURL should not be null.", loginURL);
+    // Should use the header value
+    Assert.assertEquals("https://localhost:8443/authserver?originalUrl="; + 
originalUrl, loginURL);
+  }
+
+  @Test
+  public void testOriginalUrlFromHeaderWithCustomHeaderName() throws Exception 
{
+    Properties props = getProperties();
+    props.put(SSOCookieFederationFilter.SHOULD_USE_ORIGINAL_URL_FROM_HEADER, 
"true");
+    props.put(SSOCookieFederationFilter.X_ORIGINAL_URL_HEADER_NAME, 
"X-Custom-Original-URL");
+    
props.put(SSOCookieFederationFilter.VERIFY_ORIGINAL_URL_FROM_HEADER_DOMAIN, 
"false"); // Disable domain verification
+    handler.init(new TestFilterConfig(props));
+
+    String originalUrl = "https://external.example.com/app";;
+    HttpServletRequest request = 
EasyMock.createNiceMock(HttpServletRequest.class);
+    EasyMock.expect(request.getRequestURL()).andReturn(new 
StringBuffer(SERVICE_URL)).anyTimes();
+    EasyMock.expect(request.getQueryString()).andReturn(null);
+    
EasyMock.expect(request.getHeader("X-Custom-Original-URL")).andReturn(originalUrl).anyTimes();
+    EasyMock.replay(request);
+
+    String loginURL = ((TestSSOCookieFederationProvider) 
handler).constructLoginURL(request);
+    Assert.assertNotNull("LoginURL should not be null.", loginURL);
+    // Should use the header value from the custom header name
+    Assert.assertEquals("https://localhost:8443/authserver?originalUrl="; + 
originalUrl, loginURL);
+  }
+
+  @Test
+  public void testOriginalUrlFromHeaderWithDomainVerificationEnabled() throws 
Exception {
+    Properties props = getProperties();
+    props.put(SSOCookieFederationFilter.SHOULD_USE_ORIGINAL_URL_FROM_HEADER, 
"true");
+    
props.put(SSOCookieFederationFilter.VERIFY_ORIGINAL_URL_FROM_HEADER_DOMAIN, 
"true");
+    
props.put(SSOCookieFederationFilter.VERIFY_ORIGINAL_URL_FROM_HEADER_DOMAIN_WHITELIST,
 "external.example.com, trusted.example.com");
+    handler.init(new TestFilterConfig(props));
+
+    String originalUrl = "https://external.example.com/app";;
+    HttpServletRequest request = 
EasyMock.createNiceMock(HttpServletRequest.class);
+    EasyMock.expect(request.getRequestURL()).andReturn(new 
StringBuffer(SERVICE_URL)).anyTimes();
+    EasyMock.expect(request.getQueryString()).andReturn(null);
+    
EasyMock.expect(request.getHeader("X-Original-URL")).andReturn(originalUrl).anyTimes();
+    EasyMock.replay(request);
+
+    String loginURL = ((TestSSOCookieFederationProvider) 
handler).constructLoginURL(request);
+    Assert.assertNotNull("LoginURL should not be null.", loginURL);
+    // Should use the header value since domain is in whitelist
+    Assert.assertEquals("https://localhost:8443/authserver?originalUrl="; + 
originalUrl, loginURL);
+  }
+
+  @Test
+  public void 
testOriginalUrlFromHeaderWithDomainVerificationEnabledMultipleDomainsInWhitelist()
 throws Exception {
+    Properties props = getProperties();
+    props.put(SSOCookieFederationFilter.SHOULD_USE_ORIGINAL_URL_FROM_HEADER, 
"true");
+    
props.put(SSOCookieFederationFilter.VERIFY_ORIGINAL_URL_FROM_HEADER_DOMAIN, 
"true");
+    
props.put(SSOCookieFederationFilter.VERIFY_ORIGINAL_URL_FROM_HEADER_DOMAIN_WHITELIST,
 "external.example.com, trusted.example.com, another.example.com");
+    handler.init(new TestFilterConfig(props));
+
+    String originalUrl = "https://trusted.example.com/different/app";;
+    HttpServletRequest request = 
EasyMock.createNiceMock(HttpServletRequest.class);
+    EasyMock.expect(request.getRequestURL()).andReturn(new 
StringBuffer(SERVICE_URL)).anyTimes();
+    EasyMock.expect(request.getQueryString()).andReturn(null);
+    
EasyMock.expect(request.getHeader("X-Original-URL")).andReturn(originalUrl).anyTimes();
+    EasyMock.replay(request);
+
+    String loginURL = ((TestSSOCookieFederationProvider) 
handler).constructLoginURL(request);
+    Assert.assertNotNull("LoginURL should not be null.", loginURL);
+    // Should use the header value since domain is in whitelist
+    Assert.assertEquals("https://localhost:8443/authserver?originalUrl="; + 
originalUrl, loginURL);
+  }
+
+  @Test
+  public void testOriginalUrlFromHeaderWithDomainVerificationInvalidDomain() 
throws Exception {
+    Properties props = getProperties();
+    props.put(SSOCookieFederationFilter.SHOULD_USE_ORIGINAL_URL_FROM_HEADER, 
"true");
+    
props.put(SSOCookieFederationFilter.VERIFY_ORIGINAL_URL_FROM_HEADER_DOMAIN, 
"true");
+    
props.put(SSOCookieFederationFilter.VERIFY_ORIGINAL_URL_FROM_HEADER_DOMAIN_WHITELIST,
 "external.example.com, trusted.example.com");
+    handler.init(new TestFilterConfig(props));
+
+    String originalUrl = "https://malicious.example.com/app";;
+    HttpServletRequest request = 
EasyMock.createNiceMock(HttpServletRequest.class);
+    EasyMock.expect(request.getRequestURL()).andReturn(new 
StringBuffer(SERVICE_URL)).anyTimes();
+    EasyMock.expect(request.getQueryString()).andReturn(null);
+    
EasyMock.expect(request.getHeader("X-Original-URL")).andReturn(originalUrl).anyTimes();
+    EasyMock.replay(request);
+
+    try {
+      ((TestSSOCookieFederationProvider) handler).constructLoginURL(request);
+      Assert.fail("Should have thrown IllegalArgumentException for domain not 
in whitelist");
+    } catch (IllegalArgumentException e) {
+      Assert.assertTrue("Exception should mention domain not in whitelist",
+                       e.getMessage().contains("not in the allowed 
whitelist"));
+    }
+  }
+
+  @Test
+  public void testOriginalUrlFromHeaderWithMalformedUrl() throws Exception {
+    Properties props = getProperties();
+    props.put(SSOCookieFederationFilter.SHOULD_USE_ORIGINAL_URL_FROM_HEADER, 
"true");
+    
props.put(SSOCookieFederationFilter.VERIFY_ORIGINAL_URL_FROM_HEADER_DOMAIN, 
"true");
+    
props.put(SSOCookieFederationFilter.VERIFY_ORIGINAL_URL_FROM_HEADER_DOMAIN_WHITELIST,
 "external.example.com");
+    handler.init(new TestFilterConfig(props));
+
+    String malformedUrl = "not-a-valid-url";
+    HttpServletRequest request = 
EasyMock.createNiceMock(HttpServletRequest.class);
+    EasyMock.expect(request.getRequestURL()).andReturn(new 
StringBuffer(SERVICE_URL)).anyTimes();
+    EasyMock.expect(request.getQueryString()).andReturn(null);
+    
EasyMock.expect(request.getHeader("X-Original-URL")).andReturn(malformedUrl).anyTimes();
+    EasyMock.replay(request);
+
+    try {
+      ((TestSSOCookieFederationProvider) handler).constructLoginURL(request);
+      Assert.fail("Should have thrown IllegalArgumentException for malformed 
URL");
+    } catch (IllegalArgumentException e) {
+      Assert.assertTrue("Exception should mention invalid URL format",
+                       e.getMessage().contains("Invalid original URL format"));
+    }
+  }
+
+  @Test
+  public void testOriginalUrlFromHeaderWithEmptyHeader() throws Exception {
+    Properties props = getProperties();
+    props.put(SSOCookieFederationFilter.SHOULD_USE_ORIGINAL_URL_FROM_HEADER, 
"true");
+    
props.put(SSOCookieFederationFilter.VERIFY_ORIGINAL_URL_FROM_HEADER_DOMAIN, 
"false");
+    handler.init(new TestFilterConfig(props));
+
+    HttpServletRequest request = 
EasyMock.createNiceMock(HttpServletRequest.class);
+    EasyMock.expect(request.getRequestURL()).andReturn(new 
StringBuffer(SERVICE_URL)).anyTimes();
+    EasyMock.expect(request.getQueryString()).andReturn(null);
+    
EasyMock.expect(request.getHeader("X-Original-URL")).andReturn("").anyTimes();
+    EasyMock.replay(request);
+
+    String loginURL = ((TestSSOCookieFederationProvider) 
handler).constructLoginURL(request);
+    Assert.assertNotNull("LoginURL should not be null.", loginURL);
+    // Should use the request URL since header is empty
+    Assert.assertEquals("https://localhost:8443/authserver?originalUrl="; + 
SERVICE_URL, loginURL);
+  }
+
+  @Test
+  public void testOriginalUrlFromHeaderWithWhitespaceOnlyHeader() throws 
Exception {
+    Properties props = getProperties();
+    props.put(SSOCookieFederationFilter.SHOULD_USE_ORIGINAL_URL_FROM_HEADER, 
"true");
+    
props.put(SSOCookieFederationFilter.VERIFY_ORIGINAL_URL_FROM_HEADER_DOMAIN, 
"false");
+    handler.init(new TestFilterConfig(props));
+
+    HttpServletRequest request = 
EasyMock.createNiceMock(HttpServletRequest.class);
+    EasyMock.expect(request.getRequestURL()).andReturn(new 
StringBuffer(SERVICE_URL)).anyTimes();
+    EasyMock.expect(request.getQueryString()).andReturn(null);
+    EasyMock.expect(request.getHeader("X-Original-URL")).andReturn("   
").anyTimes(); // Only whitespace
+    EasyMock.replay(request);
+
+    String loginURL = ((TestSSOCookieFederationProvider) 
handler).constructLoginURL(request);
+    Assert.assertNotNull("LoginURL should not be null.", loginURL);
+    // Should use the request URL since header contains only whitespace
+    Assert.assertEquals("https://localhost:8443/authserver?originalUrl="; + 
SERVICE_URL, loginURL);
+  }
+
+  @Test
+  public void testOriginalUrlFromHeaderWithWhitespaceInDomainWhitelist() 
throws Exception {
+    Properties props = getProperties();
+    props.put(SSOCookieFederationFilter.SHOULD_USE_ORIGINAL_URL_FROM_HEADER, 
"true");
+    
props.put(SSOCookieFederationFilter.VERIFY_ORIGINAL_URL_FROM_HEADER_DOMAIN, 
"true");
+    // Note: whitespace around domains to test trimming
+    
props.put(SSOCookieFederationFilter.VERIFY_ORIGINAL_URL_FROM_HEADER_DOMAIN_WHITELIST,
 " external.example.com , trusted.example.com , ");
+    handler.init(new TestFilterConfig(props));
+
+    String originalUrl = "https://external.example.com/app";;
+    HttpServletRequest request = 
EasyMock.createNiceMock(HttpServletRequest.class);
+    EasyMock.expect(request.getRequestURL()).andReturn(new 
StringBuffer(SERVICE_URL)).anyTimes();
+    EasyMock.expect(request.getQueryString()).andReturn(null);
+    
EasyMock.expect(request.getHeader("X-Original-URL")).andReturn(originalUrl).anyTimes();
+    EasyMock.replay(request);
+
+    String loginURL = ((TestSSOCookieFederationProvider) 
handler).constructLoginURL(request);
+    Assert.assertNotNull("LoginURL should not be null.", loginURL);
+    // Should use the header value since domain is in whitelist (after 
trimming)
+    Assert.assertEquals("https://localhost:8443/authserver?originalUrl="; + 
originalUrl, loginURL);
+  }
+
+  @Test
+  public void testOriginalUrlFromHeaderWithQueryStringInOriginalUrl() throws 
Exception {
+    Properties props = getProperties();
+    props.put(SSOCookieFederationFilter.SHOULD_USE_ORIGINAL_URL_FROM_HEADER, 
"true");
+    
props.put(SSOCookieFederationFilter.VERIFY_ORIGINAL_URL_FROM_HEADER_DOMAIN, 
"true");
+    
props.put(SSOCookieFederationFilter.VERIFY_ORIGINAL_URL_FROM_HEADER_DOMAIN_WHITELIST,
 "external.example.com");
+    handler.init(new TestFilterConfig(props));
+
+    String originalUrl = 
"https://external.example.com/app?param=value&other=test";;
+    HttpServletRequest request = 
EasyMock.createNiceMock(HttpServletRequest.class);
+    EasyMock.expect(request.getRequestURL()).andReturn(new 
StringBuffer(SERVICE_URL)).anyTimes();
+    EasyMock.expect(request.getQueryString()).andReturn(null);
+    
EasyMock.expect(request.getHeader("X-Original-URL")).andReturn(originalUrl).anyTimes();
+    EasyMock.replay(request);
+
+    String loginURL = ((TestSSOCookieFederationProvider) 
handler).constructLoginURL(request);
+    Assert.assertNotNull("LoginURL should not be null.", loginURL);
+    // Should use the header value including query string
+    Assert.assertEquals("https://localhost:8443/authserver?originalUrl="; + 
originalUrl, loginURL);
+  }
+
+  @Test
+  public void testOriginalUrlFromHeaderWithPortInDomain() throws Exception {
+    Properties props = getProperties();
+    props.put(SSOCookieFederationFilter.SHOULD_USE_ORIGINAL_URL_FROM_HEADER, 
"true");
+    
props.put(SSOCookieFederationFilter.VERIFY_ORIGINAL_URL_FROM_HEADER_DOMAIN, 
"true");
+    
props.put(SSOCookieFederationFilter.VERIFY_ORIGINAL_URL_FROM_HEADER_DOMAIN_WHITELIST,
 "external.example.com");
+    handler.init(new TestFilterConfig(props));
+
+    String originalUrl = "https://external.example.com:8080/app";;
+    HttpServletRequest request = 
EasyMock.createNiceMock(HttpServletRequest.class);
+    EasyMock.expect(request.getRequestURL()).andReturn(new 
StringBuffer(SERVICE_URL)).anyTimes();
+    EasyMock.expect(request.getQueryString()).andReturn(null);
+    
EasyMock.expect(request.getHeader("X-Original-URL")).andReturn(originalUrl).anyTimes();
+    EasyMock.replay(request);
+
+    String loginURL = ((TestSSOCookieFederationProvider) 
handler).constructLoginURL(request);
+    Assert.assertNotNull("LoginURL should not be null.", loginURL);
+    // Should use the header value - domain validation should work with port
+    Assert.assertEquals("https://localhost:8443/authserver?originalUrl="; + 
originalUrl, loginURL);
+  }
+
+  @Test
+  public void testOriginalUrlFromHeaderWithHttpProtocol() throws Exception {
+    Properties props = getProperties();
+    props.put(SSOCookieFederationFilter.SHOULD_USE_ORIGINAL_URL_FROM_HEADER, 
"true");
+    
props.put(SSOCookieFederationFilter.VERIFY_ORIGINAL_URL_FROM_HEADER_DOMAIN, 
"true");
+    
props.put(SSOCookieFederationFilter.VERIFY_ORIGINAL_URL_FROM_HEADER_DOMAIN_WHITELIST,
 "external.example.com");
+    handler.init(new TestFilterConfig(props));
+
+    String originalUrl = "http://external.example.com/app";;
+    HttpServletRequest request = 
EasyMock.createNiceMock(HttpServletRequest.class);
+    EasyMock.expect(request.getRequestURL()).andReturn(new 
StringBuffer(SERVICE_URL)).anyTimes();
+    EasyMock.expect(request.getQueryString()).andReturn(null);
+    
EasyMock.expect(request.getHeader("X-Original-URL")).andReturn(originalUrl).anyTimes();
+    EasyMock.replay(request);
+
+    String loginURL = ((TestSSOCookieFederationProvider) 
handler).constructLoginURL(request);
+    Assert.assertNotNull("LoginURL should not be null.", loginURL);
+    // Should use the header value
+    Assert.assertEquals("https://localhost:8443/authserver?originalUrl="; + 
originalUrl, loginURL);
+  }
+
+  @Test
+  public void testOriginalUrlFromHeaderDomainVerificationEnabledByDefault() 
throws Exception {
+    Properties props = getProperties();
+    props.put(SSOCookieFederationFilter.SHOULD_USE_ORIGINAL_URL_FROM_HEADER, 
"true");
+    // Don't explicitly set domain verification - should default to true
+    
props.put(SSOCookieFederationFilter.VERIFY_ORIGINAL_URL_FROM_HEADER_DOMAIN_WHITELIST,
 "external.example.com");
+    handler.init(new TestFilterConfig(props));
+
+    String originalUrl = "https://external.example.com/app";;
+    HttpServletRequest request = 
EasyMock.createNiceMock(HttpServletRequest.class);
+    EasyMock.expect(request.getRequestURL()).andReturn(new 
StringBuffer(SERVICE_URL)).anyTimes();
+    EasyMock.expect(request.getQueryString()).andReturn(null);
+    
EasyMock.expect(request.getHeader("X-Original-URL")).andReturn(originalUrl).anyTimes();
+    EasyMock.replay(request);
+
+    String loginURL = ((TestSSOCookieFederationProvider) 
handler).constructLoginURL(request);
+    Assert.assertNotNull("LoginURL should not be null.", loginURL);
+    // Should use the header value since domain is in whitelist and 
verification is enabled by default
+    Assert.assertEquals("https://localhost:8443/authserver?originalUrl="; + 
originalUrl, loginURL);
+  }
+
   @Override
   protected String getVerificationPemProperty() {
     return SSOCookieFederationFilter.SSO_VERIFICATION_PEM;

Reply via email to