Explorar o código

Use a Custom Authentication Token for CAS

Closes gh-12304
hdeadman %!s(int64=2) %!d(string=hai) anos
pai
achega
04369cf2da

+ 6 - 16
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).
  * <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));
 	}

+ 112 - 0
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
+	 * <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;
+	}
+
+}

+ 4 - 2
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);
 	}
 
 	/**

+ 19 - 29
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;
  * <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);
 	}

+ 7 - 13
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();
 	}
 

+ 0 - 11
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");