Skip to content

Commit f516fbc

Browse files
leleujmarcusdacoregio
authored andcommitted
Added support for the CAS gateway feature
1 parent ec02c22 commit f516fbc

File tree

6 files changed

+532
-2
lines changed

6 files changed

+532
-2
lines changed

cas/src/main/java/org/springframework/security/cas/web/CasAuthenticationFilter.java

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import jakarta.servlet.ServletException;
2323
import jakarta.servlet.http.HttpServletRequest;
2424
import jakarta.servlet.http.HttpServletResponse;
25+
import jakarta.servlet.http.HttpSession;
2526
import org.apereo.cas.client.proxy.ProxyGrantingTicketStorage;
2627
import org.apereo.cas.client.util.WebUtils;
2728
import org.apereo.cas.client.validation.TicketValidator;
@@ -39,11 +40,16 @@
3940
import org.springframework.security.core.context.SecurityContext;
4041
import org.springframework.security.core.context.SecurityContextHolder;
4142
import org.springframework.security.core.context.SecurityContextHolderStrategy;
43+
import org.springframework.security.web.DefaultRedirectStrategy;
44+
import org.springframework.security.web.RedirectStrategy;
4245
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
4346
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
4447
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
4548
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
4649
import org.springframework.security.web.context.SecurityContextRepository;
50+
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
51+
import org.springframework.security.web.savedrequest.RequestCache;
52+
import org.springframework.security.web.savedrequest.SavedRequest;
4753
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
4854
import org.springframework.security.web.util.matcher.RequestMatcher;
4955
import org.springframework.util.Assert;
@@ -199,6 +205,10 @@ public class CasAuthenticationFilter extends AbstractAuthenticationProcessingFil
199205
private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder
200206
.getContextHolderStrategy();
201207

