From 69808bfda384f1e122c6def3d094b3c153e36bd2 Mon Sep 17 00:00:00 2001 From: Marcus Hert Da Coregio Date: Wed, 20 Dec 2023 10:54:16 -0300 Subject: [PATCH] Polish gh-14193 Issue gh-14193 --- .../cas/web/CasAuthenticationFilter.java | 38 +++-- .../web/CasCookieGatewayRequestMatcher.java | 144 ------------------ ...asGatewayAuthenticationRedirectFilter.java | 126 +++++++++++++++ .../web/CasGatewayResolverRequestMatcher.java | 67 ++++++++ .../cas/web/TriggerCasGatewayFilter.java | 122 --------------- .../cas/web/CasAuthenticationFilterTests.java | 9 +- .../CasCookieGatewayRequestMatcherTests.java | 117 -------------- ...ewayAuthenticationRedirectFilterTests.java | 95 ++++++++++++ ...CasGatewayResolverRequestMatcherTests.java | 74 +++++++++ .../cas/web/TriggerCasGatewayFilterTests.java | 95 ------------ docs/modules/ROOT/pages/whats-new.adoc | 4 + 11 files changed, 398 insertions(+), 493 deletions(-) delete mode 100644 cas/src/main/java/org/springframework/security/cas/web/CasCookieGatewayRequestMatcher.java create mode 100644 cas/src/main/java/org/springframework/security/cas/web/CasGatewayAuthenticationRedirectFilter.java create mode 100644 cas/src/main/java/org/springframework/security/cas/web/CasGatewayResolverRequestMatcher.java delete mode 100644 cas/src/main/java/org/springframework/security/cas/web/TriggerCasGatewayFilter.java delete mode 100644 cas/src/test/java/org/springframework/security/cas/web/CasCookieGatewayRequestMatcherTests.java create mode 100644 cas/src/test/java/org/springframework/security/cas/web/CasGatewayAuthenticationRedirectFilterTests.java create mode 100644 cas/src/test/java/org/springframework/security/cas/web/CasGatewayResolverRequestMatcherTests.java delete mode 100644 cas/src/test/java/org/springframework/security/cas/web/TriggerCasGatewayFilterTests.java 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: - * - * - * Implementors can override this class to customize the authentication check and the - * gateway criteria. - *

- * 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