Просмотр исходного кода

Added support for the CAS gateway feature

Jérôme LELEU 1 год назад
Родитель
Сommit
f516fbc39a

+ 38 - 2
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.

+ 144 - 0
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 <code>true</code> if:
+ * <ul>
+ * <li>User is not already authenticated (see {@link #isAuthenticated})</li>
+ * <li>The request was not previously gatewayed</li>
+ * <li>The request matches additional criteria (see
+ * {@link #performGatewayAuthentication})</li>
+ * </ul>
+ *
+ * Implementors can override this class to customize the authentication check and the
+ * gateway criteria.
+ * <p>
+ * The request is marked as "gatewayed" using the configured {@link GatewayResolver} to
+ * avoid infinite loop.
+ *
+ * @author Michael Remond
+ *
+ */
+public class CasCookieGatewayRequestMatcher implements RequestMatcher {
+
+	private ServiceProperties serviceProperties;
+
+	private String cookieName;
+
+	private GatewayResolver gatewayStorage = new DefaultGatewayResolverImpl();
+
+	public CasCookieGatewayRequestMatcher(ServiceProperties serviceProperties, final String cookieName) {
+		Assert.notNull(serviceProperties, "serviceProperties cannot be null");
+		this.serviceProperties = serviceProperties;
+		this.cookieName = cookieName;
+	}
+
+	public final boolean matches(HttpServletRequest request) {
+
+		// Test if we are already authenticated
+		if (isAuthenticated(request)) {
+			return false;
+		}
+
+		// Test if the request was already gatewayed to avoid infinite loop
+		final boolean wasGatewayed = this.gatewayStorage.hasGatewayedAlready(request,
+				this.serviceProperties.getService());
+
+		if (wasGatewayed) {
+			return false;
+		}
+
+		// If request matches gateway criteria, we mark the request as gatewayed and
+		// return true to trigger a CAS
+		// gateway authentication
+		if (performGatewayAuthentication(request)) {
+			this.gatewayStorage.storeGatewayInformation(request, this.serviceProperties.getService());
+			return true;
+		}
+		else {
+			return false;
+		}
+	}
+
+	/**
+	 * Test if the user is authenticated in Spring Security. Default implementation test
+	 * if the user is CAS authenticated.
+	 * @param request
+	 * @return true if the user is authenticated
+	 */
+	protected boolean isAuthenticated(HttpServletRequest request) {
+		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+		return authentication instanceof CasAuthenticationToken;
+	}
+
+	/**
+	 * Method that determines if the current request triggers a CAS gateway
+	 * authentication. This implementation returns <code>true</code> only if a
+	 * {@link Cookie} with the configured name is present at the request
+	 * @param request
+	 * @return true if the request must trigger a CAS gateway authentication
+	 */
+	protected boolean performGatewayAuthentication(HttpServletRequest request) {
+		if (!StringUtils.hasText(this.cookieName)) {
+			return true;
+		}
+
+		Cookie[] cookies = request.getCookies();
+		if (cookies == null || cookies.length == 0) {
+			return false;
+		}
+
+		for (Cookie cookie : cookies) {
+			// Check the cookie name. If it matches the configured cookie name, return
+			// true
+			if (this.cookieName.equalsIgnoreCase(cookie.getName())) {
+				return true;
+			}
+		}
+		return false;
+	}
+
+	public void setGatewayStorage(GatewayResolver gatewayStorage) {
+		Assert.notNull(gatewayStorage, "gatewayStorage cannot be null");
+		this.gatewayStorage = gatewayStorage;
+	}
+
+	public String getCookieName() {
+		return this.cookieName;
+	}
+
+	public void setCookieName(String cookieName) {
+		this.cookieName = cookieName;
+	}
+
+}

+ 122 - 0
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.
+ * <p>
+ * This filter requires a web session to work.
+ * <p>
+ * This filter must be placed after the {@link CasAuthenticationFilter} if it is defined.
+ * <p>
+ * The default implementation is {@link CasCookieGatewayRequestMatcher}.
+ *
+ * @author Michael Remond
+ * @author Jerome LELEU
+ */
+public class TriggerCasGatewayFilter extends GenericFilterBean {
+
+	public static final String TRIGGER_CAS_GATEWAY_AUTHENTICATION = "triggerCasGatewayAuthentication";
+
+	private final String loginUrl;
+
+	private final ServiceProperties serviceProperties;
+
+	private RequestMatcher requestMatcher;
+
+	private RequestCache requestCache = new HttpSessionRequestCache();
+
+	public TriggerCasGatewayFilter(String loginUrl, ServiceProperties serviceProperties) {
+		this.loginUrl = loginUrl;
+		this.serviceProperties = serviceProperties;
+		this.requestMatcher = new CasCookieGatewayRequestMatcher(this.serviceProperties, null);
+	}
+
+	public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
+			throws IOException, ServletException {
+
+		HttpServletRequest request = (HttpServletRequest) req;
+		HttpServletResponse response = (HttpServletResponse) res;
+
+		if (this.requestMatcher.matches(request)) {
+			// Try a CAS gateway authentication
+			this.requestCache.saveRequest(request, response);
+			HttpSession session = request.getSession(false);
+			if (session != null) {
+				session.setAttribute(TRIGGER_CAS_GATEWAY_AUTHENTICATION, true);
+			}
+			String urlEncodedService = WebUtils.constructServiceUrl(null, response, this.serviceProperties.getService(),
+					null, this.serviceProperties.getArtifactParameter(), true);
+			String redirectUrl = CommonUtils.constructRedirectUrl(this.loginUrl,
+					this.serviceProperties.getServiceParameter(), urlEncodedService,
+					this.serviceProperties.isSendRenew(), true);
+			new DefaultRedirectStrategy().sendRedirect(request, response, redirectUrl);
+		}
+		else {
+			// Continue in the chain
+			chain.doFilter(request, response);
+		}
+
+	}
+
+	public String getLoginUrl() {
+		return this.loginUrl;
+	}
+
+	public ServiceProperties getServiceProperties() {
+		return this.serviceProperties;
+	}
+
+	public RequestMatcher getRequestMatcher() {
+		return this.requestMatcher;
+	}
+
+	public RequestCache getRequestCache() {
+		return this.requestCache;
+	}
+
+	public void setRequestMatcher(RequestMatcher requestMatcher) {
+		Assert.notNull(requestMatcher, "requestMatcher cannot be null");
+		this.requestMatcher = requestMatcher;
+	}
+
+	public final void setRequestCache(RequestCache requestCache) {
+		Assert.notNull(requestCache, "requestCache cannot be null");
+		this.requestCache = requestCache;
+	}
+
+}

+ 16 - 0
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");
+	}
+
 }

+ 117 - 0
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();
+	}
+
+}

+ 95 - 0
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);
+	}
+
+}