diff --git a/cas/src/main/java/org/springframework/security/cas/web/CasAuthenticationFilter.java b/cas/src/main/java/org/springframework/security/cas/web/CasAuthenticationFilter.java
index a8500bf05b..4727f1dd6e 100644
--- a/cas/src/main/java/org/springframework/security/cas/web/CasAuthenticationFilter.java
+++ b/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.RequestMatcher;
import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
/**
* Processes a CAS service ticket, obtains proxy granting tickets, and processes proxy
@@ -247,25 +248,24 @@ public class CasAuthenticationFilter extends AbstractAuthenticationProcessingFil
return null;
}
String serviceTicket = obtainArtifact(request);
- if (serviceTicket == null) {
- boolean gateway = false;
+ if (!StringUtils.hasText(serviceTicket)) {
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");
+ session.removeAttribute(CasGatewayAuthenticationRedirectFilter.CAS_GATEWAY_AUTHENTICATION_ATTR);
SavedRequest savedRequest = this.requestCache.getRequest(request, response);
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);
CasServiceTicketAuthenticationToken authRequest = serviceTicketRequest
@@ -329,11 +329,23 @@ public class CasAuthenticationFilter extends AbstractAuthenticationProcessingFil
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) {
Assert.notNull(redirectStrategy, "redirectStrategy cannot be null");
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) {
Assert.notNull(requestCache, "requestCache cannot be null");
this.requestCache = requestCache;
diff --git a/cas/src/main/java/org/springframework/security/cas/web/CasCookieGatewayRequestMatcher.java b/cas/src/main/java/org/springframework/security/cas/web/CasCookieGatewayRequestMatcher.java
deleted file mode 100644
index 973503efc0..0000000000
--- a/cas/src/main/java/org/springframework/security/cas/web/CasCookieGatewayRequestMatcher.java
+++ /dev/null
@@ -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 true if:
- *
- * 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 true 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;
- }
-
-}
diff --git a/cas/src/main/java/org/springframework/security/cas/web/CasGatewayAuthenticationRedirectFilter.java b/cas/src/main/java/org/springframework/security/cas/web/CasGatewayAuthenticationRedirectFilter.java
new file mode 100644
index 0000000000..7dbcbd6b2a
--- /dev/null
+++ b/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 CAS
+ * Protocol Specification 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;
+ }
+
+}
diff --git a/cas/src/main/java/org/springframework/security/cas/web/CasGatewayResolverRequestMatcher.java b/cas/src/main/java/org/springframework/security/cas/web/CasGatewayResolverRequestMatcher.java
new file mode 100644
index 0000000000..9332ebc136
--- /dev/null
+++ b/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;
+ }
+
+}
diff --git a/cas/src/main/java/org/springframework/security/cas/web/TriggerCasGatewayFilter.java b/cas/src/main/java/org/springframework/security/cas/web/TriggerCasGatewayFilter.java
deleted file mode 100644
index 4a29ce0087..0000000000
--- a/cas/src/main/java/org/springframework/security/cas/web/TriggerCasGatewayFilter.java
+++ /dev/null
@@ -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.
- *
- * This filter requires a web session to work. - *
- * This filter must be placed after the {@link CasAuthenticationFilter} if it is defined. - *
- * 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; - } - -} diff --git a/cas/src/test/java/org/springframework/security/cas/web/CasAuthenticationFilterTests.java b/cas/src/test/java/org/springframework/security/cas/web/CasAuthenticationFilterTests.java index a2e0a3e2b7..50bc5cdffe 100644 --- a/cas/src/test/java/org/springframework/security/cas/web/CasAuthenticationFilterTests.java +++ b/cas/src/test/java/org/springframework/security/cas/web/CasAuthenticationFilterTests.java @@ -17,6 +17,7 @@ package org.springframework.security.cas.web; import jakarta.servlet.FilterChain; +import jakarta.servlet.http.HttpSession; import org.apereo.cas.client.proxy.ProxyGrantingTicketStorage; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; @@ -221,11 +222,13 @@ public class CasAuthenticationFilterTests { } @Test - public void testNullServiceButGateway() throws Exception { + public void attemptAuthenticationWhenNoServiceTicketAndIsGatewayRequestThenRedirectToSavedRequestAndClearAttribute() + throws Exception { CasAuthenticationFilter filter = new CasAuthenticationFilter(); MockHttpServletRequest request = new MockHttpServletRequest(); 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); @@ -233,6 +236,8 @@ public class CasAuthenticationFilterTests { assertThat(authn).isNull(); assertThat(response.getStatus()).isEqualTo(302); assertThat(response.getRedirectedUrl()).isEqualTo("http://localhost?continue"); + assertThat(session.getAttribute(CasGatewayAuthenticationRedirectFilter.CAS_GATEWAY_AUTHENTICATION_ATTR)) + .isNull(); } } diff --git a/cas/src/test/java/org/springframework/security/cas/web/CasCookieGatewayRequestMatcherTests.java b/cas/src/test/java/org/springframework/security/cas/web/CasCookieGatewayRequestMatcherTests.java deleted file mode 100644 index 522303aea2..0000000000 --- a/cas/src/test/java/org/springframework/security/cas/web/CasCookieGatewayRequestMatcherTests.java +++ /dev/null @@ -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(); - } - -} diff --git a/cas/src/test/java/org/springframework/security/cas/web/CasGatewayAuthenticationRedirectFilterTests.java b/cas/src/test/java/org/springframework/security/cas/web/CasGatewayAuthenticationRedirectFilterTests.java new file mode 100644 index 0000000000..fd7af4a871 --- /dev/null +++ b/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; + } + +} diff --git a/cas/src/test/java/org/springframework/security/cas/web/CasGatewayResolverRequestMatcherTests.java b/cas/src/test/java/org/springframework/security/cas/web/CasGatewayResolverRequestMatcherTests.java new file mode 100644 index 0000000000..97a590c068 --- /dev/null +++ b/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(); + } + +} diff --git a/cas/src/test/java/org/springframework/security/cas/web/TriggerCasGatewayFilterTests.java b/cas/src/test/java/org/springframework/security/cas/web/TriggerCasGatewayFilterTests.java deleted file mode 100644 index 3b19771390..0000000000 --- a/cas/src/test/java/org/springframework/security/cas/web/TriggerCasGatewayFilterTests.java +++ /dev/null @@ -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); - } - -} diff --git a/docs/modules/ROOT/pages/whats-new.adoc b/docs/modules/ROOT/pages/whats-new.adoc index f163563774..9fa19e003d 100644 --- a/docs/modules/ROOT/pages/whats-new.adoc +++ b/docs/modules/ROOT/pages/whats-new.adoc @@ -7,3 +7,7 @@ Below are the highlights of the release. == 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 + +== CAS + +- https://github.com/spring-projects/spring-security/pull/14193[gh-14193] - Added support for CAS Gateway Authentication