Skip to content

Commit 04369cf

Browse files
hdeadmanmarcusdacoregio
authored andcommitted
Use a Custom Authentication Token for CAS
Closes gh-12304
1 parent e0284a4 commit 04369cf

File tree

6 files changed

+148
-71
lines changed

6 files changed

+148
-71
lines changed

cas/src/main/java/org/springframework/security/cas/authentication/CasAuthenticationProvider.java

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,7 @@
3030
import org.springframework.security.authentication.AccountStatusUserDetailsChecker;
3131
import org.springframework.security.authentication.AuthenticationProvider;
3232
import org.springframework.security.authentication.BadCredentialsException;
33-
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
3433
import org.springframework.security.cas.ServiceProperties;
35-
import org.springframework.security.cas.web.CasAuthenticationFilter;
3634
import org.springframework.security.cas.web.authentication.ServiceAuthenticationDetails;
3735
import org.springframework.security.core.Authentication;
3836
import org.springframework.security.core.AuthenticationException;
@@ -51,11 +49,11 @@
5149
* Authentication Service (CAS).
5250
* <p>
5351
* This <code>AuthenticationProvider</code> is capable of validating
54-
* {@link UsernamePasswordAuthenticationToken} requests which contain a
52+
* {@link CasServiceTicketAuthenticationToken} requests which contain a
5553
* <code>principal</code> name equal to either
56-
* {@link CasAuthenticationFilter#CAS_STATEFUL_IDENTIFIER} or
57-
* {@link CasAuthenticationFilter#CAS_STATELESS_IDENTIFIER}. It can also validate a
58-
* previously created {@link CasAuthenticationToken}.
54+
* {@link CasServiceTicketAuthenticationToken#CAS_STATEFUL_IDENTIFIER} or
55+
* {@link CasServiceTicketAuthenticationToken#CAS_STATELESS_IDENTIFIER}. It can also
56+
* validate a previously created {@link CasAuthenticationToken}.
5957
*
6058
* @author Ben Alex
6159
* @author Scott Battaglia
@@ -95,13 +93,6 @@ public Authentication authenticate(Authentication authentication) throws Authent
9593
if (!supports(authentication.getClass())) {
9694
return null;
9795
}
98-
if (authentication instanceof UsernamePasswordAuthenticationToken
99-
&& (!CasAuthenticationFilter.CAS_STATEFUL_IDENTIFIER.equals(authentication.getPrincipal().toString())
100-
&& !CasAuthenticationFilter.CAS_STATELESS_IDENTIFIER
101-
.equals(authentication.getPrincipal().toString()))) {
102-
// UsernamePasswordAuthenticationToken not CAS related
103-
return null;
104-
}
10596
// If an existing CasAuthenticationToken, just check we created it
10697
if (authentication instanceof CasAuthenticationToken) {
10798
if (this.key.hashCode() != ((CasAuthenticationToken) authentication).getKeyHash()) {
@@ -117,8 +108,7 @@ public Authentication authenticate(Authentication authentication) throws Authent
117108
"Failed to provide a CAS service ticket to validate"));
118109
}
119110

120-
boolean stateless = (authentication instanceof UsernamePasswordAuthenticationToken
121-
&& CasAuthenticationFilter.CAS_STATELESS_IDENTIFIER.equals(authentication.getPrincipal()));
111+
boolean stateless = (authentication instanceof CasServiceTicketAuthenticationToken token && token.isStateless());
122112
CasAuthenticationToken result = null;
123113

124114
if (stateless) {
@@ -236,7 +226,7 @@ public void setAuthoritiesMapper(GrantedAuthoritiesMapper authoritiesMapper) {
236226

237227
@Override
238228
public boolean supports(final Class<?> authentication) {
239-
return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication))
229+
return (CasServiceTicketAuthenticationToken.class.isAssignableFrom(authentication))
240230
|| (CasAuthenticationToken.class.isAssignableFrom(authentication))
241231
|| (CasAssertionAuthenticationToken.class.isAssignableFrom(authentication));
242232
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
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.authentication;
18+
19+
import java.io.Serial;
20+
import java.util.Collection;
21+
22+
import org.springframework.security.authentication.AbstractAuthenticationToken;
23+
import org.springframework.security.core.GrantedAuthority;
24+
import org.springframework.security.core.SpringSecurityCoreVersion;
25+
import org.springframework.util.Assert;
26+
27+
/**
28+
* An {@link org.springframework.security.core.Authentication} implementation that is
29+
* designed to process CAS service ticket.
30+
*
31+
* @author Hal Deadman
32+
* @since 6.1
33+
*/
34+
public class CasServiceTicketAuthenticationToken extends AbstractAuthenticationToken {
35+
36+
static final String CAS_STATELESS_IDENTIFIER = "_cas_stateless_";
37+
38+
static final String CAS_STATEFUL_IDENTIFIER = "_cas_stateful_";
39+
40+
@Serial
41+
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
42+
43+
private final String identifier;
44+
45+
private Object credentials;
46+
47+
/**
48+
* This constructor can be safely used by any code that wishes to create a
49+
* <code>CasServiceTicketAuthenticationToken</code>, as the {@link #isAuthenticated()}
50+
* will return <code>false</code>.
51+
*
52+
*/
53+
public CasServiceTicketAuthenticationToken(String identifier, Object credentials) {
54+
super(null);
55+
this.identifier = identifier;
56+
this.credentials = credentials;
57+
setAuthenticated(false);
58+
}
59+
60+
/**
61+
* This constructor should only be used by <code>AuthenticationManager</code> or
62+
* <code>AuthenticationProvider</code> implementations that are satisfied with
63+
* producing a trusted (i.e. {@link #isAuthenticated()} = <code>true</code>)
64+
* authentication token.
65+
* @param identifier
66+
* @param credentials
67+
* @param authorities
68+
*/
69+
public CasServiceTicketAuthenticationToken(String identifier, Object credentials,
70+
Collection<? extends GrantedAuthority> authorities) {
71+
super(authorities);
72+
this.identifier = identifier;
73+
this.credentials = credentials;
74+
super.setAuthenticated(true);
75+
}
76+
77+
public static CasServiceTicketAuthenticationToken stateful(Object credentials) {
78+
return new CasServiceTicketAuthenticationToken(CAS_STATEFUL_IDENTIFIER, credentials);
79+
}
80+
81+
public static CasServiceTicketAuthenticationToken stateless(Object credentials) {
82+
return new CasServiceTicketAuthenticationToken(CAS_STATELESS_IDENTIFIER, credentials);
83+
}
84+
85+
public boolean isStateless() {
86+
return CAS_STATELESS_IDENTIFIER.equals(this.identifier);
87+
}
88+
89+
@Override
90+
public Object getCredentials() {
91+
return this.credentials;
92+
}
93+
94+
@Override
95+
public Object getPrincipal() {
96+
return this.identifier;
97+
}
98+
99+
@Override
100+
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
101+
Assert.isTrue(!isAuthenticated,
102+
"Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
103+
super.setAuthenticated(false);
104+
}
105+
106+
@Override
107+
public void eraseCredentials() {
108+
super.eraseCredentials();
109+
this.credentials = null;
110+
}
111+
112+
}

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -26,6 +26,7 @@
2626
import org.springframework.security.cas.ServiceProperties;
2727
import org.springframework.security.core.AuthenticationException;
2828
import org.springframework.security.web.AuthenticationEntryPoint;
29+
import org.springframework.security.web.DefaultRedirectStrategy;
2930
import org.springframework.util.Assert;
3031

3132
/**
@@ -72,7 +73,8 @@ public final void commence(final HttpServletRequest servletRequest, HttpServletR
7273
String urlEncodedService = createServiceUrl(servletRequest, response);
7374
String redirectUrl = createRedirectUrl(urlEncodedService);
7475
preCommence(servletRequest, response);
75-
response.sendRedirect(redirectUrl);
76+
new DefaultRedirectStrategy().sendRedirect(servletRequest, response, redirectUrl);
77+
// response.sendRedirect(redirectUrl);
7678
}
7779

7880
/**

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

Lines changed: 19 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -29,9 +29,9 @@
2929
import org.springframework.core.log.LogMessage;
3030
import org.springframework.security.authentication.AnonymousAuthenticationToken;
3131
import org.springframework.security.authentication.AuthenticationDetailsSource;
32-
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
3332
import org.springframework.security.authentication.event.InteractiveAuthenticationSuccessEvent;
3433
import org.springframework.security.cas.ServiceProperties;
34+
import org.springframework.security.cas.authentication.CasServiceTicketAuthenticationToken;
3535
import org.springframework.security.cas.web.authentication.ServiceAuthenticationDetails;
3636
import org.springframework.security.cas.web.authentication.ServiceAuthenticationDetailsSource;
3737
import org.springframework.security.core.Authentication;
@@ -41,6 +41,7 @@
4141
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
4242
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
4343
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
44+
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
4445
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
4546
import org.springframework.security.web.util.matcher.RequestMatcher;
4647
import org.springframework.util.Assert;
@@ -63,9 +64,9 @@
6364
* <tt>filterProcessesUrl</tt>.
6465
* <p>
6566
* Processing the service ticket involves creating a
66-
* <code>UsernamePasswordAuthenticationToken</code> which uses
67-
* {@link #CAS_STATEFUL_IDENTIFIER} for the <code>principal</code> and the opaque ticket
68-
* string as the <code>credentials</code>.
67+
* <code>CasServiceTicketAuthenticationToken</code> which uses
68+
* {@link CasServiceTicketAuthenticationToken#CAS_STATEFUL_IDENTIFIER} for the
69+
* <code>principal</code> and the opaque ticket string as the <code>credentials</code>.
6970
* <h2>Obtaining Proxy Granting Tickets</h2>
7071
* <p>
7172
* If specified, the filter can also monitor the <code>proxyReceptorUrl</code>. The filter
@@ -88,15 +89,15 @@
8889
* {@link ServiceAuthenticationDetails#getServiceUrl()} will be used for the service url.
8990
* <p>
9091
* Processing the proxy ticket involves creating a
91-
* <code>UsernamePasswordAuthenticationToken</code> which uses
92-
* {@link #CAS_STATELESS_IDENTIFIER} for the <code>principal</code> and the opaque ticket
93-
* string as the <code>credentials</code>. When a proxy ticket is successfully
94-
* authenticated, the FilterChain continues and the
92+
* <code>CasServiceTicketAuthenticationToken</code> which uses
93+
* {@link CasServiceTicketAuthenticationToken#CAS_STATELESS_IDENTIFIER} for the
94+
* <code>principal</code> and the opaque ticket string as the <code>credentials</code>.
95+
* When a proxy ticket is successfully authenticated, the FilterChain continues and the
9596
* <code>authenticationSuccessHandler</code> is not used.
9697
* <h2>Notes about the <code>AuthenticationManager</code></h2>
9798
* <p>
9899
* The configured <code>AuthenticationManager</code> is expected to provide a provider
99-
* that can recognise <code>UsernamePasswordAuthenticationToken</code>s containing this
100+
* that can recognise <code>CasServiceTicketAuthenticationToken</code>s containing this
100101
* special <code>principal</code> name, and process them accordingly by validation with
101102
* the CAS server. Additionally, it should be capable of using the result of
102103
* {@link ServiceAuthenticationDetails#getServiceUrl()} as the service when validating the
@@ -175,19 +176,6 @@
175176
*/
176177
public class CasAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
177178

178-
/**
179-
* Used to identify a CAS request for a stateful user agent, such as a web browser.
180-
*/
181-
public static final String CAS_STATEFUL_IDENTIFIER = "_cas_stateful_";
182-
183-
/**
184-
* Used to identify a CAS request for a stateless user agent, such as a remoting
185-
* protocol client (e.g. Hessian, Burlap, SOAP etc). Results in a more aggressive
186-
* caching strategy being used, as the absence of a <code>HttpSession</code> will
187-
* result in a new authentication attempt on every request.
188-
*/
189-
public static final String CAS_STATELESS_IDENTIFIER = "_cas_stateless_";
190-
191179
/**
192180
* The last portion of the receptor url, i.e. /proxy/receptor
193181
*/
@@ -207,6 +195,7 @@ public class CasAuthenticationFilter extends AbstractAuthenticationProcessingFil
207195
public CasAuthenticationFilter() {
208196
super("/login/cas");
209197
setAuthenticationFailureHandler(new SimpleUrlAuthenticationFailureHandler());
198+
setSecurityContextRepository(new HttpSessionSecurityContextRepository());
210199
}
211200

212201
@Override
@@ -238,14 +227,15 @@ public Authentication attemptAuthentication(HttpServletRequest request, HttpServ
238227
CommonUtils.readAndRespondToProxyReceptorRequest(request, response, this.proxyGrantingTicketStorage);
239228
return null;
240229
}
241-
boolean serviceTicketRequest = serviceTicketRequest(request, response);
242-
String username = serviceTicketRequest ? CAS_STATEFUL_IDENTIFIER : CAS_STATELESS_IDENTIFIER;
243-
String password = obtainArtifact(request);
244-
if (password == null) {
230+
String serviceTicket = obtainArtifact(request);
231+
if (serviceTicket == null) {
245232
this.logger.debug("Failed to obtain an artifact (cas ticket)");
246-
password = "";
233+
serviceTicket = "";
247234
}
248-
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
235+
boolean serviceTicketRequest = serviceTicketRequest(request, response);
236+
CasServiceTicketAuthenticationToken authRequest = serviceTicketRequest
237+
? CasServiceTicketAuthenticationToken.stateful(serviceTicket)
238+
: CasServiceTicketAuthenticationToken.stateless(serviceTicket);
249239
authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
250240
return this.getAuthenticationManager().authenticate(authRequest);
251241
}

cas/src/test/java/org/springframework/security/cas/authentication/CasAuthenticationProviderTests.java

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929
import org.springframework.security.authentication.TestingAuthenticationToken;
3030
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
3131
import org.springframework.security.cas.ServiceProperties;
32-
import org.springframework.security.cas.web.CasAuthenticationFilter;
3332
import org.springframework.security.cas.web.authentication.ServiceAuthenticationDetails;
3433
import org.springframework.security.core.Authentication;
3534
import org.springframework.security.core.authority.AuthorityUtils;
@@ -87,8 +86,7 @@ public void statefulAuthenticationIsSuccessful() throws Exception {
8786
cap.setServiceProperties(makeServiceProperties());
8887
cap.setTicketValidator(new MockTicketValidator(true));
8988
cap.afterPropertiesSet();
90-
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
91-
CasAuthenticationFilter.CAS_STATEFUL_IDENTIFIER, "ST-123");
89+
CasServiceTicketAuthenticationToken token = CasServiceTicketAuthenticationToken.stateful("ST-123");
9290
token.setDetails("details");
9391
Authentication result = cap.authenticate(token);
9492
// Confirm ST-123 was NOT added to the cache
@@ -120,8 +118,7 @@ public void statelessAuthenticationIsSuccessful() throws Exception {
120118
cap.setTicketValidator(new MockTicketValidator(true));
121119
cap.setServiceProperties(makeServiceProperties());
122120
cap.afterPropertiesSet();
123-
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
124-
CasAuthenticationFilter.CAS_STATELESS_IDENTIFIER, "ST-456");
121+
CasServiceTicketAuthenticationToken token = CasServiceTicketAuthenticationToken.stateless("ST-456");
125122
token.setDetails("details");
126123
Authentication result = cap.authenticate(token);
127124
// Confirm ST-456 was added to the cache
@@ -135,7 +132,7 @@ public void statelessAuthenticationIsSuccessful() throws Exception {
135132
// Now try to authenticate again. To ensure TicketValidator not
136133
// called again, set it to deliver an exception...
137134
cap.setTicketValidator(new MockTicketValidator(false));
138-
// Previously created UsernamePasswordAuthenticationToken is OK
135+
// Previously created CasServiceTicketAuthenticationToken is OK
139136
Authentication newResult = cap.authenticate(token);
140137
assertThat(newResult.getPrincipal()).isEqualTo(makeUserDetailsFromAuthoritiesPopulator());
141138
assertThat(newResult.getCredentials()).isEqualTo("ST-456");
@@ -157,8 +154,7 @@ public void authenticateAllNullService() throws Exception {
157154
cap.setServiceProperties(serviceProperties);
158155
cap.afterPropertiesSet();
159156
String ticket = "ST-456";
160-
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
161-
CasAuthenticationFilter.CAS_STATELESS_IDENTIFIER, ticket);
157+
CasServiceTicketAuthenticationToken token = CasServiceTicketAuthenticationToken.stateless(ticket);
162158
Authentication result = cap.authenticate(token);
163159
}
164160

@@ -178,8 +174,7 @@ public void authenticateAllAuthenticationIsSuccessful() throws Exception {
178174
cap.setServiceProperties(serviceProperties);
179175
cap.afterPropertiesSet();
180176
String ticket = "ST-456";
181-
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
182-
CasAuthenticationFilter.CAS_STATELESS_IDENTIFIER, ticket);
177+
CasServiceTicketAuthenticationToken token = CasServiceTicketAuthenticationToken.stateless(ticket);
183178
Authentication result = cap.authenticate(token);
184179
verify(validator).validate(ticket, serviceProperties.getService());
185180
serviceProperties.setAuthenticateAllArtifacts(true);
@@ -211,8 +206,7 @@ public void missingTicketIdIsDetected() throws Exception {
211206
cap.setTicketValidator(new MockTicketValidator(true));
212207
cap.setServiceProperties(makeServiceProperties());
213208
cap.afterPropertiesSet();
214-
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
215-
CasAuthenticationFilter.CAS_STATEFUL_IDENTIFIER, "");
209+
CasServiceTicketAuthenticationToken token = CasServiceTicketAuthenticationToken.stateful("");
216210
assertThatExceptionOfType(BadCredentialsException.class).isThrownBy(() -> cap.authenticate(token));
217211
}
218212

@@ -322,7 +316,7 @@ public void ignoresUsernamePasswordAuthenticationTokensWithoutCasIdentifiersAsPr
322316
@Test
323317
public void supportsRequiredTokens() {
324318
CasAuthenticationProvider cap = new CasAuthenticationProvider();
325-
assertThat(cap.supports(UsernamePasswordAuthenticationToken.class)).isTrue();
319+
assertThat(cap.supports(CasServiceTicketAuthenticationToken.class)).isTrue();
326320
assertThat(cap.supports(CasAuthenticationToken.class)).isTrue();
327321
}
328322

0 commit comments

Comments
 (0)