Browse Source

Move toBuilder to BuildableAuthentication

Closes gh-18052
Josh Cummings 1 month ago
parent
commit
e535e61c8b
24 changed files with 279 additions and 333 deletions
  1. 3 1
      cas/src/main/java/org/springframework/security/cas/authentication/CasAuthenticationToken.java
  2. 3 1
      cas/src/main/java/org/springframework/security/cas/authentication/CasServiceTicketAuthenticationToken.java
  3. 5 4
      core/src/main/java/org/springframework/security/authentication/AbstractAuthenticationToken.java
  4. 2 1
      core/src/main/java/org/springframework/security/authentication/RememberMeAuthenticationToken.java
  5. 2 1
      core/src/main/java/org/springframework/security/authentication/TestingAuthenticationToken.java
  6. 3 1
      core/src/main/java/org/springframework/security/authentication/UsernamePasswordAuthenticationToken.java
  7. 2 1
      core/src/main/java/org/springframework/security/authentication/ott/OneTimeTokenAuthentication.java
  8. 0 107
      core/src/main/java/org/springframework/security/core/Authentication.java
  9. 163 0
      core/src/main/java/org/springframework/security/core/BuildableAuthentication.java
  10. 0 151
      core/src/main/java/org/springframework/security/core/SimpleAuthentication.java
  11. 4 2
      docs/src/test/java/org/springframework/security/docs/servlet/authentication/servletauthenticationauthentication/CopyAuthoritiesTests.java
  12. 2 1
      docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/servletauthenticationauthentication/CopyAuthoritiesTests.kt
  13. 2 1
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthenticationToken.java
  14. 3 1
      oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/BearerTokenAuthentication.java
  15. 3 1
      oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationToken.java
  16. 6 3
      oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/authentication/BearerTokenAuthenticationFilter.java
  17. 2 1
      saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AssertionAuthentication.java
  18. 16 13
      web/src/main/java/org/springframework/security/web/authentication/AbstractAuthenticationProcessingFilter.java
  19. 16 13
      web/src/main/java/org/springframework/security/web/authentication/AuthenticationFilter.java
  20. 16 13
      web/src/main/java/org/springframework/security/web/authentication/preauth/AbstractPreAuthenticatedProcessingFilter.java
  21. 3 1
      web/src/main/java/org/springframework/security/web/authentication/preauth/PreAuthenticatedAuthenticationToken.java
  22. 16 13
      web/src/main/java/org/springframework/security/web/authentication/www/BasicAuthenticationFilter.java
  23. 5 1
      web/src/main/java/org/springframework/security/web/server/authentication/AuthenticationWebFilter.java
  24. 2 1
      webauthn/src/main/java/org/springframework/security/web/webauthn/authentication/WebAuthnAuthentication.java

+ 3 - 1
cas/src/main/java/org/springframework/security/cas/authentication/CasAuthenticationToken.java

@@ -23,6 +23,7 @@ import org.apereo.cas.client.validation.Assertion;
 import org.jspecify.annotations.Nullable;
 
 import org.springframework.security.authentication.AbstractAuthenticationToken;
+import org.springframework.security.core.BuildableAuthentication;
 import org.springframework.security.core.GrantedAuthority;
 import org.springframework.security.core.userdetails.UserDetails;
 import org.springframework.util.Assert;
@@ -34,7 +35,8 @@ import org.springframework.util.ObjectUtils;
  * @author Ben Alex
  * @author Scott Battaglia
  */
