Browse Source

Polish gh-14193

Issue gh-14193
Marcus Hert Da Coregio 1 year ago
parent
commit
69808bfda3

+ 25 - 13
cas/src/main/java/org/springframework/security/cas/web/CasAuthenticationFilter.java

@@ -53,6 +53,7 @@ import org.springframework.security.web.savedrequest.SavedRequest;
 import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
 import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
 import org.springframework.security.web.util.matcher.RequestMatcher;
 import org.springframework.security.web.util.matcher.RequestMatcher;
 import org.springframework.util.Assert;
 import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
 
 
 /**
 /**
  * Processes a CAS service ticket, obtains proxy granting tickets, and processes proxy
  * Processes a CAS service ticket, obtains proxy granting tickets, and processes proxy
@@ -247,25 +248,24 @@ public class CasAuthenticationFilter extends AbstractAuthenticationProcessingFil
 			return null;
 			return null;
 		}
 		}
 		String serviceTicket = obtainArtifact(request);
 		String serviceTicket = obtainArtifact(request);
-		if (serviceTicket == null) {
-			boolean gateway = false;
+		if (!StringUtils.hasText(serviceTicket)) {
 			HttpSession session = request.getSession(false);
 			HttpSession session = request.getSession(false);
-			if (session != null) {
-				gateway = session.getAttribute(TriggerCasGatewayFilter.TRIGGER_CAS_GATEWAY_AUTHENTICATION) != null;
-				session.removeAttribute(TriggerCasGatewayFilter.TRIGGER_CAS_GATEWAY_AUTHENTICATION);
-			}
-			if (gateway) {
+			if (session != null && session
+				.getAttribute(CasGatewayAuthenticationRedirectFilter.CAS_GATEWAY_AUTHENTICATION_ATTR) != null) {
 				this.logger.debug("Failed authentication response from CAS gateway request");
 				this.logger.debug("Failed authentication response from CAS gateway request");
+				session.removeAttribute(CasGatewayAuthenticationRedirectFilter.CAS_GATEWAY_AUTHENTICATION_ATTR);
 				SavedRequest savedRequest = this.requestCache.getRequest(request, response);
 				SavedRequest savedRequest = this.requestCache.getRequest(request, response);
 				if (savedRequest != null) {
 				if (savedRequest != null) {
-					this.redirectStrategy.sendRedirect(request, response, savedRequest.getRedirectUrl());
+					String redirectUrl = savedRequest.getRedirectUrl();
+					this.logger.debug(LogMessage.format("Redirecting to: %s", redirectUrl));
+					this.requestCache.removeRequest(request, response);
+					this.redirectStrategy.sendRedirect(request, response, redirectUrl);
+					return null;
 				}
 				}
-				return null;
-			}
-			else {
-				this.logger.debug("Failed to obtain an artifact (cas ticket)");
-				serviceTicket = "";
 			}
 			}
+
+			this.logger.debug("Failed to obtain an artifact (cas ticket)");
+			serviceTicket = "";
 		}
 		}
 		boolean serviceTicketRequest = serviceTicketRequest(request, response);
 		boolean serviceTicketRequest = serviceTicketRequest(request, response);
 		CasServiceTicketAuthenticationToken authRequest = serviceTicketRequest
 		CasServiceTicketAuthenticationToken authRequest = serviceTicketRequest
@@ -329,11 +329,23 @@ public class CasAuthenticationFilter extends AbstractAuthenticationProcessingFil
 		this.authenticateAllArtifacts = serviceProperties.isAuthenticateAllArtifacts();
 		this.authenticateAllArtifacts = serviceProperties.isAuthenticateAllArtifacts();
 	}
 	}
 
 
+	/**
+	 * Set the {@link RedirectStrategy} used to redirect to the saved request if there is
+	 * one saved. Defaults to {@link DefaultRedirectStrategy}.
+	 * @param redirectStrategy the redirect strategy to use
+	 * @since 6.3
+	 */
 	public final void setRedirectStrategy(RedirectStrategy redirectStrategy) {
 	public final void setRedirectStrategy(RedirectStrategy redirectStrategy) {
 		Assert.notNull(redirectStrategy, "redirectStrategy cannot be null");
 		Assert.notNull(redirectStrategy, "redirectStrategy cannot be null");
 		this.redirectStrategy = redirectStrategy;
 		this.redirectStrategy = redirectStrategy;
 	}
 	}
 
 
