From f516fbc39a22c949c218c2d428c576b39d9e6bb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20LELEU?= Date: Mon, 11 Dec 2023 10:03:34 +0100 Subject: [PATCH] Added support for the CAS gateway feature --- .../cas/web/CasAuthenticationFilter.java | 40 ++++- .../web/CasCookieGatewayRequestMatcher.java | 144 ++++++++++++++++++ .../cas/web/TriggerCasGatewayFilter.java | 122 +++++++++++++++ .../cas/web/CasAuthenticationFilterTests.java | 16 ++ .../CasCookieGatewayRequestMatcherTests.java | 117 ++++++++++++++ .../cas/web/TriggerCasGatewayFilterTests.java | 95 ++++++++++++ 6 files changed, 532 insertions(+), 2 deletions(-) create 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/TriggerCasGatewayFilter.java create 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/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 94c783e3a2..a8500bf05b 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 @@ -22,6 +22,7 @@ import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; import org.apereo.cas.client.proxy.ProxyGrantingTicketStorage; import org.apereo.cas.client.util.WebUtils; import org.apereo.cas.client.validation.TicketValidator; @@ -39,11 +40,16 @@ import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolderStrategy; +import org.springframework.security.web.DefaultRedirectStrategy; +import org.springframework.security.web.RedirectStrategy; import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; import org.springframework.security.web.context.HttpSessionSecurityContextRepository; import org.springframework.security.web.context.SecurityContextRepository; +import org.springframework.security.web.savedrequest.HttpSessionRequestCache; +import org.springframework.security.web.savedrequest.RequestCache; +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; @@ -199,6 +205,10 @@ public class CasAuthenticationFilter extends AbstractAuthenticationProcessingFil private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder .getContextHolderStrategy(); + private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy(); + + private RequestCache requestCache = new HttpSessionRequestCache(); + public CasAuthenticationFilter() { super("/login/cas"); setAuthenticationFailureHandler(new SimpleUrlAuthenticationFailureHandler()); @@ -238,8 +248,24 @@ public class CasAuthenticationFilter extends AbstractAuthenticationProcessingFil } String serviceTicket = obtainArtifact(request); if (serviceTicket == null) { - this.logger.debug("Failed to obtain an artifact (cas ticket)"); - serviceTicket = ""; + boolean gateway = 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) { + this.logger.debug("Failed authentication response from CAS gateway request"); + SavedRequest savedRequest = this.requestCache.getRequest(request, response); + if (savedRequest != null) { + this.redirectStrategy.sendRedirect(request, response, savedRequest.getRedirectUrl()); + } + return null; + } + else { + this.logger.debug("Failed to obtain an artifact (cas ticket)"); + serviceTicket = ""; + } } boolean serviceTicketRequest = serviceTicketRequest(request, response); CasServiceTicketAuthenticationToken authRequest = serviceTicketRequest @@ -303,6 +329,16 @@ public class CasAuthenticationFilter extends AbstractAuthenticationProcessingFil this.authenticateAllArtifacts = serviceProperties.isAuthenticateAllArtifacts(); } + public final void setRedirectStrategy(RedirectStrategy redirectStrategy) { + Assert.notNull(redirectStrategy, "redirectStrategy cannot be null"); + this.redirectStrategy = redirectStrategy; + } + + public final void setRequestCache(RequestCache requestCache) { + Assert.notNull(requestCache, "requestCache cannot be null"); + this.requestCache = requestCache; + } + /** * Indicates if the request is elgible to process a service ticket. This method exists * for readability. 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 new file mode 100644 index 0000000000..973503efc0 --- /dev/null +++ b/cas/src/main/java/org/springframework/security/cas/web/CasCookieGatewayRequestMatcher.java @@ -0,0 +1,144 @@ +/* + * 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/TriggerCasGatewayFilter.java b/cas/src/main/java/org/springframework/security/cas/web/TriggerCasGatewayFilter.java new file mode 100644 index 0000000000..4a29ce0087 --- /dev/null +++ b/cas/src/main/java/org/springframework/security/cas/web/TriggerCasGatewayFilter.java @@ -0,0 +1,122 @@ +/* + * 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 46689b42e9..a2e0a3e2b7 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 @@ -36,6 +36,7 @@ import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.security.web.context.SecurityContextRepository; +import org.springframework.security.web.savedrequest.HttpSessionRequestCache; import org.springframework.test.util.ReflectionTestUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -219,4 +220,19 @@ public class CasAuthenticationFilterTests { verify(securityContextRepository).saveContext(any(SecurityContext.class), eq(request), eq(response)); } + @Test + public void testNullServiceButGateway() throws Exception { + CasAuthenticationFilter filter = new CasAuthenticationFilter(); + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + request.getSession(true).setAttribute(TriggerCasGatewayFilter.TRIGGER_CAS_GATEWAY_AUTHENTICATION, true); + + new HttpSessionRequestCache().saveRequest(request, response); + + Authentication authn = filter.attemptAuthentication(request, response); + assertThat(authn).isNull(); + assertThat(response.getStatus()).isEqualTo(302); + assertThat(response.getRedirectedUrl()).isEqualTo("http://localhost?continue"); + } + } 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 new file mode 100644 index 0000000000..522303aea2 --- /dev/null +++ b/cas/src/test/java/org/springframework/security/cas/web/CasCookieGatewayRequestMatcherTests.java @@ -0,0 +1,117 @@ +/* + * 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/TriggerCasGatewayFilterTests.java b/cas/src/test/java/org/springframework/security/cas/web/TriggerCasGatewayFilterTests.java new file mode 100644 index 0000000000..3b19771390 --- /dev/null +++ b/cas/src/test/java/org/springframework/security/cas/web/TriggerCasGatewayFilterTests.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.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); + } + +}