6 changed files with 532 additions and 2 deletions
@ -0,0 +1,144 @@
@@ -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; |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,122 @@
@@ -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; |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,117 @@
@@ -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(); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,95 @@
@@ -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); |
||||
} |
||||
|
||||
} |
||||
Loading…
Reference in new issue