208+
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
209+
210+
private RequestCache requestCache = new HttpSessionRequestCache();
211+
202212
public CasAuthenticationFilter() {
203213
super("/login/cas");
204214
setAuthenticationFailureHandler(new SimpleUrlAuthenticationFailureHandler());
@@ -238,8 +248,24 @@ public Authentication attemptAuthentication(HttpServletRequest request, HttpServ
238248
}
239249
String serviceTicket = obtainArtifact(request);
240250
if (serviceTicket == null) {
241-
this.logger.debug("Failed to obtain an artifact (cas ticket)");
242-
serviceTicket = "";
251+
boolean gateway = false;
252+
HttpSession session = request.getSession(false);
253+
if (session != null) {
254+
gateway = session.getAttribute(TriggerCasGatewayFilter.TRIGGER_CAS_GATEWAY_AUTHENTICATION) != null;
255+
session.removeAttribute(TriggerCasGatewayFilter.TRIGGER_CAS_GATEWAY_AUTHENTICATION);
256+
}
257+
if (gateway) {
258+
this.logger.debug("Failed authentication response from CAS gateway request");
259+
SavedRequest savedRequest = this.requestCache.getRequest(request, response);
260+
if (savedRequest != null) {
261+
this.redirectStrategy.sendRedirect(request, response, savedRequest.getRedirectUrl());
262+
}
263+
return null;
264+
}
265+
else {
266+
this.logger.debug("Failed to obtain an artifact (cas ticket)");
267+
serviceTicket = "";
268+
}
243269
}
244270
boolean serviceTicketRequest = serviceTicketRequest(request, response);
245271
CasServiceTicketAuthenticationToken authRequest = serviceTicketRequest
@@ -303,6 +329,16 @@ public final void setServiceProperties(final ServiceProperties serviceProperties
303329
this.authenticateAllArtifacts = serviceProperties.isAuthenticateAllArtifacts();
304330
}
305331

332+
public final void setRedirectStrategy(RedirectStrategy redirectStrategy) {
333+
Assert.notNull(redirectStrategy, "redirectStrategy cannot be null");
334+
this.redirectStrategy = redirectStrategy;
335+
}
336+
337+
public final void setRequestCache(RequestCache requestCache) {
338+
Assert.notNull(requestCache, "requestCache cannot be null");
339+
this.requestCache = requestCache;
340+
}
341+
306342
/**
307343
* Indicates if the request is elgible to process a service ticket. This method exists
308344
* for readability.
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
/*
2+
* Copyright 2002-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.cas.web;
18+
19+
import jakarta.servlet.http.Cookie;
20+
import jakarta.servlet.http.HttpServletRequest;
21+
import org.apereo.cas.client.authentication.DefaultGatewayResolverImpl;
22+
import org.apereo.cas.client.authentication.GatewayResolver;
23+
24+
import org.springframework.security.cas.ServiceProperties;
25+
import org.springframework.security.cas.authentication.CasAuthenticationToken;
26+
import org.springframework.security.core.Authentication;
27+
import org.springframework.security.core.context.SecurityContextHolder;
28+
import org.springframework.security.web.util.matcher.RequestMatcher;
29+
import org.springframework.util.Assert;
30+
import org.springframework.util.StringUtils;
31+
32+
/**
33+
* Default RequestMatcher implementation for the {@link TriggerCasGatewayFilter}.
34+
*
35+
* This RequestMatcher returns <code>true</code> if:
36+
* <ul>
37+
* <li>User is not already authenticated (see {@link #isAuthenticated})</li>
38+
* <li>The request was not previously gatewayed</li>
39+
* <li>The request matches additional criteria (see
40+
* {@link #performGatewayAuthentication})</li>
41+
* </ul>
42+
*
43+
* Implementors can override this class to customize the authentication check and the
44+
* gateway criteria.
45+
* <p>
46+
* The request is marked as "gatewayed" using the configured {@link GatewayResolver} to
47+
* avoid infinite loop.
48+
*
49+
* @author Michael Remond
50+
*
51+
*/
52+
public class CasCookieGatewayRequestMatcher implements RequestMatcher {
53+
54+
private ServiceProperties serviceProperties;
55+
56+
private String cookieName;
57+
58+
private GatewayResolver gatewayStorage = new DefaultGatewayResolverImpl();
59+
60+
public CasCookieGatewayRequestMatcher(ServiceProperties serviceProperties, final String cookieName) {
61+
Assert.notNull(serviceProperties, "serviceProperties cannot be null");
62+
this.serviceProperties = serviceProperties;
63+
this.cookieName = cookieName;
64+
}
65+
66+
public final boolean matches(HttpServletRequest request) {
67+
68+
// Test if we are already authenticated
69+
if (isAuthenticated(request)) {
70+
return false;
71+
}
72+
73+
// Test if the request was already gatewayed to avoid infinite loop
74+
final boolean wasGatewayed = this.gatewayStorage.hasGatewayedAlready(request,
75+
this.serviceProperties.getService());
76+
77+
if (wasGatewayed) {
78+
return false;
79+
}
80+
81+
// If request matches gateway criteria, we mark the request as gatewayed and
82+
// return true to trigger a CAS
83+
// gateway authentication
84+
if (performGatewayAuthentication(request)) {
85+
this.gatewayStorage.storeGatewayInformation(request, this.serviceProperties.getService());
86+
return true;
87+
}
88+
else {
89+
return false;
90+
}
91+
}
92+
93+
/**
94+
* Test if the user is authenticated in Spring Security. Default implementation test
95+
* if the user is CAS authenticated.
96+
* @param request
97+
* @return true if the user is authenticated
98+
*/
99+
protected boolean isAuthenticated(HttpServletRequest request) {
100+
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
101+
return authentication instanceof CasAuthenticationToken;
102+
}
103+
104+
/**
105+
* Method that determines if the current request triggers a CAS gateway
106+
* authentication. This implementation returns <code>true</code> only if a
107+
* {@link Cookie} with the configured name is present at the request
108+
* @param request
109+
* @return true if the request must trigger a CAS gateway authentication
110+
*/
111+
protected boolean performGatewayAuthentication(HttpServletRequest request) {
112+
if (!StringUtils.hasText(this.cookieName)) {
113+
return true;
114+
}
115+
116+
Cookie[] cookies = request.getCookies();
117+
if (cookies == null || cookies.length == 0) {
118+
return false;
119+
}
120+
121+
for (Cookie cookie : cookies) {
122+
// Check the cookie name. If it matches the configured cookie name, return
123+
// true
124+
if (this.cookieName.equalsIgnoreCase(cookie.getName())) {
125+
return true;
126+
}
127+
}
128+
return false;
129+
}
130+
131+
public void setGatewayStorage(GatewayResolver gatewayStorage) {
132+
Assert.notNull(gatewayStorage, "gatewayStorage cannot be null");
133+
this.gatewayStorage = gatewayStorage;
134+
}
135+
136+
public String getCookieName() {
137+
return this.cookieName;
138+
}
139+
140+
public void setCookieName(String cookieName) {
141+
this.cookieName = cookieName;
142+
}
143+
144+
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
/*
2+
* Copyright 2002-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.cas.web;
18+
19+
import java.io.IOException;
20+
21+
import jakarta.servlet.FilterChain;
22+
import jakarta.servlet.ServletException;
23+
import jakarta.servlet.ServletRequest;
24+
import jakarta.servlet.ServletResponse;
25+
import jakarta.servlet.http.HttpServletRequest;
26+
import jakarta.servlet.http.HttpServletResponse;
27+
import jakarta.servlet.http.HttpSession;
28+
import org.apereo.cas.client.util.CommonUtils;
29+
import org.apereo.cas.client.util.WebUtils;
30+
31+
import org.springframework.security.cas.ServiceProperties;
32+
import org.springframework.security.web.DefaultRedirectStrategy;
33+
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
34+
import org.springframework.security.web.savedrequest.RequestCache;
35+
import org.springframework.security.web.util.matcher.RequestMatcher;
36+
import org.springframework.util.Assert;
37+
import org.springframework.web.filter.GenericFilterBean;
38+
39+
/**
40+
* Triggers a CAS gateway authentication attempt.
41+
* <p>
42+
* This filter requires a web session to work.
43+
* <p>
44+
* This filter must be placed after the {@link CasAuthenticationFilter} if it is defined.
45+
* <p>
46+
* The default implementation is {@link CasCookieGatewayRequestMatcher}.
47+
*
48+
* @author Michael Remond
49+
* @author Jerome LELEU
50+
*/
51+
public class TriggerCasGatewayFilter extends GenericFilterBean {
52+
53+
public static final String TRIGGER_CAS_GATEWAY_AUTHENTICATION = "triggerCasGatewayAuthentication";
54+
55+
private final String loginUrl;
56+
57+
private final ServiceProperties serviceProperties;
58+
59+
private RequestMatcher requestMatcher;
60+
61+
private RequestCache requestCache = new HttpSessionRequestCache();
62+
63+
public TriggerCasGatewayFilter(String loginUrl, ServiceProperties serviceProperties) {
64+
this.loginUrl = loginUrl;
65+
this.serviceProperties = serviceProperties;
66+
this.requestMatcher = new CasCookieGatewayRequestMatcher(this.serviceProperties, null);
67+
}
68+
69+
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
70+
throws IOException, ServletException {
71+
72+
HttpServletRequest request = (HttpServletRequest) req;
73+
HttpServletResponse response = (HttpServletResponse) res;
74+
75+
if (this.requestMatcher.matches(request)) {
76+
// Try a CAS gateway authentication
77+
this.requestCache.saveRequest(request, response);
78+
HttpSession session = request.getSession(false);
79+
if (session != null) {
80+
session.setAttribute(TRIGGER_CAS_GATEWAY_AUTHENTICATION, true);
81+
}
82+
String urlEncodedService = WebUtils.constructServiceUrl(null, response, this.serviceProperties.getService(),
83+
null, this.serviceProperties.getArtifactParameter(), true);
84+
String redirectUrl = CommonUtils.constructRedirectUrl(this.loginUrl,
85+
this.serviceProperties.getServiceParameter(), urlEncodedService,
86+
this.serviceProperties.isSendRenew(), true);
87+
new DefaultRedirectStrategy().sendRedirect(request, response, redirectUrl);
88+
}
89+
else {
90+
// Continue in the chain
91+
chain.doFilter(request, response);
92+
}
93+
94+
}
95+
96+
public String getLoginUrl() {
97+
return this.loginUrl;
98+
}
99+
100+
public ServiceProperties getServiceProperties() {
101+
return this.serviceProperties;
102+
}
103+
104+
public RequestMatcher getRequestMatcher() {
105+
return this.requestMatcher;
106+
}
107+
108+
public RequestCache getRequestCache() {
109+
return this.requestCache;
110+
}
111+
112+
public void setRequestMatcher(RequestMatcher requestMatcher) {
113+
Assert.notNull(requestMatcher, "requestMatcher cannot be null");
114+
this.requestMatcher = requestMatcher;
115+
}
116+
117+
public final void setRequestCache(RequestCache requestCache) {
118+
Assert.notNull(requestCache, "requestCache cannot be null");
119+
this.requestCache = requestCache;
120+
}
121+
122+
}

cas/src/test/java/org/springframework/security/cas/web/CasAuthenticationFilterTests.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import org.springframework.security.core.context.SecurityContextHolder;
3737
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
3838
import org.springframework.security.web.context.SecurityContextRepository;
39+
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
3940
import org.springframework.test.util.ReflectionTestUtils;
4041

4142
import static org.assertj.core.api.Assertions.assertThat;
@@ -219,4 +220,19 @@ public void successfulAuthenticationWhenProxyRequestThenSavesSecurityContext() t
219220
verify(securityContextRepository).saveContext(any(SecurityContext.class), eq(request), eq(response));
220221
}
221222

223+
@Test
224+
public void testNullServiceButGateway() throws Exception {
225+
CasAuthenticationFilter filter = new CasAuthenticationFilter();
226+
MockHttpServletRequest request = new MockHttpServletRequest();
227+
MockHttpServletResponse response = new MockHttpServletResponse();
228+
request.getSession(true).setAttribute(TriggerCasGatewayFilter.TRIGGER_CAS_GATEWAY_AUTHENTICATION, true);
229+
230+
new HttpSessionRequestCache().saveRequest(request, response);
231+
232+
Authentication authn = filter.attemptAuthentication(request, response);
233+
assertThat(authn).isNull();
234+
assertThat(response.getStatus()).isEqualTo(302);
235+
assertThat(response.getRedirectedUrl()).isEqualTo("http://localhost?continue");
236+
}
237+
222238
}

0 commit comments

Comments
 (0)