Use a Custom Authentication Token for CAS

Closes gh-12304
This commit is contained in:
hdeadman 2022-12-10 13:46:39 -05:00 committed by Marcus Da Coregio
parent e0284a4503
commit 04369cf2da
6 changed files with 148 additions and 71 deletions

View File

@ -30,9 +30,7 @@ import org.springframework.core.log.LogMessage;
import org.springframework.security.authentication.AccountStatusUserDetailsChecker;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.cas.ServiceProperties;
import org.springframework.security.cas.web.CasAuthenticationFilter;
import org.springframework.security.cas.web.authentication.ServiceAuthenticationDetails;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
@ -51,11 +49,11 @@ import org.springframework.util.Assert;
* Authentication Service (CAS).
* <p>
* This <code>AuthenticationProvider</code> is capable of validating
* {@link UsernamePasswordAuthenticationToken} requests which contain a
* {@link CasServiceTicketAuthenticationToken} requests which contain a
* <code>principal</code> name equal to either
* {@link CasAuthenticationFilter#CAS_STATEFUL_IDENTIFIER} or
* {@link CasAuthenticationFilter#CAS_STATELESS_IDENTIFIER}. It can also validate a
* previously created {@link CasAuthenticationToken}.
* {@link CasServiceTicketAuthenticationToken#CAS_STATEFUL_IDENTIFIER} or
* {@link CasServiceTicketAuthenticationToken#CAS_STATELESS_IDENTIFIER}. It can also
* validate a previously created {@link CasAuthenticationToken}.
*
* @author Ben Alex
* @author Scott Battaglia
@ -95,13 +93,6 @@ public class CasAuthenticationProvider implements AuthenticationProvider, Initia
if (!supports(authentication.getClass())) {
return null;
}
if (authentication instanceof UsernamePasswordAuthenticationToken
&& (!CasAuthenticationFilter.CAS_STATEFUL_IDENTIFIER.equals(authentication.getPrincipal().toString())
&& !CasAuthenticationFilter.CAS_STATELESS_IDENTIFIER
.equals(authentication.getPrincipal().toString()))) {
// UsernamePasswordAuthenticationToken not CAS related
return null;
}
// If an existing CasAuthenticationToken, just check we created it
if (authentication instanceof CasAuthenticationToken) {
if (this.key.hashCode() != ((CasAuthenticationToken) authentication).getKeyHash()) {
@ -117,8 +108,7 @@ public class CasAuthenticationProvider implements AuthenticationProvider, Initia
"Failed to provide a CAS service ticket to validate"));
}
boolean stateless = (authentication instanceof UsernamePasswordAuthenticationToken
&& CasAuthenticationFilter.CAS_STATELESS_IDENTIFIER.equals(authentication.getPrincipal()));
boolean stateless = (authentication instanceof CasServiceTicketAuthenticationToken token && token.isStateless());
CasAuthenticationToken result = null;
if (stateless) {
@ -236,7 +226,7 @@ public class CasAuthenticationProvider implements AuthenticationProvider, Initia
@Override
public boolean supports(final Class<?> authentication) {
return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication))
return (CasServiceTicketAuthenticationToken.class.isAssignableFrom(authentication))
|| (CasAuthenticationToken.class.isAssignableFrom(authentication))
|| (CasAssertionAuthenticationToken.class.isAssignableFrom(authentication));
}

View File

@ -0,0 +1,112 @@
/*
* 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.authentication;
import java.io.Serial;
import java.util.Collection;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.SpringSecurityCoreVersion;
import org.springframework.util.Assert;
/**
* An {@link org.springframework.security.core.Authentication} implementation that is
* designed to process CAS service ticket.
*
* @author Hal Deadman
* @since 6.1
*/
public class CasServiceTicketAuthenticationToken extends AbstractAuthenticationToken {
static final String CAS_STATELESS_IDENTIFIER = "_cas_stateless_";
static final String CAS_STATEFUL_IDENTIFIER = "_cas_stateful_";
@Serial
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
private final String identifier;
private Object credentials;
/**
* This constructor can be safely used by any code that wishes to create a
* <code>CasServiceTicketAuthenticationToken</code>, as the {@link #isAuthenticated()}
* will return <code>false</code>.
*
*/
public CasServiceTicketAuthenticationToken(String identifier, Object credentials) {
super(null);
this.identifier = identifier;
this.credentials = credentials;
setAuthenticated(false);
}
/**
* This constructor should only be used by <code>AuthenticationManager</code> or
* <code>AuthenticationProvider</code> implementations that are satisfied with
* producing a trusted (i.e. {@link #isAuthenticated()} = <code>true</code>)
* authentication token.
* @param identifier
* @param credentials
* @param authorities
*/
public CasServiceTicketAuthenticationToken(String identifier, Object credentials,
Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.identifier = identifier;
this.credentials = credentials;
super.setAuthenticated(true);
}
public static CasServiceTicketAuthenticationToken stateful(Object credentials) {
return new CasServiceTicketAuthenticationToken(CAS_STATEFUL_IDENTIFIER, credentials);
}
public static CasServiceTicketAuthenticationToken stateless(Object credentials) {
return new CasServiceTicketAuthenticationToken(CAS_STATELESS_IDENTIFIER, credentials);
}
public boolean isStateless() {
return CAS_STATELESS_IDENTIFIER.equals(this.identifier);
}
@Override
public Object getCredentials() {
return this.credentials;
}
@Override
public Object getPrincipal() {
return this.identifier;
}
@Override
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
Assert.isTrue(!isAuthenticated,
"Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
super.setAuthenticated(false);
}
@Override
public void eraseCredentials() {
super.eraseCredentials();
this.credentials = null;
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
* 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.
@ -26,6 +26,7 @@ import org.springframework.beans.factory.InitializingBean;
import org.springframework.security.cas.ServiceProperties;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.util.Assert;
/**
@ -72,7 +73,8 @@ public class CasAuthenticationEntryPoint implements AuthenticationEntryPoint, In
String urlEncodedService = createServiceUrl(servletRequest, response);
String redirectUrl = createRedirectUrl(urlEncodedService);
preCommence(servletRequest, response);
response.sendRedirect(redirectUrl);
new DefaultRedirectStrategy().sendRedirect(servletRequest, response, redirectUrl);
// response.sendRedirect(redirectUrl);
}
/**

View File

@ -1,5 +1,5 @@
/*
* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
* 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.
@ -29,9 +29,9 @@ import org.apereo.cas.client.validation.TicketValidator;
import org.springframework.core.log.LogMessage;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.authentication.AuthenticationDetailsSource;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.event.InteractiveAuthenticationSuccessEvent;
import org.springframework.security.cas.ServiceProperties;
import org.springframework.security.cas.authentication.CasServiceTicketAuthenticationToken;
import org.springframework.security.cas.web.authentication.ServiceAuthenticationDetails;
import org.springframework.security.cas.web.authentication.ServiceAuthenticationDetailsSource;
import org.springframework.security.core.Authentication;
@ -41,6 +41,7 @@ import org.springframework.security.core.context.SecurityContextHolder;
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.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
@ -63,9 +64,9 @@ import org.springframework.util.Assert;
* <tt>filterProcessesUrl</tt>.
* <p>
* Processing the service ticket involves creating a
* <code>UsernamePasswordAuthenticationToken</code> which uses
* {@link #CAS_STATEFUL_IDENTIFIER} for the <code>principal</code> and the opaque ticket
* string as the <code>credentials</code>.
* <code>CasServiceTicketAuthenticationToken</code> which uses
* {@link CasServiceTicketAuthenticationToken#CAS_STATEFUL_IDENTIFIER} for the
* <code>principal</code> and the opaque ticket string as the <code>credentials</code>.
* <h2>Obtaining Proxy Granting Tickets</h2>
* <p>
* If specified, the filter can also monitor the <code>proxyReceptorUrl</code>. The filter
@ -88,15 +89,15 @@ import org.springframework.util.Assert;
* {@link ServiceAuthenticationDetails#getServiceUrl()} will be used for the service url.
* <p>
* Processing the proxy ticket involves creating a
* <code>UsernamePasswordAuthenticationToken</code> which uses
* {@link #CAS_STATELESS_IDENTIFIER} for the <code>principal</code> and the opaque ticket
* string as the <code>credentials</code>. When a proxy ticket is successfully
* authenticated, the FilterChain continues and the
* <code>CasServiceTicketAuthenticationToken</code> which uses
* {@link CasServiceTicketAuthenticationToken#CAS_STATELESS_IDENTIFIER} for the
* <code>principal</code> and the opaque ticket string as the <code>credentials</code>.
* When a proxy ticket is successfully authenticated, the FilterChain continues and the
* <code>authenticationSuccessHandler</code> is not used.
* <h2>Notes about the <code>AuthenticationManager</code></h2>
* <p>
* The configured <code>AuthenticationManager</code> is expected to provide a provider
* that can recognise <code>UsernamePasswordAuthenticationToken</code>s containing this
* that can recognise <code>CasServiceTicketAuthenticationToken</code>s containing this
* special <code>principal</code> name, and process them accordingly by validation with
* the CAS server. Additionally, it should be capable of using the result of
* {@link ServiceAuthenticationDetails#getServiceUrl()} as the service when validating the
@ -175,19 +176,6 @@ import org.springframework.util.Assert;
*/
public class CasAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
/**
* Used to identify a CAS request for a stateful user agent, such as a web browser.
*/
public static final String CAS_STATEFUL_IDENTIFIER = "_cas_stateful_";
/**
* Used to identify a CAS request for a stateless user agent, such as a remoting
* protocol client (e.g. Hessian, Burlap, SOAP etc). Results in a more aggressive
* caching strategy being used, as the absence of a <code>HttpSession</code> will
* result in a new authentication attempt on every request.
*/
public static final String CAS_STATELESS_IDENTIFIER = "_cas_stateless_";
/**
* The last portion of the receptor url, i.e. /proxy/receptor
*/
@ -207,6 +195,7 @@ public class CasAuthenticationFilter extends AbstractAuthenticationProcessingFil
public CasAuthenticationFilter() {
super("/login/cas");
setAuthenticationFailureHandler(new SimpleUrlAuthenticationFailureHandler());
setSecurityContextRepository(new HttpSessionSecurityContextRepository());
}
@Override
@ -238,14 +227,15 @@ public class CasAuthenticationFilter extends AbstractAuthenticationProcessingFil
CommonUtils.readAndRespondToProxyReceptorRequest(request, response, this.proxyGrantingTicketStorage);
return null;
}
boolean serviceTicketRequest = serviceTicketRequest(request, response);
String username = serviceTicketRequest ? CAS_STATEFUL_IDENTIFIER : CAS_STATELESS_IDENTIFIER;
String password = obtainArtifact(request);
if (password == null) {
String serviceTicket = obtainArtifact(request);
if (serviceTicket == null) {
this.logger.debug("Failed to obtain an artifact (cas ticket)");
password = "";
serviceTicket = "";
}
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
boolean serviceTicketRequest = serviceTicketRequest(request, response);
CasServiceTicketAuthenticationToken authRequest = serviceTicketRequest
? CasServiceTicketAuthenticationToken.stateful(serviceTicket)
: CasServiceTicketAuthenticationToken.stateless(serviceTicket);
authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
return this.getAuthenticationManager().authenticate(authRequest);
}

View File

@ -29,7 +29,6 @@ import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.cas.ServiceProperties;
import org.springframework.security.cas.web.CasAuthenticationFilter;
import org.springframework.security.cas.web.authentication.ServiceAuthenticationDetails;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.AuthorityUtils;
@ -87,8 +86,7 @@ public class CasAuthenticationProviderTests {
cap.setServiceProperties(makeServiceProperties());
cap.setTicketValidator(new MockTicketValidator(true));
cap.afterPropertiesSet();
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
CasAuthenticationFilter.CAS_STATEFUL_IDENTIFIER, "ST-123");
CasServiceTicketAuthenticationToken token = CasServiceTicketAuthenticationToken.stateful("ST-123");
token.setDetails("details");
Authentication result = cap.authenticate(token);
// Confirm ST-123 was NOT added to the cache
@ -120,8 +118,7 @@ public class CasAuthenticationProviderTests {
cap.setTicketValidator(new MockTicketValidator(true));
cap.setServiceProperties(makeServiceProperties());
cap.afterPropertiesSet();
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
CasAuthenticationFilter.CAS_STATELESS_IDENTIFIER, "ST-456");
CasServiceTicketAuthenticationToken token = CasServiceTicketAuthenticationToken.stateless("ST-456");
token.setDetails("details");
Authentication result = cap.authenticate(token);
// Confirm ST-456 was added to the cache
@ -135,7 +132,7 @@ public class CasAuthenticationProviderTests {
// Now try to authenticate again. To ensure TicketValidator not
// called again, set it to deliver an exception...
cap.setTicketValidator(new MockTicketValidator(false));
// Previously created UsernamePasswordAuthenticationToken is OK
// Previously created CasServiceTicketAuthenticationToken is OK
Authentication newResult = cap.authenticate(token);
assertThat(newResult.getPrincipal()).isEqualTo(makeUserDetailsFromAuthoritiesPopulator());
assertThat(newResult.getCredentials()).isEqualTo("ST-456");
@ -157,8 +154,7 @@ public class CasAuthenticationProviderTests {
cap.setServiceProperties(serviceProperties);
cap.afterPropertiesSet();
String ticket = "ST-456";
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
CasAuthenticationFilter.CAS_STATELESS_IDENTIFIER, ticket);
CasServiceTicketAuthenticationToken token = CasServiceTicketAuthenticationToken.stateless(ticket);
Authentication result = cap.authenticate(token);
}
@ -178,8 +174,7 @@ public class CasAuthenticationProviderTests {
cap.setServiceProperties(serviceProperties);
cap.afterPropertiesSet();
String ticket = "ST-456";
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
CasAuthenticationFilter.CAS_STATELESS_IDENTIFIER, ticket);
CasServiceTicketAuthenticationToken token = CasServiceTicketAuthenticationToken.stateless(ticket);
Authentication result = cap.authenticate(token);
verify(validator).validate(ticket, serviceProperties.getService());
serviceProperties.setAuthenticateAllArtifacts(true);
@ -211,8 +206,7 @@ public class CasAuthenticationProviderTests {
cap.setTicketValidator(new MockTicketValidator(true));
cap.setServiceProperties(makeServiceProperties());
cap.afterPropertiesSet();
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
CasAuthenticationFilter.CAS_STATEFUL_IDENTIFIER, "");
CasServiceTicketAuthenticationToken token = CasServiceTicketAuthenticationToken.stateful("");
assertThatExceptionOfType(BadCredentialsException.class).isThrownBy(() -> cap.authenticate(token));
}
@ -322,7 +316,7 @@ public class CasAuthenticationProviderTests {
@Test
public void supportsRequiredTokens() {
CasAuthenticationProvider cap = new CasAuthenticationProvider();
assertThat(cap.supports(UsernamePasswordAuthenticationToken.class)).isTrue();
assertThat(cap.supports(CasServiceTicketAuthenticationToken.class)).isTrue();
assertThat(cap.supports(CasAuthenticationToken.class)).isTrue();
}

View File

@ -23,7 +23,6 @@ import org.apereo.cas.client.validation.Assertion;
import org.apereo.cas.client.validation.AssertionImpl;
import org.junit.jupiter.api.Test;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
@ -116,16 +115,6 @@ public class CasAuthenticationTokenTests {
assertThat(!token1.equals(token2)).isTrue();
}
@Test
public void testNotEqualsDueToDifferentAuthenticationClass() {
final Assertion assertion = new AssertionImpl("test");
CasAuthenticationToken token1 = new CasAuthenticationToken("key", makeUserDetails(), "Password", this.ROLES,
makeUserDetails(), assertion);
UsernamePasswordAuthenticationToken token2 = new UsernamePasswordAuthenticationToken("Test", "Password",
this.ROLES);
assertThat(!token1.equals(token2)).isTrue();
}
@Test
public void testNotEqualsDueToKey() {
final Assertion assertion = new AssertionImpl("test");