+	/**
+	 * The {@link RequestCache} used to retrieve the saved request in failed gateway
+	 * authentication scenarios.
+	 * @param requestCache the request cache to use
+	 * @since 6.3
+	 */
 	public final void setRequestCache(RequestCache requestCache) {
 	public final void setRequestCache(RequestCache requestCache) {
 		Assert.notNull(requestCache, "requestCache cannot be null");
 		Assert.notNull(requestCache, "requestCache cannot be null");
 		this.requestCache = requestCache;
 		this.requestCache = requestCache;

+ 0 - 144
cas/src/main/java/org/springframework/security/cas/web/CasCookieGatewayRequestMatcher.java

@@ -1,144 +0,0 @@
-/*
- * Copyright 2002-2023 the original author or authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.springframework.security.cas.web;
-
-import jakarta.servlet.http.Cookie;
-import jakarta.servlet.http.HttpServletRequest;
-import org.apereo.cas.client.authentication.DefaultGatewayResolverImpl;
-import org.apereo.cas.client.authentication.GatewayResolver;
-
-import org.springframework.security.cas.ServiceProperties;
-import org.springframework.security.cas.authentication.CasAuthenticationToken;
-import org.springframework.security.core.Authentication;
-import org.springframework.security.core.context.SecurityContextHolder;
-import org.springframework.security.web.util.matcher.RequestMatcher;
-import org.springframework.util.Assert;
-import org.springframework.util.StringUtils;
-
-/**
- * Default RequestMatcher implementation for the {@link TriggerCasGatewayFilter}.
- *
- * This RequestMatcher returns <code>true</code> if:
- * <ul>
- * <li>User is not already authenticated (see {@link #isAuthenticated})</li>
- * <li>The request was not previously gatewayed</li>
- * <li>The request matches additional criteria (see
- * {@link #performGatewayAuthentication})</li>
- * </ul>
- *
- * Implementors can override this class to customize the authentication check and the
- * gateway criteria.
- * <p>
- * The request is marked as "gatewayed" using the configured {@link GatewayResolver} to
- * avoid infinite loop.
- *
- * @author Michael Remond
- *
- */
-public class CasCookieGatewayRequestMatcher implements RequestMatcher {
-
-	private ServiceProperties serviceProperties;
-
-	private String cookieName;
-
-	private GatewayResolver gatewayStorage = new DefaultGatewayResolverImpl();
-
-	public CasCookieGatewayRequestMatcher(ServiceProperties serviceProperties, final String cookieName) {
-		Assert.notNull(serviceProperties, "serviceProperties cannot be null");
-		this.serviceProperties = serviceProperties;
-		this.cookieName = cookieName;
-	}
-
-	public final boolean matches(HttpServletRequest request) {
-
-		// Test if we are already authenticated
-		if (isAuthenticated(request)) {
-			return false;
-		}
-
-		// Test if the request was already gatewayed to avoid infinite loop
-		final boolean wasGatewayed = this.gatewayStorage.hasGatewayedAlready(request,
-				this.serviceProperties.getService());
-
-		if (wasGatewayed) {
-			return false;
-		}
-
-		// If request matches gateway criteria, we mark the request as gatewayed and
-		// return true to trigger a CAS
-		// gateway authentication
-		if (performGatewayAuthentication(request)) {
-			this.gatewayStorage.storeGatewayInformation(request, this.serviceProperties.getService());
-			return true;
-		}
-		else {
-			return false;
-		}
-	}
-
-	/**
-	 * Test if the user is authenticated in Spring Security. Default implementation test
-	 * if the user is CAS authenticated.
-	 * @param request
-	 * @return true if the user is authenticated
-	 */
-	protected boolean isAuthenticated(HttpServletRequest request) {
-		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
-		return authentication instanceof CasAuthenticationToken;
-	}
-
-	/**
-	 * Method that determines if the current request triggers a CAS gateway
-	 * authentication. This implementation returns <code>true</code> only if a
-	 * {@link Cookie} with the configured name is present at the request
-	 * @param request
-	 * @return true if the request must trigger a CAS gateway authentication
-	 */
-	protected boolean performGatewayAuthentication(HttpServletRequest request) {
-		if (!StringUtils.hasText(this.cookieName)) {
-			return true;
-		}
-
-		Cookie[] cookies = request.getCookies();
-		if (cookies == null || cookies.length == 0) {
-			return false;
-		}
-
-		for (Cookie cookie : cookies) {
-			// Check the cookie name. If it matches the configured cookie name, return
-			// true
-			if (this.cookieName.equalsIgnoreCase(cookie.getName())) {
-				return true;
-			}
-		}
-		return false;
-	}
-
-	public void setGatewayStorage(GatewayResolver gatewayStorage) {
-		Assert.notNull(gatewayStorage, "gatewayStorage cannot be null");
-		this.gatewayStorage = gatewayStorage;
-	}
-
-	public String getCookieName() {
-		return this.cookieName;
-	}
-
-	public void setCookieName(String cookieName) {
-		this.cookieName = cookieName;
-	}
-
-}

+ 126 - 0
cas/src/main/java/org/springframework/security/cas/web/CasGatewayAuthenticationRedirectFilter.java

@@ -0,0 +1,126 @@
+/*
+ * Copyright 2002-2023 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.cas.web;
+
+import java.io.IOException;
+
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.ServletRequest;
+import jakarta.servlet.ServletResponse;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
+import org.apereo.cas.client.util.CommonUtils;
+import org.apereo.cas.client.util.WebUtils;
+
+import org.springframework.security.cas.ServiceProperties;
+import org.springframework.security.web.DefaultRedirectStrategy;
+import org.springframework.security.web.RedirectStrategy;
+import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
+import org.springframework.security.web.savedrequest.RequestCache;
+import org.springframework.security.web.util.matcher.RequestMatcher;
+import org.springframework.util.Assert;
+import org.springframework.web.filter.GenericFilterBean;
+
+/**
+ * Redirects the request to the CAS server appending {@code gateway=true} to the URL. Upon
+ * redirection, the {@link ServiceProperties#isSendRenew()} is ignored and considered as
+ * {@code false} to align with the specification says that the {@code sendRenew} parameter
+ * is not compatible with the {@code gateway} parameter. See the <a href=
+ * "https://apereo.github.io/cas/6.6.x/protocol/CAS-Protocol-V2-Specification.html#:~:text=This%20parameter%20is%20not%20compatible%20with%20the%20%E2%80%9Crenew%E2%80%9D%20parameter.%20Behavior%20is%20undefined%20if%20both%20are%20set.">CAS
+ * Protocol Specification</a> for more details. To allow other filters to know if the
+ * request is a gateway request, this filter creates a session and add an attribute with
+ * name {@link #CAS_GATEWAY_AUTHENTICATION_ATTR} which can be checked by other filters if
+ * needed. It is recommended that this filter is placed after
+ * {@link CasAuthenticationFilter} if it is defined.
+ *
+ * @author Michael Remond
+ * @author Jerome LELEU
+ * @author Marcus da Coregio
+ * @since 6.3
+ */
+public final class CasGatewayAuthenticationRedirectFilter extends GenericFilterBean {
+
+	public static final String CAS_GATEWAY_AUTHENTICATION_ATTR = "CAS_GATEWAY_AUTHENTICATION";
+
+	private final String casLoginUrl;
+
+	private final ServiceProperties serviceProperties;
+
+	private RequestMatcher requestMatcher;
+
+	private RequestCache requestCache = new HttpSessionRequestCache();
+
+	private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
+
+	/**
+	 * Constructs a new instance of this class
+	 * @param serviceProperties the {@link ServiceProperties}
+	 */
+	public CasGatewayAuthenticationRedirectFilter(String casLoginUrl, ServiceProperties serviceProperties) {
+		Assert.hasText(casLoginUrl, "casLoginUrl cannot be null or empty");
+		Assert.notNull(serviceProperties, "serviceProperties cannot be null");
+		this.casLoginUrl = casLoginUrl;
+		this.serviceProperties = serviceProperties;
+		this.requestMatcher = new CasGatewayResolverRequestMatcher(this.serviceProperties);
+	}
+
+	@Override
+	public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
+			throws IOException, ServletException {
+
+		HttpServletRequest request = (HttpServletRequest) req;
+		HttpServletResponse response = (HttpServletResponse) res;
+
+		if (!this.requestMatcher.matches(request)) {
+			chain.doFilter(request, response);
+			return;
+		}
+
+		this.requestCache.saveRequest(request, response);
+		HttpSession session = request.getSession(true);
+		session.setAttribute(CAS_GATEWAY_AUTHENTICATION_ATTR, true);
+		String urlEncodedService = WebUtils.constructServiceUrl(request, response, this.serviceProperties.getService(),
+				null, this.serviceProperties.getServiceParameter(), this.serviceProperties.getArtifactParameter(),
+				true);
+		String redirectUrl = CommonUtils.constructRedirectUrl(this.casLoginUrl,
+				this.serviceProperties.getServiceParameter(), urlEncodedService, false, true);
+		this.redirectStrategy.sendRedirect(request, response, redirectUrl);
+	}
+
+	/**
+	 * Sets the {@link RequestMatcher} used to trigger this filter. Defaults to
+	 * {@link CasGatewayResolverRequestMatcher}.
+	 * @param requestMatcher the {@link RequestMatcher} to use
+	 */
+	public void setRequestMatcher(RequestMatcher requestMatcher) {
+		Assert.notNull(requestMatcher, "requestMatcher cannot be null");
+		this.requestMatcher = requestMatcher;
+	}
+
+	/**
+	 * Sets the {@link RequestCache} used to store the current request to be replayed
+	 * after redirect from the CAS server. Defaults to {@link HttpSessionRequestCache}.
+	 * @param requestCache the {@link RequestCache} to use
+	 */
+	public void setRequestCache(RequestCache requestCache) {
+		Assert.notNull(requestCache, "requestCache cannot be null");
+		this.requestCache = requestCache;
+	}
+
+}

+ 67 - 0
cas/src/main/java/org/springframework/security/cas/web/CasGatewayResolverRequestMatcher.java

@@ -0,0 +1,67 @@
+/*
+ * Copyright 2002-2023 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.cas.web;
+
+import jakarta.servlet.http.HttpServletRequest;
+import org.apereo.cas.client.authentication.DefaultGatewayResolverImpl;
+import org.apereo.cas.client.authentication.GatewayResolver;
+
+import org.springframework.security.cas.ServiceProperties;
+import org.springframework.security.web.util.matcher.RequestMatcher;
+import org.springframework.util.Assert;
+
+/**
+ * A {@link RequestMatcher} implementation that delegates the check to an instance of
+ * {@link GatewayResolver}. The request is marked as "gatewayed" using the configured
+ * {@link GatewayResolver} to avoid infinite loop.
+ *
+ * @author Michael Remond
+ * @author Marcus da Coregio
+ * @since 6.3
+ */
+public final class CasGatewayResolverRequestMatcher implements RequestMatcher {
+
+	private final ServiceProperties serviceProperties;
+
+	private GatewayResolver gatewayStorage = new DefaultGatewayResolverImpl();
+
+	public CasGatewayResolverRequestMatcher(ServiceProperties serviceProperties) {
+		Assert.notNull(serviceProperties, "serviceProperties cannot be null");
+		this.serviceProperties = serviceProperties;
+	}
+
+	@Override
+	public boolean matches(HttpServletRequest request) {
+		boolean wasGatewayed = this.gatewayStorage.hasGatewayedAlready(request, this.serviceProperties.getService());
+		if (!wasGatewayed) {
+			this.gatewayStorage.storeGatewayInformation(request, this.serviceProperties.getService());
+			return true;
+		}
+		return false;
+	}
+
+	/**
+	 * Sets the {@link GatewayResolver} to check if the request was already gatewayed.
+	 * Defaults to {@link DefaultGatewayResolverImpl}
+	 * @param gatewayStorage the {@link GatewayResolver} to use. Cannot be null.
+	 */
+	public void setGatewayStorage(GatewayResolver gatewayStorage) {
+		Assert.notNull(gatewayStorage, "gatewayStorage cannot be null");
+		this.gatewayStorage = gatewayStorage;
+	}
+
+}

+ 0 - 122
cas/src/main/java/org/springframework/security/cas/web/TriggerCasGatewayFilter.java

@@ -1,122 +0,0 @@
-/*
- * Copyright 2002-2023 the original author or authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.springframework.security.cas.web;
-
-import java.io.IOException;
-
-import jakarta.servlet.FilterChain;
-import jakarta.servlet.ServletException;
-import jakarta.servlet.ServletRequest;
-import jakarta.servlet.ServletResponse;
-import jakarta.servlet.http.HttpServletRequest;
-import jakarta.servlet.http.HttpServletResponse;
-import jakarta.servlet.http.HttpSession;
-import org.apereo.cas.client.util.CommonUtils;
-import org.apereo.cas.client.util.WebUtils;
-
-import org.springframework.security.cas.ServiceProperties;
-import org.springframework.security.web.DefaultRedirectStrategy;
-import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
-import org.springframework.security.web.savedrequest.RequestCache;
-import org.springframework.security.web.util.matcher.RequestMatcher;
-import org.springframework.util.Assert;
-import org.springframework.web.filter.GenericFilterBean;
-
-/**
- * Triggers a CAS gateway authentication attempt.
- * <p>
- * This filter requires a web session to work.
- * <p>
- * This filter must be placed after the {@link CasAuthenticationFilter} if it is defined.
- * <p>
- * The default implementation is {@link CasCookieGatewayRequestMatcher}.
- *
- * @author Michael Remond
- * @author Jerome LELEU
- */
-public class TriggerCasGatewayFilter extends GenericFilterBean {
-
-	public static final String TRIGGER_CAS_GATEWAY_AUTHENTICATION = "triggerCasGatewayAuthentication";
-
-	private final String loginUrl;
-
-	private final ServiceProperties serviceProperties;
-
-	private RequestMatcher requestMatcher;
-
-	private RequestCache requestCache = new HttpSessionRequestCache();
-
-	public TriggerCasGatewayFilter(String loginUrl, ServiceProperties serviceProperties) {
-		this.loginUrl = loginUrl;
-		this.serviceProperties = serviceProperties;
-		this.requestMatcher = new CasCookieGatewayRequestMatcher(this.serviceProperties, null);
-	}
-
-	public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
-			throws IOException, ServletException {
-
-		HttpServletRequest request = (HttpServletRequest) req;
-		HttpServletResponse response = (HttpServletResponse) res;
-
-		if (this.requestMatcher.matches(request)) {
-			// Try a CAS gateway authentication
-			this.requestCache.saveRequest(request, response);
-			HttpSession session = request.getSession(false);
-			if (session != null) {
-				session.setAttribute(TRIGGER_CAS_GATEWAY_AUTHENTICATION, true);
-			}
-			String urlEncodedService = WebUtils.constructServiceUrl(null, response, this.serviceProperties.getService(),
-					null, this.serviceProperties.getArtifactParameter(), true);
-			String redirectUrl = CommonUtils.constructRedirectUrl(this.loginUrl,
-					this.serviceProperties.getServiceParameter(), urlEncodedService,
-					this.serviceProperties.isSendRenew(), true);
-			new DefaultRedirectStrategy().sendRedirect(request, response, redirectUrl);
-		}
-		else {
-			// Continue in the chain
-			chain.doFilter(request, response);
-		}
-
-	}
-
-	public String getLoginUrl() {
-		return this.loginUrl;
-	}
-
-	public ServiceProperties getServiceProperties() {
-		return this.serviceProperties;
-	}
-
-	public RequestMatcher getRequestMatcher() {
-		return this.requestMatcher;
-	}
-
-	public RequestCache getRequestCache() {
-		return this.requestCache;
-	}
-
-	public void setRequestMatcher(RequestMatcher requestMatcher) {
-		Assert.notNull(requestMatcher, "requestMatcher cannot be null");
-		this.requestMatcher = requestMatcher;
-	}
-
-	public final void setRequestCache(RequestCache requestCache) {
-		Assert.notNull(requestCache, "requestCache cannot be null");
-		this.requestCache = requestCache;
-	}
-
-}

+ 7 - 2
cas/src/test/java/org/springframework/security/cas/web/CasAuthenticationFilterTests.java

@@ -17,6 +17,7 @@
 package org.springframework.security.cas.web;
 package org.springframework.security.cas.web;
 
 
 import jakarta.servlet.FilterChain;
 import jakarta.servlet.FilterChain;
+import jakarta.servlet.http.HttpSession;
 import org.apereo.cas.client.proxy.ProxyGrantingTicketStorage;
 import org.apereo.cas.client.proxy.ProxyGrantingTicketStorage;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.Test;
@@ -221,11 +222,13 @@ public class CasAuthenticationFilterTests {
 	}
 	}
 
 
 	@Test
 	@Test
-	public void testNullServiceButGateway() throws Exception {
+	public void attemptAuthenticationWhenNoServiceTicketAndIsGatewayRequestThenRedirectToSavedRequestAndClearAttribute()
+			throws Exception {
 		CasAuthenticationFilter filter = new CasAuthenticationFilter();
 		CasAuthenticationFilter filter = new CasAuthenticationFilter();
 		MockHttpServletRequest request = new MockHttpServletRequest();
 		MockHttpServletRequest request = new MockHttpServletRequest();
 		MockHttpServletResponse response = new MockHttpServletResponse();
 		MockHttpServletResponse response = new MockHttpServletResponse();
-		request.getSession(true).setAttribute(TriggerCasGatewayFilter.TRIGGER_CAS_GATEWAY_AUTHENTICATION, true);
+		HttpSession session = request.getSession(true);
+		session.setAttribute(CasGatewayAuthenticationRedirectFilter.CAS_GATEWAY_AUTHENTICATION_ATTR, true);
 
 
 		new HttpSessionRequestCache().saveRequest(request, response);
 		new HttpSessionRequestCache().saveRequest(request, response);
 
 
@@ -233,6 +236,8 @@ public class CasAuthenticationFilterTests {
 		assertThat(authn).isNull();
 		assertThat(authn).isNull();
 		assertThat(response.getStatus()).isEqualTo(302);
 		assertThat(response.getStatus()).isEqualTo(302);
 		assertThat(response.getRedirectedUrl()).isEqualTo("http://localhost?continue");
 		assertThat(response.getRedirectedUrl()).isEqualTo("http://localhost?continue");
+		assertThat(session.getAttribute(CasGatewayAuthenticationRedirectFilter.CAS_GATEWAY_AUTHENTICATION_ATTR))
+			.isNull();
 	}
 	}
 
 
 }
 }

