diff --git a/cas/src/main/java/org/springframework/security/cas/authentication/CasAuthenticationProvider.java b/cas/src/main/java/org/springframework/security/cas/authentication/CasAuthenticationProvider.java index 36fd203f88..1f75d1276d 100644 --- a/cas/src/main/java/org/springframework/security/cas/authentication/CasAuthenticationProvider.java +++ b/cas/src/main/java/org/springframework/security/cas/authentication/CasAuthenticationProvider.java @@ -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). *

* This AuthenticationProvider is capable of validating - * {@link UsernamePasswordAuthenticationToken} requests which contain a + * {@link CasServiceTicketAuthenticationToken} requests which contain a * principal 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)); } diff --git a/cas/src/main/java/org/springframework/security/cas/authentication/CasServiceTicketAuthenticationToken.java b/cas/src/main/java/org/springframework/security/cas/authentication/CasServiceTicketAuthenticationToken.java new file mode 100644 index 0000000000..d4471b6c57 --- /dev/null +++ b/cas/src/main/java/org/springframework/security/cas/authentication/CasServiceTicketAuthenticationToken.java @@ -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 + * CasServiceTicketAuthenticationToken, as the {@link #isAuthenticated()} + * will return false. + * + */ + public CasServiceTicketAuthenticationToken(String identifier, Object credentials) { + super(null); + this.identifier = identifier; + this.credentials = credentials; + setAuthenticated(false); + } + + /** + * This constructor should only be used by AuthenticationManager or + * AuthenticationProvider implementations that are satisfied with + * producing a trusted (i.e. {@link #isAuthenticated()} = true) + * authentication token. + * @param identifier + * @param credentials + * @param authorities + */ + public CasServiceTicketAuthenticationToken(String identifier, Object credentials, + Collection 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; + } + +} diff --git a/cas/src/main/java/org/springframework/security/cas/web/CasAuthenticationEntryPoint.java b/cas/src/main/java/org/springframework/security/cas/web/CasAuthenticationEntryPoint.java index 47311ccf7d..a1b58e6cc2 100644 --- a/cas/src/main/java/org/springframework/security/cas/web/CasAuthenticationEntryPoint.java +++ b/cas/src/main/java/org/springframework/security/cas/web/CasAuthenticationEntryPoint.java @@ -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); } /** diff --git a/cas/src/main/java/org/springframework/security/cas/web/CasAuthenticationFilter.java b/cas/src/main/java/org/springframework/security/cas/web/CasAuthenticationFilter.java index 8a0790c0cd..a951168f66 100644 --- a/cas/src/main/java/org/springframework/security/cas/web/CasAuthenticationFilter.java +++ b/cas/src/main/java/org/springframework/security/cas/web/CasAuthenticationFilter.java @@ -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; * filterProcessesUrl. *

* Processing the service ticket involves creating a - * UsernamePasswordAuthenticationToken which uses - * {@link #CAS_STATEFUL_IDENTIFIER} for the principal and the opaque ticket - * string as the credentials. + * CasServiceTicketAuthenticationToken which uses + * {@link CasServiceTicketAuthenticationToken#CAS_STATEFUL_IDENTIFIER} for the + * principal and the opaque ticket string as the credentials. *

Obtaining Proxy Granting Tickets

*

* If specified, the filter can also monitor the proxyReceptorUrl. The filter @@ -88,15 +89,15 @@ import org.springframework.util.Assert; * {@link ServiceAuthenticationDetails#getServiceUrl()} will be used for the service url. *

* Processing the proxy ticket involves creating a - * UsernamePasswordAuthenticationToken which uses - * {@link #CAS_STATELESS_IDENTIFIER} for the principal and the opaque ticket - * string as the credentials. When a proxy ticket is successfully - * authenticated, the FilterChain continues and the + * CasServiceTicketAuthenticationToken which uses + * {@link CasServiceTicketAuthenticationToken#CAS_STATELESS_IDENTIFIER} for the + * principal and the opaque ticket string as the credentials. + * When a proxy ticket is successfully authenticated, the FilterChain continues and the * authenticationSuccessHandler is not used. *

Notes about the AuthenticationManager

*

* The configured AuthenticationManager is expected to provide a provider - * that can recognise UsernamePasswordAuthenticationTokens containing this + * that can recognise CasServiceTicketAuthenticationTokens containing this * special principal 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 HttpSession 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); } diff --git a/cas/src/test/java/org/springframework/security/cas/authentication/CasAuthenticationProviderTests.java b/cas/src/test/java/org/springframework/security/cas/authentication/CasAuthenticationProviderTests.java index ed3e296029..f11f191507 100644 --- a/cas/src/test/java/org/springframework/security/cas/authentication/CasAuthenticationProviderTests.java +++ b/cas/src/test/java/org/springframework/security/cas/authentication/CasAuthenticationProviderTests.java @@ -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(); } diff --git a/cas/src/test/java/org/springframework/security/cas/authentication/CasAuthenticationTokenTests.java b/cas/src/test/java/org/springframework/security/cas/authentication/CasAuthenticationTokenTests.java index 3a5ee3ee49..6cc388d988 100644 --- a/cas/src/test/java/org/springframework/security/cas/authentication/CasAuthenticationTokenTests.java +++ b/cas/src/test/java/org/springframework/security/cas/authentication/CasAuthenticationTokenTests.java @@ -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");