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;