+ 0 - 117
cas/src/test/java/org/springframework/security/cas/web/CasCookieGatewayRequestMatcherTests.java

@@ -1,117 +0,0 @@
-/*
- * Copyright 2002-2023 the original author or authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.springframework.security.cas.web;
-
-import java.io.IOException;
-
-import jakarta.servlet.ServletException;
-import jakarta.servlet.http.Cookie;
-import jakarta.servlet.http.HttpServletRequest;
-import org.apereo.cas.client.authentication.DefaultGatewayResolverImpl;
-import org.junit.jupiter.api.Test;
-
-import org.springframework.mock.web.MockHttpServletRequest;
-import org.springframework.security.cas.ServiceProperties;
-import org.springframework.security.cas.authentication.CasAuthenticationToken;
-import org.springframework.security.core.context.SecurityContextHolder;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.fail;
-import static org.mockito.Mockito.mock;
-
-/**
- * Tests {@link CasCookieGatewayRequestMatche}.
- *
- * @author Michael Remond
- */
-public class CasCookieGatewayRequestMatcherTests {
-
-	@Test
-	public void testNullServiceProperties() throws Exception {
-		try {
-			new CasCookieGatewayRequestMatcher(null, null);
-			fail("Should have thrown IllegalArgumentException");
-		}
-		catch (IllegalArgumentException expected) {
-			assertThat(expected.getMessage()).isEqualTo("serviceProperties cannot be null");
-		}
-	}
-
-	@Test
-	public void testNormalOperationWithNoSSOSession() throws IOException, ServletException {
-		SecurityContextHolder.getContext().setAuthentication(null);
-		ServiceProperties serviceProperties = new ServiceProperties();
-		serviceProperties.setService("http://localhost/j_spring_cas_security_check");
-		CasCookieGatewayRequestMatcher rm = new CasCookieGatewayRequestMatcher(serviceProperties, null);
-		MockHttpServletRequest request = new MockHttpServletRequest("GET", "/some_path");
-
-		// First request
-		assertThat(rm.matches(request)).isTrue();
-		assertThat(request.getSession(false).getAttribute(DefaultGatewayResolverImpl.CONST_CAS_GATEWAY)).isNotNull();
-		// Second request
-		assertThat(rm.matches(request)).isFalse();
-		assertThat(request.getSession(false).getAttribute(DefaultGatewayResolverImpl.CONST_CAS_GATEWAY)).isNotNull();
-	}
-
-	@Test
-	public void testGatewayWhenCasAuthenticated() throws IOException, ServletException {
-		SecurityContextHolder.getContext().setAuthentication(null);
-		ServiceProperties serviceProperties = new ServiceProperties();
-		serviceProperties.setService("http://localhost/j_spring_cas_security_check");
-		CasCookieGatewayRequestMatcher rm = new CasCookieGatewayRequestMatcher(serviceProperties,
-				"CAS_TGT_COOKIE_TEST_NAME");
-		MockHttpServletRequest request = new MockHttpServletRequest("GET", "/some_path");
-		request.setCookies(new Cookie("CAS_TGT_COOKIE_TEST_NAME", "casTGCookieValue"));
-
-		assertThat(rm.matches(request)).isTrue();
-
-		MockHttpServletRequest requestWithoutCasCookie = new MockHttpServletRequest("GET", "/some_path");
-		requestWithoutCasCookie.setCookies(new Cookie("WRONG_CAS_TGT_COOKIE_TEST_NAME", "casTGCookieValue"));
-
-		assertThat(rm.matches(requestWithoutCasCookie)).isFalse();
-	}
-
-	@Test
-	public void testGatewayWhenAlreadySessionCreated() throws IOException, ServletException {
-		SecurityContextHolder.getContext().setAuthentication(mock(CasAuthenticationToken.class));
-
-		ServiceProperties serviceProperties = new ServiceProperties();
-		serviceProperties.setService("http://localhost/j_spring_cas_security_check");
-		CasCookieGatewayRequestMatcher rm = new CasCookieGatewayRequestMatcher(serviceProperties,
-				"CAS_TGT_COOKIE_TEST_NAME");
-		MockHttpServletRequest request = new MockHttpServletRequest("GET", "/some_path");
-		assertThat(rm.matches(request)).isFalse();
-	}
-
-	@Test
-	public void testGatewayWithNoMatchingRequest() throws IOException, ServletException {
-		SecurityContextHolder.getContext().setAuthentication(null);
-		ServiceProperties serviceProperties = new ServiceProperties();
-		serviceProperties.setService("http://localhost/j_spring_cas_security_check");
-		CasCookieGatewayRequestMatcher rm = new CasCookieGatewayRequestMatcher(serviceProperties,
-				"CAS_TGT_COOKIE_TEST_NAME") {
-			@Override
-			protected boolean performGatewayAuthentication(HttpServletRequest request) {
-				return false;
-			}
-		};
-		MockHttpServletRequest request = new MockHttpServletRequest("GET", "/some_path");
-
-		assertThat(rm.matches(request)).isFalse();
-	}
-
-}