-public class CasAuthenticationToken extends AbstractAuthenticationToken implements Serializable {
+public class CasAuthenticationToken extends AbstractAuthenticationToken
+		implements BuildableAuthentication, Serializable {
 
 	private static final long serialVersionUID = 620L;
 

+ 3 - 1
cas/src/main/java/org/springframework/security/cas/authentication/CasServiceTicketAuthenticationToken.java

@@ -22,6 +22,7 @@ import java.util.Collection;
 import org.jspecify.annotations.Nullable;
 
 import org.springframework.security.authentication.AbstractAuthenticationToken;
+import org.springframework.security.core.BuildableAuthentication;
 import org.springframework.security.core.GrantedAuthority;
 import org.springframework.util.Assert;
 
@@ -32,7 +33,8 @@ import org.springframework.util.Assert;
  * @author Hal Deadman
  * @since 6.1
  */
-public class CasServiceTicketAuthenticationToken extends AbstractAuthenticationToken {
+public class CasServiceTicketAuthenticationToken extends AbstractAuthenticationToken
+		implements BuildableAuthentication {
 
 	static final String CAS_STATELESS_IDENTIFIER = "_cas_stateless_";
 

+ 5 - 4
core/src/main/java/org/springframework/security/authentication/AbstractAuthenticationToken.java

@@ -28,6 +28,7 @@ import org.jspecify.annotations.Nullable;
 
 import org.springframework.security.core.AuthenticatedPrincipal;
 import org.springframework.security.core.Authentication;
+import org.springframework.security.core.BuildableAuthentication;
 import org.springframework.security.core.CredentialsContainer;
 import org.springframework.security.core.GrantedAuthority;
 import org.springframework.security.core.authority.AuthorityUtils;
@@ -198,15 +199,15 @@ public abstract class AbstractAuthenticationToken implements Authentication, Cre
 	}
 
 	/**
-	 * A common abstract implementation of {@link Authentication.Builder}. It implements
-	 * the builder methods that correspond to the {@link Authentication} methods that
-	 * {@link AbstractAuthenticationToken} implements
+	 * A common abstract implementation of {@link BuildableAuthentication.Builder}. It
+	 * implements the builder methods that correspond to the {@link Authentication}
+	 * methods that {@link AbstractAuthenticationToken} implements
 	 *
 	 * @param <B>
 	 * @since 7.0
 	 */
 	protected abstract static class AbstractAuthenticationBuilder<B extends AbstractAuthenticationBuilder<B>>
-			implements Authentication.Builder<B> {
+			implements BuildableAuthentication.Builder<B> {
 
 		private boolean authenticated;
 

+ 2 - 1
core/src/main/java/org/springframework/security/authentication/RememberMeAuthenticationToken.java

@@ -20,6 +20,7 @@ import java.util.Collection;
 
 import org.jspecify.annotations.Nullable;
 
+import org.springframework.security.core.BuildableAuthentication;
 import org.springframework.security.core.GrantedAuthority;
 import org.springframework.util.Assert;
 
@@ -32,7 +33,7 @@ import org.springframework.util.Assert;
  * @author Ben Alex
  * @author Luke Taylor
  */
-public class RememberMeAuthenticationToken extends AbstractAuthenticationToken {
+public class RememberMeAuthenticationToken extends AbstractAuthenticationToken implements BuildableAuthentication {
 
 	private static final long serialVersionUID = 620L;
 

+ 2 - 1
core/src/main/java/org/springframework/security/authentication/TestingAuthenticationToken.java

@@ -22,6 +22,7 @@ import java.util.List;
 
 import org.jspecify.annotations.Nullable;
 
+import org.springframework.security.core.BuildableAuthentication;
 import org.springframework.security.core.GrantedAuthority;
 import org.springframework.security.core.authority.AuthorityUtils;
 import org.springframework.util.Assert;
@@ -34,7 +35,7 @@ import org.springframework.util.Assert;
  *
  * @author Ben Alex
  */
-public class TestingAuthenticationToken extends AbstractAuthenticationToken {
+public class TestingAuthenticationToken extends AbstractAuthenticationToken implements BuildableAuthentication {
 
 	private static final long serialVersionUID = 1L;
 

+ 3 - 1
core/src/main/java/org/springframework/security/authentication/UsernamePasswordAuthenticationToken.java

@@ -20,6 +20,7 @@ import java.util.Collection;
 
 import org.jspecify.annotations.Nullable;
 
+import org.springframework.security.core.BuildableAuthentication;
 import org.springframework.security.core.GrantedAuthority;
 import org.springframework.util.Assert;
 
@@ -35,7 +36,8 @@ import org.springframework.util.Assert;
  * @author Ben Alex
  * @author Norbert Nowak
  */
-public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
+public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken
+		implements BuildableAuthentication {
 
 	private static final long serialVersionUID = 620L;
 

+ 2 - 1
core/src/main/java/org/springframework/security/authentication/ott/OneTimeTokenAuthentication.java

@@ -22,6 +22,7 @@ import java.util.Collection;
 import org.jspecify.annotations.Nullable;
 
 import org.springframework.security.authentication.AbstractAuthenticationToken;
+import org.springframework.security.core.BuildableAuthentication;
 import org.springframework.security.core.GrantedAuthority;
 import org.springframework.util.Assert;
 
@@ -31,7 +32,7 @@ import org.springframework.util.Assert;
  * @author Josh Cummings
  * @since 7.0
  */
-public class OneTimeTokenAuthentication extends AbstractAuthenticationToken {
+public class OneTimeTokenAuthentication extends AbstractAuthenticationToken implements BuildableAuthentication {
 
 	@Serial
 	private static final long serialVersionUID = 1195893764725073959L;

+ 0 - 107
core/src/main/java/org/springframework/security/core/Authentication.java

@@ -19,7 +19,6 @@ package org.springframework.security.core;
 import java.io.Serializable;
 import java.security.Principal;
 import java.util.Collection;
-import java.util.function.Consumer;
 
 import org.jspecify.annotations.Nullable;
 
@@ -137,110 +136,4 @@ public interface Authentication extends Principal, Serializable {
 	 */
 	void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
 
-	/**
-	 * Return an {@link Builder} based on this instance. By default, returns a builder
-	 * that builds a {@link SimpleAuthentication}.
-	 * <p>
-	 * Although a {@code default} method, all {@link Authentication} implementations
-	 * should implement this. The reason is to ensure that the {@link Authentication} type
-	 * is preserved when {@link Builder#build} is invoked. This is especially important in
-	 * the event that your authentication implementation contains custom fields.
-	 * </p>
-	 * <p>
-	 * This isn't strictly necessary since it is recommended that applications code to the
-	 * {@link Authentication} interface and that custom information is often contained in
-	 * the {@link Authentication#getPrincipal} value.
-	 * </p>
-	 * @return an {@link Builder} for building a new {@link Authentication} based on this
-	 * instance
-	 * @since 7.0
-	 */
-	default Builder<?> toBuilder() {
-		return new SimpleAuthentication.Builder(this);
-	}
-
-	/**
-	 * A builder based on a given {@link Authentication} instance
-	 *
-	 * @author Josh Cummings
-	 * @since 7.0
-	 */
-	interface Builder<B extends Builder<B>> {
-
-		/**
-		 * Mutate the authorities with this {@link Consumer}.
-		 * <p>
-		 * Note that since a non-empty set of authorities implies an
-		 * {@link Authentication} is authenticated, this method also marks the
-		 * authentication as {@link #authenticated} by default.
-		 * </p>
-		 * @param authorities a consumer that receives the full set of authorities
-		 * @return the {@link Builder} for additional configuration
-		 * @see Authentication#getAuthorities
-		 */
-		B authorities(Consumer<Collection<GrantedAuthority>> authorities);
-
-		/**
-		 * Use this credential.
-		 * <p>
-		 * Note that since some credentials are insecure to store, this method is
-		 * implemented as unsupported by default. Only implement or use this method if you
-		 * support secure storage of the credential or if your implementation also
-		 * implements {@link CredentialsContainer} and the credentials are thereby erased.
-		 * </p>
-		 * @param credentials the credentials to use
-		 * @return the {@link Builder} for additional configuration
-		 * @see Authentication#getCredentials
-		 */
-		default B credentials(@Nullable Object credentials) {
-			throw new UnsupportedOperationException(
-					String.format("%s does not store credentials", this.getClass().getSimpleName()));
-		}
-
-		/**
-		 * Use this details object.
-		 * <p>
-		 * Implementations may choose to use these {@code details} in combination with any
-		 * principal from the pre-existing {@link Authentication} instance.
-		 * </p>
-		 * @param details the details to use
-		 * @return the {@link Builder} for additional configuration
-		 * @see Authentication#getDetails
-		 */
-		B details(@Nullable Object details);
-
-		/**
-		 * Use this principal.
-		 * <p>
-		 * Note that in many cases, the principal is strongly-typed. Implementations may
-		 * choose to do a type check and are not necessarily expected to allow any object
-		 * as a principal.
-		 * </p>
-		 * <p>
-		 * Implementations may choose to use this {@code principal} in combination with
-		 * any principal from the pre-existing {@link Authentication} instance.
-		 * </p>
-		 * @param principal the principal to use
-		 * @return the {@link Builder} for additional configuration
-		 * @see Authentication#getPrincipal
-		 */
-		B principal(@Nullable Object principal);
-
-		/**
-		 * Mark this authentication as authenticated or not
-		 * @param authenticated whether this is an authenticated {@link Authentication}
-		 * instance
-		 * @return the {@link Builder} for additional configuration
-		 * @see Authentication#isAuthenticated
-		 */
-		B authenticated(boolean authenticated);
-
-		/**
-		 * Build an {@link Authentication} instance
-		 * @return the {@link Authentication} instance
-		 */
-		Authentication build();
-
-	}
-
 }

+ 163 - 0
core/src/main/java/org/springframework/security/core/BuildableAuthentication.java

@@ -0,0 +1,163 @@
+/*
+ * Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
+ *
+ * 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.core;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+
+import org.jspecify.annotations.Nullable;
+
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.core.context.SecurityContextHolder;
+
+/**
+ * Represents the token for an authentication request or for an authenticated principal
+ * once the request has been processed by the
+ * {@link AuthenticationManager#authenticate(Authentication)} method.
+ * <p>
+ * Once the request has been authenticated, the <tt>Authentication</tt> will usually be
+ * stored in a thread-local <tt>SecurityContext</tt> managed by the
+ * {@link SecurityContextHolder} by the authentication mechanism which is being used. An
+ * explicit authentication can be achieved, without using one of Spring Security's
+ * authentication mechanisms, by creating an <tt>Authentication</tt> instance and using
+ * the code:
+ *
+ * <pre>
+ * SecurityContext context = SecurityContextHolder.createEmptyContext();
+ * context.setAuthentication(anAuthentication);
+ * SecurityContextHolder.setContext(context);
+ * </pre>
+ *
+ * Note that unless the <tt>Authentication</tt> has the <tt>authenticated</tt> property
+ * set to <tt>true</tt>, it will still be authenticated by any security interceptor (for
+ * method or web invocations) which encounters it.
+ * <p>
+ * In most cases, the framework transparently takes care of managing the security context
+ * and authentication objects for you.
+ *
+ * @author Ben Alex
+ */
+public interface BuildableAuthentication extends Authentication {
+
+	/**
+	 * Return an {@link Builder} based on this instance.
+	 * <p>
+	 * Although a {@code default} method, all {@link BuildableAuthentication}
+	 * implementations should implement this. The reason is to ensure that the
+	 * {@link BuildableAuthentication} type is preserved when {@link Builder#build} is
+	 * invoked. This is especially important in the event that your authentication
+	 * implementation contains custom fields.
+	 * </p>
+	 * <p>
+	 * This isn't strictly necessary since it is recommended that applications code to the
+	 * {@link Authentication} interface and that custom information is often contained in
+	 * the {@link Authentication#getPrincipal} value.
+	 * </p>
+	 * @return an {@link Builder} for building a new {@link BuildableAuthentication} based
+	 * on this instance
+	 * @since 7.0
+	 */
+	Builder<?> toBuilder();
+
+	/**
+	 * A builder based on a given {@link BuildableAuthentication} instance
+	 *
+	 * @author Josh Cummings
+	 * @since 7.0
+	 */
+	interface Builder<B extends Builder<B>> {
+
+		/**
+		 * Mutate the authorities with this {@link Consumer}.
+		 * <p>
+		 * Note that since a non-empty set of authorities implies an
+		 * {@link Authentication} is authenticated, this method also marks the
+		 * authentication as {@link #authenticated} by default.
+		 * </p>
+		 * @param authorities a consumer that receives the full set of authorities
+		 * @return the {@link Builder} for additional configuration
+		 * @see Authentication#getAuthorities
+		 */
+		B authorities(Consumer<Collection<GrantedAuthority>> authorities);
+
+		/**
+		 * Use this credential.
+		 * <p>
+		 * Note that since some credentials are insecure to store, this method is
+		 * implemented as unsupported by default. Only implement or use this method if you
+		 * support secure storage of the credential or if your implementation also
+		 * implements {@link CredentialsContainer} and the credentials are thereby erased.
+		 * </p>
+		 * @param credentials the credentials to use
+		 * @return the {@link Builder} for additional configuration
+		 * @see Authentication#getCredentials
+		 */
+		default B credentials(@Nullable Object credentials) {
+			throw new UnsupportedOperationException(
+					String.format("%s does not store credentials", this.getClass().getSimpleName()));
+		}
+
+		/**
+		 * Use this details object.
+		 * <p>
+		 * Implementations may choose to use these {@code details} in combination with any
+		 * principal from the pre-existing {@link Authentication} instance.
+		 * </p>
+		 * @param details the details to use
+		 * @return the {@link Builder} for additional configuration
+		 * @see Authentication#getDetails
+		 */
+		B details(@Nullable Object details);
+
+		/**
+		 * Use this principal.
+		 * <p>
+		 * Note that in many cases, the principal is strongly-typed. Implementations may
+		 * choose to do a type check and are not necessarily expected to allow any object
+		 * as a principal.
+		 * </p>
+		 * <p>
+		 * Implementations may choose to use this {@code principal} in combination with
+		 * any principal from the pre-existing {@link Authentication} instance.
+		 * </p>
+		 * @param principal the principal to use
+		 * @return the {@link Builder} for additional configuration
+		 * @see Authentication#getPrincipal
+		 */
+		B principal(@Nullable Object principal);
+
+		/**
+		 * Mark this authentication as authenticated or not
+		 * @param authenticated whether this is an authenticated
+		 * {@link Authentication} instance
+		 * @return the {@link Builder} for additional configuration
+		 * @see Authentication#isAuthenticated
+		 */
+		B authenticated(boolean authenticated);
+
+		/**
+		 * Build an {@link Authentication} instance
+		 * @return the {@link Authentication} instance
+		 */
+		Authentication build();
+
+	}
+
+}

+ 0 - 151
core/src/main/java/org/springframework/security/core/SimpleAuthentication.java

@@ -1,151 +0,0 @@
-/*
- * Copyright 2004-present 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.core;
-
-import java.io.Serial;
-import java.util.Collection;
-import java.util.LinkedHashSet;
-import java.util.function.Consumer;
-
-import org.apache.commons.logging.Log;
-import org.apache.commons.logging.LogFactory;
-import org.jspecify.annotations.Nullable;
-
-@Transient
-final class SimpleAuthentication implements Authentication {
-
-	@Serial
-	private static final long serialVersionUID = 3194696462184782814L;
-
-	private final @Nullable Object principal;
-
-	private final @Nullable Object credentials;
-
-	private final Collection<GrantedAuthority> authorities;
-
-	private final @Nullable Object details;
-
-	private final boolean authenticated;
-
-	private SimpleAuthentication(Builder builder) {
-		this.principal = builder.principal;
-		this.credentials = builder.credentials;
-		this.authorities = builder.authorities;
-		this.details = builder.details;
-		this.authenticated = builder.authenticated;
-	}
-
-	@Override
-	public Collection<? extends GrantedAuthority> getAuthorities() {
-		return this.authorities;
-	}
-
-	@Override
-	public @Nullable Object getCredentials() {
-		return this.credentials;
-	}
-
-	@Override
-	public @Nullable Object getDetails() {
-		return this.details;
-	}
-
-	@Override
-	public @Nullable Object getPrincipal() {
-		return this.principal;
-	}
-
-	@Override
-	public boolean isAuthenticated() {
-		return this.authenticated;
-	}
-
-	@Override
-	public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
-		throw new IllegalArgumentException(
-				"Instead of calling this setter, please call toBuilder to create a new instance");
-	}
-
-	@Override
-	public String getName() {
-		return (this.principal == null) ? "" : this.principal.toString();
-	}
-
-	static final class Builder implements Authentication.Builder<Builder> {
-
-		private final Log logger = LogFactory.getLog(getClass());
-
-		private final Collection<GrantedAuthority> authorities = new LinkedHashSet<>();
-
-		private @Nullable Object principal;
-
-		private @Nullable Object credentials;
-
-		private @Nullable Object details;
-
-		private boolean authenticated;
-
-		Builder(Authentication authentication) {
-			this.logger.debug("Creating a builder which will result in exchanging an authentication of type "
-					+ authentication.getClass() + " for " + SimpleAuthentication.class.getSimpleName() + ";"
-					+ " consider implementing " + authentication.getClass().getSimpleName() + "#toBuilder");
-			this.authorities.addAll(authentication.getAuthorities());
-			this.principal = authentication.getPrincipal();
-			this.credentials = authentication.getCredentials();
-			this.details = authentication.getDetails();
-			this.authenticated = authentication.isAuthenticated();
-
-		}
-
-		@Override
-		public Builder authorities(Consumer<Collection<GrantedAuthority>> authorities) {
-			authorities.accept(this.authorities);
-			return this;
-		}
-
-		@Override
-		public Builder details(@Nullable Object details) {
-			this.details = details;
-			return this;
-		}
-
-		@Override
-		public Builder principal(@Nullable Object principal) {
-			this.principal = principal;
-			return this;
-		}
-
-		@Override
-		public Builder credentials(@Nullable Object credentials) {
-			this.credentials = credentials;
-			return this;
-		}
-
-		@Override
-		public Builder authenticated(boolean authenticated) {
-			this.authenticated = authenticated;
-			return this;
-		}
-
-		@Override
-		public Authentication build() {
-			return new SimpleAuthentication(this);
-		}
-
-	}
-
-}

+ 4 - 2
docs/src/test/java/org/springframework/security/docs/servlet/authentication/servletauthenticationauthentication/CopyAuthoritiesTests.java

@@ -8,6 +8,7 @@ import org.springframework.security.authentication.TestingAuthenticationToken;
 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
 import org.springframework.security.authentication.ott.OneTimeTokenAuthentication;
 import org.springframework.security.core.Authentication;
+import org.springframework.security.core.BuildableAuthentication;
 import org.springframework.security.core.authority.AuthorityUtils;
 import org.springframework.security.core.authority.FactorGrantedAuthority;
 import org.springframework.security.core.context.SecurityContextHolder;
@@ -30,8 +31,9 @@ public class CopyAuthoritiesTests {
 		// tag::springSecurity[]
 		Authentication lastestResult = authenticationManager.authenticate(authenticationRequest);
 		Authentication previousResult = SecurityContextHolder.getContext().getAuthentication();
-		if (previousResult != null && previousResult.isAuthenticated()) {
-			lastestResult = lastestResult.toBuilder()
+		if (previousResult != null && previousResult.isAuthenticated() &&
+			lastestResult instanceof BuildableAuthentication buildable) {
+			lastestResult = buildable.toBuilder()
 					.authorities((a) -> a.addAll(previous.getAuthorities()))
 					.build();
 		}

+ 2 - 1
docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/servletauthenticationauthentication/CopyAuthoritiesTests.kt

@@ -10,6 +10,7 @@ import org.springframework.security.authentication.TestingAuthenticationToken
 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
 import org.springframework.security.authentication.ott.OneTimeTokenAuthentication
 import org.springframework.security.core.Authentication
+import org.springframework.security.core.BuildableAuthentication
 import org.springframework.security.core.authority.AuthorityUtils
 import org.springframework.security.core.authority.FactorGrantedAuthority
 import org.springframework.security.core.context.SecurityContextHolder
@@ -28,7 +29,7 @@ class CopyAuthoritiesTests {
         // tag::springSecurity[]
         var latestResult: Authentication = authenticationManager.authenticate(authenticationRequest)
         val previousResult = SecurityContextHolder.getContext().authentication;
-        if (previousResult?.isAuthenticated == true) {
+        if (previousResult?.isAuthenticated == true && latestResult is BuildableAuthentication) {
             latestResult = latestResult.toBuilder().authorities { a ->
                 a.addAll(previousResult.authorities)
             }.build()

+ 2 - 1
oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthenticationToken.java

@@ -22,6 +22,7 @@ import org.jspecify.annotations.Nullable;
 
 import org.springframework.security.authentication.AbstractAuthenticationToken;
 import org.springframework.security.core.Authentication;
+import org.springframework.security.core.BuildableAuthentication;
 import org.springframework.security.core.GrantedAuthority;
 import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
 import org.springframework.security.oauth2.core.user.OAuth2User;
@@ -42,7 +43,7 @@ import org.springframework.util.Assert;
  * @see OAuth2User
  * @see OAuth2AuthorizedClient
  */
-public class OAuth2AuthenticationToken extends AbstractAuthenticationToken {
+public class OAuth2AuthenticationToken extends AbstractAuthenticationToken implements BuildableAuthentication {
 
 	private static final long serialVersionUID = 620L;
 

+ 3 - 1
oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/BearerTokenAuthentication.java

@@ -24,6 +24,7 @@ import java.util.Map;
 import org.jspecify.annotations.Nullable;
 
 import org.springframework.security.core.Authentication;
+import org.springframework.security.core.BuildableAuthentication;
 import org.springframework.security.core.GrantedAuthority;
 import org.springframework.security.core.Transient;
 import org.springframework.security.oauth2.core.OAuth2AccessToken;
@@ -38,7 +39,8 @@ import org.springframework.util.Assert;
  * @since 5.2
  */
 @Transient
-public class BearerTokenAuthentication extends AbstractOAuth2TokenAuthenticationToken<OAuth2AccessToken> {
+public class BearerTokenAuthentication extends AbstractOAuth2TokenAuthenticationToken<OAuth2AccessToken>
+		implements BuildableAuthentication {
 
 	private static final long serialVersionUID = 620L;
 

+ 3 - 1
oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationToken.java

@@ -22,6 +22,7 @@ import java.util.Map;
 import org.jspecify.annotations.Nullable;
 
 import org.springframework.security.core.Authentication;
+import org.springframework.security.core.BuildableAuthentication;
 import org.springframework.security.core.GrantedAuthority;
 import org.springframework.security.core.Transient;
 import org.springframework.security.oauth2.jwt.Jwt;
@@ -37,7 +38,8 @@ import org.springframework.util.Assert;
  * @see Jwt
  */
 @Transient
-public class JwtAuthenticationToken extends AbstractOAuth2TokenAuthenticationToken<Jwt> {
+public class JwtAuthenticationToken extends AbstractOAuth2TokenAuthenticationToken<Jwt>
+		implements BuildableAuthentication {
 
 	private static final long serialVersionUID = 620L;
 

+ 6 - 3
oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/authentication/BearerTokenAuthenticationFilter.java

@@ -30,6 +30,7 @@ import org.springframework.security.authentication.AuthenticationManager;
 import org.springframework.security.authentication.AuthenticationManagerResolver;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.core.BuildableAuthentication;
 import org.springframework.security.core.context.SecurityContext;
 import org.springframework.security.core.context.SecurityContextHolder;
 import org.springframework.security.core.context.SecurityContextHolderStrategy;
@@ -182,9 +183,11 @@ public class BearerTokenAuthenticationFilter extends OncePerRequestFilter {
 			}
 			Authentication current = this.securityContextHolderStrategy.getContext().getAuthentication();
 			if (current != null && current.isAuthenticated()) {
-				authenticationResult = authenticationResult.toBuilder()
-					.authorities((a) -> a.addAll(current.getAuthorities()))
-					.build();
+				if (authenticationResult instanceof BuildableAuthentication buildable) {
+					authenticationResult = buildable.toBuilder()
+						.authorities((a) -> a.addAll(current.getAuthorities()))
+						.build();
+				}
 			}
 			SecurityContext context = this.securityContextHolderStrategy.createEmptyContext();
 			context.setAuthentication(authenticationResult);

+ 2 - 1
saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AssertionAuthentication.java

@@ -21,6 +21,7 @@ import java.util.Collection;
 
 import org.jspecify.annotations.Nullable;
 
+import org.springframework.security.core.BuildableAuthentication;
 import org.springframework.security.core.GrantedAuthority;
 import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
 import org.springframework.util.Assert;
@@ -33,7 +34,7 @@ import org.springframework.util.Assert;
  * @see Saml2ResponseAssertionAccessor
  * @see Saml2ResponseAssertion
  */
-public class Saml2AssertionAuthentication extends Saml2Authentication {
+public class Saml2AssertionAuthentication extends Saml2Authentication implements BuildableAuthentication {
 
 	@Serial
 	private static final long serialVersionUID = -4194323643788693205L;

+ 16 - 13
web/src/main/java/org/springframework/security/web/authentication/AbstractAuthenticationProcessingFilter.java

@@ -41,6 +41,7 @@ import org.springframework.security.authentication.InternalAuthenticationService
 import org.springframework.security.authentication.event.InteractiveAuthenticationSuccessEvent;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.core.BuildableAuthentication;
 import org.springframework.security.core.GrantedAuthority;
 import org.springframework.security.core.SpringSecurityMessageSource;
 import org.springframework.security.core.context.SecurityContext;
@@ -253,20 +254,22 @@ public abstract class AbstractAuthenticationProcessingFilter extends GenericFilt
 			}
 			Authentication current = this.securityContextHolderStrategy.getContext().getAuthentication();
 			if (current != null && current.isAuthenticated()) {
-				authenticationResult = authenticationResult.toBuilder()
-				// @formatter:off
-					.authorities((a) -> {
-						Set<String> newAuthorities = a.stream()
-							.map(GrantedAuthority::getAuthority)
-							.collect(Collectors.toUnmodifiableSet());
-						for (GrantedAuthority currentAuthority : current.getAuthorities()) {
-							if (!newAuthorities.contains(currentAuthority.getAuthority())) {
-								a.add(currentAuthority);
+				if (authenticationResult instanceof BuildableAuthentication buildable) {
+					authenticationResult = buildable.toBuilder()
+					// @formatter:off
+						.authorities((a) -> {
+							Set<String> newAuthorities = a.stream()
+								.map(GrantedAuthority::getAuthority)
+								.collect(Collectors.toUnmodifiableSet());
+							for (GrantedAuthority currentAuthority : current.getAuthorities()) {
+								if (!newAuthorities.contains(currentAuthority.getAuthority())) {
+									a.add(currentAuthority);
+								}
 							}
-						}
-					})
-					.build();
-					// @formatter:on
+						})
+						.build();
+						// @formatter:on
+				}
 			}
 			this.sessionStrategy.onAuthentication(authenticationResult, request, response);
 			// Authentication success

+ 16 - 13
web/src/main/java/org/springframework/security/web/authentication/AuthenticationFilter.java

@@ -33,6 +33,7 @@ import org.springframework.security.authentication.AuthenticationManager;
 import org.springframework.security.authentication.AuthenticationManagerResolver;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.core.BuildableAuthentication;
 import org.springframework.security.core.GrantedAuthority;
 import org.springframework.security.core.context.SecurityContext;
 import org.springframework.security.core.context.SecurityContextHolder;
@@ -189,20 +190,22 @@ public class AuthenticationFilter extends OncePerRequestFilter {
 			}
 			Authentication current = this.securityContextHolderStrategy.getContext().getAuthentication();
 			if (current != null && current.isAuthenticated()) {
-				authenticationResult = authenticationResult.toBuilder()
-				// @formatter:off
-					.authorities((a) -> {
-						Set<String> newAuthorities = a.stream()
-							.map(GrantedAuthority::getAuthority)
-							.collect(Collectors.toUnmodifiableSet());
-						for (GrantedAuthority currentAuthority : current.getAuthorities()) {
-							if (!newAuthorities.contains(currentAuthority.getAuthority())) {
-								a.add(currentAuthority);
+				if (authenticationResult instanceof BuildableAuthentication buildable) {
+					authenticationResult = buildable.toBuilder()
+					// @formatter:off
+						.authorities((a) -> {
+							Set<String> newAuthorities = a.stream()
+								.map(GrantedAuthority::getAuthority)
+								.collect(Collectors.toUnmodifiableSet());
+							for (GrantedAuthority currentAuthority : current.getAuthorities()) {
+								if (!newAuthorities.contains(currentAuthority.getAuthority())) {
+									a.add(currentAuthority);
+								}
 							}
-						}
-					})
-					.build();
-					// @formatter:on
+						})
+						.build();
+						// @formatter:on
+				}
 			}
 			HttpSession session = request.getSession(false);
 			if (session != null) {

+ 16 - 13
web/src/main/java/org/springframework/security/web/authentication/preauth/AbstractPreAuthenticatedProcessingFilter.java

@@ -37,6 +37,7 @@ import org.springframework.security.authentication.AuthenticationManager;
 import org.springframework.security.authentication.event.InteractiveAuthenticationSuccessEvent;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.core.BuildableAuthentication;
 import org.springframework.security.core.GrantedAuthority;
 import org.springframework.security.core.context.SecurityContext;
 import org.springframework.security.core.context.SecurityContextHolder;
@@ -209,20 +210,22 @@ public abstract class AbstractPreAuthenticatedProcessingFilter extends GenericFi
 			Authentication authenticationResult = this.authenticationManager.authenticate(authenticationRequest);
 			Authentication current = this.securityContextHolderStrategy.getContext().getAuthentication();
 			if (current != null && current.isAuthenticated()) {
-				authenticationResult = authenticationResult.toBuilder()
-				// @formatter:off
-					.authorities((a) -> {
-						Set<String> newAuthorities = a.stream()
-								.map(GrantedAuthority::getAuthority)
-								.collect(Collectors.toUnmodifiableSet());
-						for (GrantedAuthority currentAuthority : current.getAuthorities()) {
-							if (!newAuthorities.contains(currentAuthority.getAuthority())) {
-								a.add(currentAuthority);
+				if (authenticationResult instanceof BuildableAuthentication buildable) {
+					authenticationResult = buildable.toBuilder()
+					// @formatter:off
+						.authorities((a) -> {
+							Set<String> newAuthorities = a.stream()
+									.map(GrantedAuthority::getAuthority)
+									.collect(Collectors.toUnmodifiableSet());
+							for (GrantedAuthority currentAuthority : current.getAuthorities()) {
+								if (!newAuthorities.contains(currentAuthority.getAuthority())) {
+									a.add(currentAuthority);
+								}
 							}
-						}
-					})
-					.build();
-					// @formatter:on
+						})
+						.build();
+						// @formatter:on
+				}
 			}
 			successfulAuthentication(request, response, authenticationResult);
 		}

+ 3 - 1
web/src/main/java/org/springframework/security/web/authentication/preauth/PreAuthenticatedAuthenticationToken.java

@@ -21,6 +21,7 @@ import java.util.Collection;
 import org.jspecify.annotations.Nullable;
 
 import org.springframework.security.authentication.AbstractAuthenticationToken;
+import org.springframework.security.core.BuildableAuthentication;
 import org.springframework.security.core.GrantedAuthority;
 import org.springframework.util.Assert;
 
@@ -31,7 +32,8 @@ import org.springframework.util.Assert;
  * @author Ruud Senden
  * @since 2.0
  */
-public class PreAuthenticatedAuthenticationToken extends AbstractAuthenticationToken {
+public class PreAuthenticatedAuthenticationToken extends AbstractAuthenticationToken
+		implements BuildableAuthentication {
 
 	private static final long serialVersionUID = 620L;
 

+ 16 - 13
web/src/main/java/org/springframework/security/web/authentication/www/BasicAuthenticationFilter.java

@@ -33,6 +33,7 @@ import org.springframework.security.authentication.AuthenticationDetailsSource;
 import org.springframework.security.authentication.AuthenticationManager;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.core.BuildableAuthentication;
 import org.springframework.security.core.GrantedAuthority;
 import org.springframework.security.core.context.SecurityContext;
 import org.springframework.security.core.context.SecurityContextHolder;
@@ -191,20 +192,22 @@ public class BasicAuthenticationFilter extends OncePerRequestFilter {
 				Authentication authResult = this.authenticationManager.authenticate(authRequest);
 				Authentication current = this.securityContextHolderStrategy.getContext().getAuthentication();
 				if (current != null && current.isAuthenticated()) {
-					authResult = authResult.toBuilder()
-					// @formatter:off
-						.authorities((a) -> {
-							Set<String> newAuthorities = a.stream()
-								.map(GrantedAuthority::getAuthority)
-								.collect(Collectors.toUnmodifiableSet());
-							for (GrantedAuthority currentAuthority : current.getAuthorities()) {
-								if (!newAuthorities.contains(currentAuthority.getAuthority())) {
-									a.add(currentAuthority);
+					if (authResult instanceof BuildableAuthentication buildable) {
+						authResult = buildable.toBuilder()
+						// @formatter:off
+							.authorities((a) -> {
+								Set<String> newAuthorities = a.stream()
+									.map(GrantedAuthority::getAuthority)
+									.collect(Collectors.toUnmodifiableSet());
+								for (GrantedAuthority currentAuthority : current.getAuthorities()) {
+									if (!newAuthorities.contains(currentAuthority.getAuthority())) {
+										a.add(currentAuthority);
+									}
 								}
-							}
-						})
-						.build();
-						// @formatter:on
+							})
+							.build();
+							// @formatter:on
+					}
 				}
 				SecurityContext context = this.securityContextHolderStrategy.createEmptyContext();
 				context.setAuthentication(authResult);

+ 5 - 1
web/src/main/java/org/springframework/security/web/server/authentication/AuthenticationWebFilter.java

@@ -29,6 +29,7 @@ import org.springframework.security.authentication.ReactiveAuthenticationManager
 import org.springframework.security.authentication.ReactiveAuthenticationManagerResolver;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.core.BuildableAuthentication;
 import org.springframework.security.core.GrantedAuthority;
 import org.springframework.security.core.context.ReactiveSecurityContextHolder;
 import org.springframework.security.core.context.SecurityContextImpl;
@@ -141,7 +142,10 @@ public class AuthenticationWebFilter implements WebFilter {
 			if (!current.isAuthenticated()) {
 				return result;
 			}
-			return result.toBuilder()
+			if (!(result instanceof BuildableAuthentication buildable)) {
+				return result;
+			}
+			return buildable.toBuilder()
 			// @formatter:off
 				.authorities((a) -> {
 					Set<String> newAuthorities = a.stream()

+ 2 - 1
webauthn/src/main/java/org/springframework/security/web/webauthn/authentication/WebAuthnAuthentication.java

@@ -22,6 +22,7 @@ import java.util.Collection;
 import org.jspecify.annotations.Nullable;
 
 import org.springframework.security.authentication.AbstractAuthenticationToken;
+import org.springframework.security.core.BuildableAuthentication;
 import org.springframework.security.core.GrantedAuthority;
 import org.springframework.security.web.webauthn.api.PublicKeyCredentialUserEntity;
 import org.springframework.util.Assert;
@@ -34,7 +35,7 @@ import org.springframework.util.Assert;
  * @since 6.4
  * @see WebAuthnAuthenticationRequestToken
  */
-public class WebAuthnAuthentication extends AbstractAuthenticationToken {
+public class WebAuthnAuthentication extends AbstractAuthenticationToken implements BuildableAuthentication {
 
 	@Serial
 	private static final long serialVersionUID = -4879907158750659197L;