+ 95 - 0
cas/src/test/java/org/springframework/security/cas/web/CasGatewayAuthenticationRedirectFilterTests.java

@@ -0,0 +1,95 @@
+/*
+ * Copyright 2002-2023 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.cas.web;
+
+import java.io.IOException;
+
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import org.junit.jupiter.api.Test;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.mock.web.MockFilterChain;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpServletResponse;
+import org.springframework.security.cas.ServiceProperties;
+import org.springframework.security.web.savedrequest.RequestCache;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+
+/**
+ * Tests for {@link CasGatewayAuthenticationRedirectFilter}.
+ *
+ * @author Jerome LELEU
+ * @author Marcus da Coregio
+ */
+public class CasGatewayAuthenticationRedirectFilterTests {
+
+	private static final String CAS_LOGIN_URL = "http://mycasserver/login";
+
+	CasGatewayAuthenticationRedirectFilter filter = new CasGatewayAuthenticationRedirectFilter(CAS_LOGIN_URL,
+			serviceProperties());
+
+	@Test
+	void doFilterWhenMatchesThenSavesRequestAndSavesAttributeAndSendRedirect() throws IOException, ServletException {
+		RequestCache requestCache = mock();
+		MockHttpServletRequest request = new MockHttpServletRequest();
+		MockHttpServletResponse response = new MockHttpServletResponse();
+		this.filter.setRequestMatcher((req) -> true);
+		this.filter.setRequestCache(requestCache);
+		this.filter.doFilter(request, response, new MockFilterChain());
+		assertThat(response.getStatus()).isEqualTo(HttpStatus.FOUND.value());
+		assertThat(response.getHeader("Location"))
+			.isEqualTo("http://mycasserver/login?service=http%3A%2F%2Flocalhost%2Flogin%2Fcas&gateway=true");
+		verify(requestCache).saveRequest(request, response);
+	}
+
+	@Test
+	void doFilterWhenNotMatchThenContinueFilter() throws ServletException, IOException {
+		this.filter.setRequestMatcher((req) -> false);
+		FilterChain chain = mock();
+		MockHttpServletResponse response = mock();
+		this.filter.doFilter(new MockHttpServletRequest(), response, chain);
+		verify(chain).doFilter(any(), any());
+		verifyNoInteractions(response);
+	}
+
+	@Test
+	void doFilterWhenSendRenewTrueThenIgnores() throws ServletException, IOException {
+		ServiceProperties serviceProperties = serviceProperties();
+		serviceProperties.setSendRenew(true);
+		this.filter = new CasGatewayAuthenticationRedirectFilter(CAS_LOGIN_URL, serviceProperties);
+		MockHttpServletRequest request = new MockHttpServletRequest();
+		MockHttpServletResponse response = new MockHttpServletResponse();
+		this.filter.setRequestMatcher((req) -> true);
+		this.filter.doFilter(request, response, new MockFilterChain());
+		assertThat(response.getStatus()).isEqualTo(HttpStatus.FOUND.value());
+		assertThat(response.getHeader("Location"))
+			.isEqualTo("http://mycasserver/login?service=http%3A%2F%2Flocalhost%2Flogin%2Fcas&gateway=true");
+	}
+
+	private static ServiceProperties serviceProperties() {
+		ServiceProperties serviceProperties = new ServiceProperties();
+		serviceProperties.setService("http://localhost/login/cas");
+		return serviceProperties;
+	}
+
+}

+ 74 - 0
cas/src/test/java/org/springframework/security/cas/web/CasGatewayResolverRequestMatcherTests.java

@@ -0,0 +1,74 @@
+/*
+ * Copyright 2002-2023 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.security.cas.web;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.security.cas.ServiceProperties;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+
+/**
+ * Tests {@link CasGatewayResolverRequestMatcher}.
+ *
+ * @author Marcus da Coregio
+ */
+class CasGatewayResolverRequestMatcherTests {
+
+	CasGatewayResolverRequestMatcher matcher = new CasGatewayResolverRequestMatcher(new ServiceProperties());
+
+	@Test
+	void constructorWhenServicePropertiesNullThenException() {
+		assertThatIllegalArgumentException().isThrownBy(() -> new CasGatewayResolverRequestMatcher(null))
+			.withMessage("serviceProperties cannot be null");
+	}
+
+	@Test
+	void matchesWhenAlreadyGatewayedThenReturnsFalse() {
+		MockHttpServletRequest request = new MockHttpServletRequest();
+		request.getSession().setAttribute("_const_cas_gateway_", "yes");
+		boolean matches = this.matcher.matches(request);
+		assertThat(matches).isFalse();
+	}
+
+	@Test
+	void matchesWhenNotGatewayedThenReturnsTrue() {
+		MockHttpServletRequest request = new MockHttpServletRequest();
+		boolean matches = this.matcher.matches(request);
+		assertThat(matches).isTrue();
+	}
+
+	@Test
+	void matchesWhenNoSessionThenReturnsTrue() {
+		MockHttpServletRequest request = new MockHttpServletRequest();
+		request.setSession(null);
+		boolean matches = this.matcher.matches(request);
+		assertThat(matches).isTrue();
+	}
+
+	@Test
+	void matchesWhenNotGatewayedAndCheckedAgainThenSavesAsGatewayedAndReturnsFalse() {
+		MockHttpServletRequest request = new MockHttpServletRequest();
+		boolean matches = this.matcher.matches(request);
+		boolean secondMatch = this.matcher.matches(request);
+		assertThat(matches).isTrue();
+		assertThat(secondMatch).isFalse();
+	}
+
+}

+ 0 - 95
cas/src/test/java/org/springframework/security/cas/web/TriggerCasGatewayFilterTests.java

@@ -1,95 +0,0 @@
-/*
- * Copyright 2002-2023 the original author or authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.springframework.security.cas.web;
-
-import java.io.IOException;
-
-import jakarta.servlet.FilterChain;
-import jakarta.servlet.ServletException;
-import org.junit.jupiter.api.AfterEach;
-import org.junit.jupiter.api.Test;
-
-import org.springframework.mock.web.MockHttpServletRequest;
-import org.springframework.mock.web.MockHttpServletResponse;
-import org.springframework.security.cas.ServiceProperties;
-import org.springframework.security.core.context.SecurityContextHolder;
-import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
-import org.springframework.security.web.savedrequest.RequestCache;
-import org.springframework.security.web.util.matcher.RequestMatcher;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-
-/**
- * Tests {@link TriggerCasGatewayFilter}.
- *
- * @author Jerome LELEU
- */
-public class TriggerCasGatewayFilterTests {
-
-	private static final String CAS_LOGIN_URL = "http://mycasserver/login";
-
-	@AfterEach
-	public void tearDown() {
-		SecurityContextHolder.clearContext();
-	}
-
-	@Test
-	public void testGettersSetters() {
-		ServiceProperties sp = new ServiceProperties();
-		TriggerCasGatewayFilter filter = new TriggerCasGatewayFilter(CAS_LOGIN_URL, sp);
-		assertThat(filter.getLoginUrl()).isEqualTo(CAS_LOGIN_URL);
-		assertThat(filter.getServiceProperties()).isEqualTo(sp);
-		assertThat(filter.getRequestMatcher().getClass()).isEqualTo(CasCookieGatewayRequestMatcher.class);
-		assertThat(filter.getRequestCache().getClass()).isEqualTo(HttpSessionRequestCache.class);
-		RequestMatcher requestMatcher = mock(RequestMatcher.class);
-		filter.setRequestMatcher(requestMatcher);
-		assertThat(filter.getRequestMatcher()).isEqualTo(requestMatcher);
-		assertThatExceptionOfType(RuntimeException.class).isThrownBy(() -> filter.setRequestMatcher(null));
-		RequestCache requestCache = mock(RequestCache.class);
-		filter.setRequestCache(requestCache);
-		assertThat(filter.getRequestCache()).isEqualTo(requestCache);
-		assertThatExceptionOfType(RuntimeException.class).isThrownBy(() -> filter.setRequestCache(null));
-	}
-
-	@Test
-	public void testOperation() throws IOException, ServletException {
-		ServiceProperties sp = new ServiceProperties();
-		sp.setService("http://myservice");
-		TriggerCasGatewayFilter filter = new TriggerCasGatewayFilter(CAS_LOGIN_URL, sp);
-		MockHttpServletRequest request = new MockHttpServletRequest();
-		MockHttpServletResponse response = new MockHttpServletResponse();
-		FilterChain chain = mock(FilterChain.class);
-
-		filter.doFilter(request, response, chain);
-		assertThat(filter.getRequestCache().getRequest(request, response)).isNotNull();
-		assertThat(request.getSession(false).getAttribute(TriggerCasGatewayFilter.TRIGGER_CAS_GATEWAY_AUTHENTICATION))
-			.isEqualTo(true);
-		assertThat(response.getStatus()).isEqualTo(302);
-		assertThat(response.getRedirectedUrl())
-			.isEqualTo(CAS_LOGIN_URL + "?service=http%3A%2F%2Fmyservice&gateway=true");
-		verify(chain, never()).doFilter(request, response);
-
-		filter.doFilter(request, response, chain);
-		verify(chain, times(1)).doFilter(request, response);
-	}
-
-}

+ 4 - 0
docs/modules/ROOT/pages/whats-new.adoc

@@ -7,3 +7,7 @@ Below are the highlights of the release.
 == Configuration
 == Configuration
 
 
 - https://github.com/spring-projects/spring-security/issues/6192[gh-6192] - xref:reactive/authentication/concurrent-sessions-control.adoc[docs] Add Concurrent Sessions Control on WebFlux
 - https://github.com/spring-projects/spring-security/issues/6192[gh-6192] - xref:reactive/authentication/concurrent-sessions-control.adoc[docs] Add Concurrent Sessions Control on WebFlux
+
+== CAS
+
+- https://github.com/spring-projects/spring-security/pull/14193[gh-14193] - Added support for CAS Gateway Authentication