2
0
Эх сурвалжийг харах

Merge remote-tracking branch 'jzheaux/authentication-builder'

Issue gh-17861
Issue gh-17862
Josh Cummings 2 сар өмнө
parent
commit
6689798257
53 өөрчлөгдсөн 1739 нэмэгдсэн , 26 устгасан
  1. 96 0
      cas/src/main/java/org/springframework/security/cas/authentication/CasAuthenticationToken.java
  2. 49 1
      cas/src/main/java/org/springframework/security/cas/authentication/CasServiceTicketAuthenticationToken.java
  3. 27 0
      cas/src/test/java/org/springframework/security/cas/authentication/CasAuthenticationTokenTests.java
  4. 4 1
      config/src/test/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerTransientAuthenticationTests.java
  5. 4 1
      config/src/test/java/org/springframework/security/config/http/SessionManagementConfigTransientAuthenticationTests.java
  6. 56 0
      core/src/main/java/org/springframework/security/authentication/AbstractAuthenticationToken.java
  7. 0 1
      core/src/main/java/org/springframework/security/authentication/DelegatingReactiveAuthenticationManager.java
  8. 8 4
      core/src/main/java/org/springframework/security/authentication/ProviderManager.java
  9. 55 0
      core/src/main/java/org/springframework/security/authentication/RememberMeAuthenticationToken.java
  10. 53 1
      core/src/main/java/org/springframework/security/authentication/TestingAuthenticationToken.java
  11. 49 1
      core/src/main/java/org/springframework/security/authentication/UsernamePasswordAuthenticationToken.java
  12. 41 0
      core/src/main/java/org/springframework/security/authentication/jaas/JaasAuthenticationToken.java
  13. 43 0
      core/src/main/java/org/springframework/security/authentication/ott/OneTimeTokenAuthentication.java
  14. 107 0
      core/src/main/java/org/springframework/security/core/Authentication.java
  15. 151 0
      core/src/main/java/org/springframework/security/core/SimpleAuthentication.java
  16. 54 0
      core/src/test/java/org/springframework/security/authentication/AbstractAuthenticationBuilderTests.java
  17. 11 8
      core/src/test/java/org/springframework/security/authentication/ProviderManagerTests.java
  18. 19 0
      core/src/test/java/org/springframework/security/authentication/TestingAuthenticationTokenTests.java
  19. 19 0
      core/src/test/java/org/springframework/security/authentication/UsernamePasswordAuthenticationTokenTests.java
  20. 51 0
      core/src/test/java/org/springframework/security/authentication/jaas/JaasAuthenticationTokenTests.java
  21. 44 0
      core/src/test/java/org/springframework/security/authentication/ott/OneTimeTokenAuthenticationTests.java
  22. 19 0
      core/src/test/java/org/springframework/security/authentication/rememberme/RememberMeAuthenticationTokenTests.java
  23. 7 2
      docs/modules/ROOT/pages/servlet/authentication/architecture.adoc
  24. 2 0
      docs/modules/ROOT/pages/servlet/authentication/passwords/basic.adoc
  25. 2 0
      docs/modules/ROOT/pages/servlet/oauth2/resource-server/index.adoc
  26. 1 0
      docs/modules/ROOT/pages/whats-new.adoc
  27. 41 0
      docs/src/test/java/org/springframework/security/docs/servlet/authentication/servletauthenticationauthentication/CopyAuthoritiesTests.java
  28. 39 0
      docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/servletauthenticationauthentication/CopyAuthoritiesTests.kt
  29. 3 1
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/OAuth2AuthorizeRequest.java
  30. 59 0
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthenticationToken.java
  31. 3 1
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunction.java
  32. 20 0
      oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthenticationTokenTests.java
  33. 60 0
      oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/AbstractOAuth2TokenAuthenticationToken.java
  34. 75 0
      oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/BearerTokenAuthentication.java
  35. 79 0
      oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationToken.java
  36. 6 0
      oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/authentication/BearerTokenAuthenticationFilter.java
  37. 22 0
      oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/BearerTokenAuthenticationTests.java
  38. 19 1
      oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationTokenTests.java
  39. 23 0
      oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/authentication/BearerTokenAuthenticationFilterTests.java
  40. 65 0
      saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AssertionAuthentication.java
  41. 33 0
      saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2Authentication.java
  42. 49 0
      saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/Saml2AssertionAuthenticationTests.java
  43. 6 0
      web/src/main/java/org/springframework/security/web/authentication/AbstractAuthenticationProcessingFilter.java
  44. 6 0
      web/src/main/java/org/springframework/security/web/authentication/AuthenticationFilter.java
  45. 6 0
      web/src/main/java/org/springframework/security/web/authentication/preauth/AbstractPreAuthenticatedProcessingFilter.java
  46. 50 1
      web/src/main/java/org/springframework/security/web/authentication/preauth/PreAuthenticatedAuthenticationToken.java
  47. 4 0
      web/src/main/java/org/springframework/security/web/authentication/www/BasicAuthenticationFilter.java
  48. 14 0
      web/src/main/java/org/springframework/security/web/server/authentication/AuthenticationWebFilter.java
  49. 1 0
      web/src/test/java/org/springframework/security/web/authentication/AuthenticationFilterTests.java
  50. 18 0
      web/src/test/java/org/springframework/security/web/authentication/preauth/PreAuthenticatedAuthenticationTokenTests.java
  51. 4 2
      web/src/test/java/org/springframework/security/web/context/HttpSessionSecurityContextRepositoryTests.java
  52. 44 0
      webauthn/src/main/java/org/springframework/security/web/webauthn/authentication/WebAuthnAuthentication.java
  53. 18 0
      webauthn/src/test/java/org/springframework/security/web/webauthn/authentication/WebAuthnAuthenticationTests.java

+ 96 - 0
cas/src/main/java/org/springframework/security/cas/authentication/CasAuthenticationToken.java

@@ -20,6 +20,7 @@ import java.io.Serializable;
 import java.util.Collection;
 
 import org.apereo.cas.client.validation.Assertion;
+import org.jspecify.annotations.Nullable;
 
 import org.springframework.security.authentication.AbstractAuthenticationToken;
 import org.springframework.security.core.GrantedAuthority;
@@ -104,6 +105,19 @@ public class CasAuthenticationToken extends AbstractAuthenticationToken implemen
 		setAuthenticated(true);
 	}
 
+	protected CasAuthenticationToken(Builder<?> builder) {
+		super(builder);
+		Assert.isTrue(!"".equals(builder.principal), "principal cannot be null or empty");
+		Assert.notNull(!"".equals(builder.credentials), "credentials cannot be null or empty");
+		Assert.notNull(builder.userDetails, "userDetails cannot be null");
+		Assert.notNull(builder.assertion, "assertion cannot be null");
+		this.keyHash = builder.keyHash;
+		this.principal = builder.principal;
+		this.credentials = builder.credentials;
+		this.userDetails = builder.userDetails;
+		this.assertion = builder.assertion;
+	}
+
 	private static Integer extractKeyHash(String key) {
 		Assert.hasLength(key, "key cannot be null or empty");
 		return key.hashCode();
@@ -153,6 +167,11 @@ public class CasAuthenticationToken extends AbstractAuthenticationToken implemen
 		return this.userDetails;
 	}
 
+	@Override
+	public Builder<?> toBuilder() {
+		return new Builder<>(this);
+	}
+
 	@Override
 	public String toString() {
 		StringBuilder sb = new StringBuilder();
@@ -162,4 +181,81 @@ public class CasAuthenticationToken extends AbstractAuthenticationToken implemen
 		return (sb.toString());
 	}
 
+	/**
+	 * A builder of {@link CasAuthenticationToken} instances
+	 *
+	 * @since 7.0
+	 */
+	public static class Builder<B extends Builder<B>> extends AbstractAuthenticationBuilder<B> {
+
+		private Integer keyHash;
+
+		private Object principal;
+
+		private Object credentials;
+
+		private UserDetails userDetails;
+
+		private Assertion assertion;
+
+		protected Builder(CasAuthenticationToken token) {
+			super(token);
+			this.keyHash = token.keyHash;
+			this.principal = token.principal;
+			this.credentials = token.credentials;
+			this.userDetails = token.userDetails;
+			this.assertion = token.assertion;
+		}
+
+		/**
+		 * Use this key
+		 * @param key the key to use
+		 * @return the {@link Builder} for further configurations
+		 */
+		public B key(String key) {
+			this.keyHash = key.hashCode();
+			return (B) this;
+		}
+
+		@Override
+		public B principal(@Nullable Object principal) {
+			Assert.notNull(principal, "principal cannot be null");
+			this.principal = principal;
+			return (B) this;
+		}
+
+		@Override
+		public B credentials(@Nullable Object credentials) {
+			Assert.notNull(credentials, "credentials cannot be null");
+			this.credentials = credentials;
+			return (B) this;
+		}
+
+		/**
+		 * Use this {@link UserDetails}
+		 * @param userDetails the {@link UserDetails} to use
+		 * @return the {@link Builder} for further configurations
+		 */
+		public B userDetails(UserDetails userDetails) {
+			this.userDetails = userDetails;
+			return (B) this;
+		}
+
+		/**
+		 * Use this {@link Assertion}
+		 * @param assertion the {@link Assertion} to use
+		 * @return the {@link Builder} for further configurations
+		 */
+		public B assertion(Assertion assertion) {
+			this.assertion = assertion;
+			return (B) this;
+		}
+
+		@Override
+		public CasAuthenticationToken build() {
+			return new CasAuthenticationToken(this);
+		}
+
+	}
+
 }

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

@@ -52,7 +52,7 @@ public class CasServiceTicketAuthenticationToken extends AbstractAuthenticationT
 	 *
 	 */
 	public CasServiceTicketAuthenticationToken(String identifier, Object credentials) {
-		super(null);
+		super((Collection<? extends GrantedAuthority>) null);
 		this.identifier = identifier;
 		this.credentials = credentials;
 		setAuthenticated(false);
@@ -75,6 +75,12 @@ public class CasServiceTicketAuthenticationToken extends AbstractAuthenticationT
 		super.setAuthenticated(true);
 	}
 
+	protected CasServiceTicketAuthenticationToken(Builder<?> builder) {
+		super(builder);
+		this.identifier = builder.principal;
+		this.credentials = builder.credentials;
+	}
+
 	public static CasServiceTicketAuthenticationToken stateful(Object credentials) {
 		return new CasServiceTicketAuthenticationToken(CAS_STATEFUL_IDENTIFIER, credentials);
 	}
@@ -110,4 +116,46 @@ public class CasServiceTicketAuthenticationToken extends AbstractAuthenticationT
 		this.credentials = null;
 	}
 
+	public Builder<?> toBuilder() {
+		return new Builder<>(this);
+	}
+
+	/**
+	 * A builder of {@link CasServiceTicketAuthenticationToken} instances
+	 *
+	 * @since 7.0
+	 */
+	public static class Builder<B extends Builder<B>> extends AbstractAuthenticationBuilder<B> {
+
+		private String principal;
+
+		private @Nullable Object credentials;
+
+		protected Builder(CasServiceTicketAuthenticationToken token) {
+			super(token);
+			this.principal = token.identifier;
+			this.credentials = token.credentials;
+		}
+
+		@Override
+		public B principal(@Nullable Object principal) {
+			Assert.isInstanceOf(String.class, principal, "principal must be of type String");
+			this.principal = (String) principal;
+			return (B) this;
+		}
+
+		@Override
+		public B credentials(@Nullable Object credentials) {
+			Assert.notNull(credentials, "credentials cannot be null");
+			this.credentials = credentials;
+			return (B) this;
+		}
+
+		@Override
+		public CasServiceTicketAuthenticationToken build() {
+			return new CasServiceTicketAuthenticationToken(this);
+		}
+
+	}
+
 }

+ 27 - 0
cas/src/test/java/org/springframework/security/cas/authentication/CasAuthenticationTokenTests.java

@@ -18,6 +18,7 @@ package org.springframework.security.cas.authentication;
 
 import java.util.Collections;
 import java.util.List;
+import java.util.Set;
 
 import org.apereo.cas.client.validation.Assertion;
 import org.apereo.cas.client.validation.AssertionImpl;
@@ -26,6 +27,7 @@ import org.junit.jupiter.api.Test;
 import org.springframework.security.core.GrantedAuthority;
 import org.springframework.security.core.authority.AuthorityUtils;
 import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.userdetails.PasswordEncodedUser;
 import org.springframework.security.core.userdetails.User;
 import org.springframework.security.core.userdetails.UserDetails;
 
@@ -155,4 +157,29 @@ public class CasAuthenticationTokenTests {
 		assertThat(result.lastIndexOf("Credentials (Service/Proxy Ticket):") != -1).isTrue();
 	}
 
+	@Test
+	public void toBuilderWhenApplyThenCopies() {
+		Assertion assertionOne = new AssertionImpl("test");
+		CasAuthenticationToken factorOne = new CasAuthenticationToken("key", "alice", "pass",
+				AuthorityUtils.createAuthorityList("FACTOR_ONE"), PasswordEncodedUser.user(), assertionOne);
+		Assertion assertionTwo = new AssertionImpl("test");
+		CasAuthenticationToken factorTwo = new CasAuthenticationToken("yek", "bob", "ssap",
+				AuthorityUtils.createAuthorityList("FACTOR_TWO"), PasswordEncodedUser.admin(), assertionTwo);
+		CasAuthenticationToken authentication = factorOne.toBuilder()
+			.authorities((a) -> a.addAll(factorTwo.getAuthorities()))
+			.key("yek")
+			.principal(factorTwo.getPrincipal())
+			.credentials(factorTwo.getCredentials())
+			.userDetails(factorTwo.getUserDetails())
+			.assertion(factorTwo.getAssertion())
+			.build();
+		Set<String> authorities = AuthorityUtils.authorityListToSet(authentication.getAuthorities());
+		assertThat(authentication.getKeyHash()).isEqualTo(factorTwo.getKeyHash());
+		assertThat(authentication.getPrincipal()).isEqualTo(factorTwo.getPrincipal());
+		assertThat(authentication.getCredentials()).isEqualTo(factorTwo.getCredentials());
+		assertThat(authentication.getUserDetails()).isEqualTo(factorTwo.getUserDetails());
+		assertThat(authentication.getAssertion()).isEqualTo(factorTwo.getAssertion());
+		assertThat(authorities).containsExactlyInAnyOrder("FACTOR_ONE", "FACTOR_TWO");
+	}
+
 }

+ 4 - 1
config/src/test/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerTransientAuthenticationTests.java

@@ -16,6 +16,8 @@
 
 package org.springframework.security.config.annotation.web.configurers;
 
+import java.util.Collection;
+
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
 
@@ -31,6 +33,7 @@ import org.springframework.security.config.test.SpringTestContext;
 import org.springframework.security.config.test.SpringTestContextExtension;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.core.GrantedAuthority;
 import org.springframework.security.core.Transient;
 import org.springframework.security.web.SecurityFilterChain;
 import org.springframework.test.web.servlet.MockMvc;
@@ -113,7 +116,7 @@ public class SessionManagementConfigurerTransientAuthenticationTests {
 	static class SomeTransientAuthentication extends AbstractAuthenticationToken {
 
 		SomeTransientAuthentication() {
-			super(null);
+			super((Collection<? extends GrantedAuthority>) null);
 		}
 
 		@Override

+ 4 - 1
config/src/test/java/org/springframework/security/config/http/SessionManagementConfigTransientAuthenticationTests.java

@@ -16,6 +16,8 @@
 
 package org.springframework.security.config.http;
 
+import java.util.Collection;
+
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
 
@@ -26,6 +28,7 @@ import org.springframework.security.config.test.SpringTestContext;
 import org.springframework.security.config.test.SpringTestContextExtension;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.core.GrantedAuthority;
 import org.springframework.security.core.Transient;
 import org.springframework.test.web.servlet.MockMvc;
 import org.springframework.test.web.servlet.MvcResult;
@@ -82,7 +85,7 @@ public class SessionManagementConfigTransientAuthenticationTests {
 	static class SomeTransientAuthentication extends AbstractAuthenticationToken {
 
 		SomeTransientAuthentication() {
-			super(null);
+			super((Collection<? extends GrantedAuthority>) null);
 		}
 
 		@Override

+ 56 - 0
core/src/main/java/org/springframework/security/authentication/AbstractAuthenticationToken.java

@@ -16,10 +16,13 @@
 
 package org.springframework.security.authentication;
 
+import java.io.Serial;
 import java.security.Principal;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.function.Consumer;
 
 import org.jspecify.annotations.Nullable;
 
@@ -41,6 +44,9 @@ import org.springframework.util.Assert;
  */
 public abstract class AbstractAuthenticationToken implements Authentication, CredentialsContainer {
 
+	@Serial
+	private static final long serialVersionUID = -3194696462184782834L;
+
 	private final Collection<GrantedAuthority> authorities;
 
 	private @Nullable Object details;
@@ -63,6 +69,12 @@ public abstract class AbstractAuthenticationToken implements Authentication, Cre
 		this.authorities = Collections.unmodifiableList(new ArrayList<>(authorities));
 	}
 
+	protected AbstractAuthenticationToken(AbstractAuthenticationBuilder<?> builder) {
+		this(builder.authorities);
+		this.authenticated = builder.authenticated;
+		this.details = builder.details;
+	}
+
 	@Override
 	public Collection<GrantedAuthority> getAuthorities() {
 		return this.authorities;
@@ -185,4 +197,48 @@ public abstract class AbstractAuthenticationToken implements Authentication, Cre
 		return sb.toString();
 	}
 
+	/**
+	 * A common abstract implementation of {@link Authentication.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> {
+
+		private boolean authenticated;
+
+		private @Nullable Object details;
+
+		private final Collection<GrantedAuthority> authorities;
+
+		protected AbstractAuthenticationBuilder(AbstractAuthenticationToken token) {
+			this.authorities = new LinkedHashSet<>(token.getAuthorities());
+			this.authenticated = token.isAuthenticated();
+			this.details = token.getDetails();
+		}
+
+		@Override
+		public B authenticated(boolean authenticated) {
+			this.authenticated = authenticated;
+			return (B) this;
+		}
+
+		@Override
+		public B details(@Nullable Object details) {
+			this.details = details;
+			return (B) this;
+		}
+
+		@Override
+		public B authorities(Consumer<Collection<GrantedAuthority>> authorities) {
+			authorities.accept(this.authorities);
+			this.authenticated = true;
+			return (B) this;
+		}
+
+	}
+
 }

+ 0 - 1
core/src/main/java/org/springframework/security/authentication/DelegatingReactiveAuthenticationManager.java

@@ -61,7 +61,6 @@ public class DelegatingReactiveAuthenticationManager implements ReactiveAuthenti
 		Function<ReactiveAuthenticationManager, Mono<Authentication>> logging = (m) -> m.authenticate(authentication)
 			.doOnError(AuthenticationException.class, (ex) -> ex.setAuthenticationRequest(authentication))
 			.doOnError(this.logger::debug);
-
 		return ((this.continueOnError) ? result.concatMapDelayError(logging) : result.concatMap(logging)).next();
 	}
 

+ 8 - 4
core/src/main/java/org/springframework/security/authentication/ProviderManager.java

@@ -182,7 +182,7 @@ public class ProviderManager implements AuthenticationManager, MessageSourceAwar
 			try {
 				result = provider.authenticate(authentication);
 				if (result != null) {
-					copyDetails(authentication, result);
+					result = copyDetails(authentication, result);
 					break;
 				}
 			}
@@ -277,10 +277,14 @@ public class ProviderManager implements AuthenticationManager, MessageSourceAwar
 	 * @param source source authentication
 	 * @param dest the destination authentication object
 	 */
-	private void copyDetails(Authentication source, Authentication dest) {
-		if ((dest instanceof AbstractAuthenticationToken token) && (dest.getDetails() == null)) {
-			token.setDetails(source.getDetails());
+	private Authentication copyDetails(Authentication source, Authentication dest) {
+		if (source.getDetails() == null) {
+			return dest;
 		}
+		if (dest.getDetails() != null) {
+			return dest;
+		}
+		return dest.toBuilder().details(source.getDetails()).build();
 	}
 
 	public List<AuthenticationProvider> getProviders() {

+ 55 - 0
core/src/main/java/org/springframework/security/authentication/RememberMeAuthenticationToken.java

@@ -18,7 +18,10 @@ package org.springframework.security.authentication;
 
 import java.util.Collection;
 
+import org.jspecify.annotations.Nullable;
+
 import org.springframework.security.core.GrantedAuthority;
+import org.springframework.util.Assert;
 
 /**
  * Represents a remembered <code>Authentication</code>.
@@ -70,6 +73,12 @@ public class RememberMeAuthenticationToken extends AbstractAuthenticationToken {
 		setAuthenticated(true);
 	}
 
+	protected RememberMeAuthenticationToken(Builder<?> builder) {
+		super(builder);
+		this.keyHash = builder.keyHash;
+		this.principal = builder.principal;
+	}
+
 	/**
 	 * Always returns an empty <code>String</code>
 	 * @return an empty String
@@ -88,6 +97,11 @@ public class RememberMeAuthenticationToken extends AbstractAuthenticationToken {
 		return this.principal;
 	}
 
+	@Override
+	public Builder<?> toBuilder() {
+		return new Builder<>(this);
+	}
+
 	@Override
 	public boolean equals(Object obj) {
 		if (!super.equals(obj)) {
@@ -106,4 +120,45 @@ public class RememberMeAuthenticationToken extends AbstractAuthenticationToken {
 		return result;
 	}
 
+	/**
+	 * A builder of {@link RememberMeAuthenticationToken} instances
+	 *
+	 * @since 7.0
+	 */
+	public static class Builder<B extends Builder<B>> extends AbstractAuthenticationBuilder<B> {
+
+		private Integer keyHash;
+
+		private Object principal;
+
+		protected Builder(RememberMeAuthenticationToken token) {
+			super(token);
+			this.keyHash = token.getKeyHash();
+			this.principal = token.getPrincipal();
+		}
+
+		@Override
+		public B principal(@Nullable Object principal) {
+			Assert.notNull(principal, "principal cannot be null");
+			this.principal = principal;
+			return (B) this;
+		}
+
+		/**
+		 * Use this key
+		 * @param key the key to use
+		 * @return the {@link Builder} for further configurations
+		 */
+		public B key(String key) {
+			this.keyHash = key.hashCode();
+			return (B) this;
+		}
+
+		@Override
+		public RememberMeAuthenticationToken build() {
+			return new RememberMeAuthenticationToken(this);
+		}
+
+	}
+
 }

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

@@ -19,8 +19,11 @@ package org.springframework.security.authentication;
 import java.util.Collection;
 import java.util.List;
 
+import org.jspecify.annotations.Nullable;
+
 import org.springframework.security.core.GrantedAuthority;
 import org.springframework.security.core.authority.AuthorityUtils;
+import org.springframework.util.Assert;
 
 /**
  * An {@link org.springframework.security.core.Authentication} implementation that is
@@ -39,7 +42,7 @@ public class TestingAuthenticationToken extends AbstractAuthenticationToken {
 	private final Object principal;
 
 	public TestingAuthenticationToken(Object principal, Object credentials) {
-		super(null);
+		super((Collection<? extends GrantedAuthority>) null);
 		this.principal = principal;
 		this.credentials = credentials;
 	}
@@ -61,6 +64,12 @@ public class TestingAuthenticationToken extends AbstractAuthenticationToken {
 		setAuthenticated(true);
 	}
 
+	protected TestingAuthenticationToken(Builder<?> builder) {
+		super(builder);
+		this.principal = builder.principal;
+		this.credentials = builder.credentials;
+	}
+
 	@Override
 	public Object getCredentials() {
 		return this.credentials;
@@ -71,4 +80,47 @@ public class TestingAuthenticationToken extends AbstractAuthenticationToken {
 		return this.principal;
 	}
 
+	@Override
+	public Builder<?> toBuilder() {
+		return new Builder<>(this);
+	}
+
+	/**
+	 * A builder of {@link TestingAuthenticationToken} instances
+	 *
+	 * @since 7.0
+	 */
+	public static class Builder<B extends Builder<B>> extends AbstractAuthenticationBuilder<B> {
+
+		private Object principal;
+
+		private Object credentials;
+
+		protected Builder(TestingAuthenticationToken token) {
+			super(token);
+			this.principal = token.principal;
+			this.credentials = token.credentials;
+		}
+
+		@Override
+		public B principal(@Nullable Object principal) {
+			Assert.notNull(principal, "principal cannot be null");
+			this.principal = principal;
+			return (B) this;
+		}
+
+		@Override
+		public B credentials(@Nullable Object credentials) {
+			Assert.notNull(credentials, "credentials cannot be null");
+			this.credentials = credentials;
+			return (B) this;
+		}
+
+		@Override
+		public TestingAuthenticationToken build() {
+			return new TestingAuthenticationToken(this);
+		}
+
+	}
+
 }

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

@@ -50,7 +50,7 @@ public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationT
 	 *
 	 */
 	public UsernamePasswordAuthenticationToken(@Nullable Object principal, @Nullable Object credentials) {
-		super(null);
+		super((Collection<? extends GrantedAuthority>) null);
 		this.principal = principal;
 		this.credentials = credentials;
 		setAuthenticated(false);
@@ -73,6 +73,12 @@ public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationT
 		super.setAuthenticated(true); // must use super, as we override
 	}
 
+	protected UsernamePasswordAuthenticationToken(Builder<?> builder) {
+		super(builder);
+		this.principal = builder.principal;
+		this.credentials = builder.credentials;
+	}
+
 	/**
 	 * This factory method can be safely used by any code that wishes to create a
 	 * unauthenticated <code>UsernamePasswordAuthenticationToken</code>.
@@ -124,4 +130,46 @@ public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationT
 		this.credentials = null;
 	}
 
+	@Override
+	public Builder<?> toBuilder() {
+		return new Builder<>(this);
+	}
+
+	/**
+	 * A builder of {@link UsernamePasswordAuthenticationToken} instances
+	 *
+	 * @since 7.0
+	 */
+	public static class Builder<B extends Builder<B>> extends AbstractAuthenticationBuilder<B> {
+
+		private @Nullable Object principal;
+
+		private @Nullable Object credentials;
+
+		protected Builder(UsernamePasswordAuthenticationToken token) {
+			super(token);
+			this.principal = token.principal;
+			this.credentials = token.credentials;
+		}
+
+		@Override
+		public B principal(@Nullable Object principal) {
+			Assert.notNull(principal, "principal cannot be null");
+			this.principal = principal;
+			return (B) this;
+		}
+
+		@Override
+		public B credentials(@Nullable Object credentials) {
+			this.credentials = credentials;
+			return (B) this;
+		}
+
+		@Override
+		public UsernamePasswordAuthenticationToken build() {
+			return new UsernamePasswordAuthenticationToken(this);
+		}
+
+	}
+
 }

+ 41 - 0
core/src/main/java/org/springframework/security/authentication/jaas/JaasAuthenticationToken.java

@@ -48,8 +48,49 @@ public class JaasAuthenticationToken extends UsernamePasswordAuthenticationToken
 		this.loginContext = loginContext;
 	}
 
+	protected JaasAuthenticationToken(Builder<?> builder) {
+		super(builder);
+		this.loginContext = builder.loginContext;
+	}
+
 	public LoginContext getLoginContext() {
 		return this.loginContext;
 	}
 
+	@Override
+	public Builder<?> toBuilder() {
+		return new Builder<>(this);
+	}
+
+	/**
+	 * A builder of {@link JaasAuthenticationToken} instances
+	 *
+	 * @since 7.0
+	 */
+	public static class Builder<B extends Builder<B>> extends UsernamePasswordAuthenticationToken.Builder<B> {
+
+		private LoginContext loginContext;
+
+		protected Builder(JaasAuthenticationToken token) {
+			super(token);
+			this.loginContext = token.getLoginContext();
+		}
+
+		/**
+		 * Use this {@link LoginContext}
+		 * @param loginContext the {@link LoginContext} to use
+		 * @return the {@link Builder} for further configurations
+		 */
+		public B loginContext(LoginContext loginContext) {
+			this.loginContext = loginContext;
+			return (B) this;
+		}
+
+		@Override
+		public JaasAuthenticationToken build() {
+			return new JaasAuthenticationToken(this);
+		}
+
+	}
+
 }

+ 43 - 0
core/src/main/java/org/springframework/security/authentication/ott/OneTimeTokenAuthentication.java

@@ -23,6 +23,7 @@ import org.jspecify.annotations.Nullable;
 
 import org.springframework.security.authentication.AbstractAuthenticationToken;
 import org.springframework.security.core.GrantedAuthority;
+import org.springframework.util.Assert;
 
 /**
  * The result of a successful one-time-token authentication
@@ -43,6 +44,11 @@ public class OneTimeTokenAuthentication extends AbstractAuthenticationToken {
 		setAuthenticated(true);
 	}
 
+	protected OneTimeTokenAuthentication(Builder<?> builder) {
+		super(builder);
+		this.principal = builder.principal;
+	}
+
 	@Override
 	public Object getPrincipal() {
 		return this.principal;
@@ -53,4 +59,41 @@ public class OneTimeTokenAuthentication extends AbstractAuthenticationToken {
 		return null;
 	}
 
+	@Override
+	public Builder<?> toBuilder() {
+		return new Builder<>(this);
+	}
+
+	/**
+	 * A builder of {@link OneTimeTokenAuthentication} instances
+	 *
+	 * @since 7.0
+	 */
+	public static class Builder<B extends Builder<B>> extends AbstractAuthenticationBuilder<B> {
+
+		private Object principal;
+
+		protected Builder(OneTimeTokenAuthentication token) {
+			super(token);
+			this.principal = token.principal;
+		}
+
+		/**
+		 * Use this principal.
+		 * @return the {@link Builder} for further configuration
+		 */
+		@Override
+		public B principal(@Nullable Object principal) {
+			Assert.notNull(principal, "principal cannot be null");
+			this.principal = principal;
+			return (B) this;
+		}
+
+		@Override
+		public OneTimeTokenAuthentication build() {
+			return new OneTimeTokenAuthentication(this);
+		}
+
+	}
+
 }

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

@@ -19,6 +19,7 @@ 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;
 
@@ -136,4 +137,110 @@ 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();
+
+	}
+
 }

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

@@ -0,0 +1,151 @@
+/*
+ * 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);
+		}
+
+	}
+
+}

+ 54 - 0
core/src/test/java/org/springframework/security/authentication/AbstractAuthenticationBuilderTests.java

@@ -0,0 +1,54 @@
+/*
+ * 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.authentication;
+
+import java.util.Set;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.authority.AuthorityUtils;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class AbstractAuthenticationBuilderTests {
+
+	@Test
+	void applyWhenAuthoritiesThenAdds() {
+		TestingAuthenticationToken factorOne = new TestingAuthenticationToken("user", "pass", "FACTOR_ONE");
+		TestingAuthenticationToken factorTwo = new TestingAuthenticationToken("user", "pass", "FACTOR_TWO");
+		TestAbstractAuthenticationBuilder builder = new TestAbstractAuthenticationBuilder(factorOne);
+		Authentication result = builder.authorities((a) -> a.addAll(factorTwo.getAuthorities())).build();
+		Set<String> authorities = AuthorityUtils.authorityListToSet(result.getAuthorities());
+		assertThat(authorities).containsExactlyInAnyOrder("FACTOR_ONE", "FACTOR_TWO");
+	}
+
+	private static final class TestAbstractAuthenticationBuilder
+			extends TestingAuthenticationToken.Builder<TestAbstractAuthenticationBuilder> {
+
+		private TestAbstractAuthenticationBuilder(TestingAuthenticationToken token) {
+			super(token);
+		}
+
+		@Override
+		public TestingAuthenticationToken build() {
+			return new TestingAuthenticationToken(this);
+		}
+
+	}
+
+}

+ 11 - 8
core/src/test/java/org/springframework/security/authentication/ProviderManagerTests.java

@@ -18,6 +18,7 @@ package org.springframework.security.authentication;
 
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collection;
 import java.util.List;
 
 import org.junit.jupiter.api.Test;
@@ -25,6 +26,7 @@ import org.junit.jupiter.api.Test;
 import org.springframework.context.MessageSource;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.core.GrantedAuthority;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
@@ -47,7 +49,7 @@ public class ProviderManagerTests {
 
 	@Test
 	void authenticationFailsWithUnsupportedToken() {
-		Authentication token = new AbstractAuthenticationToken(null) {
+		Authentication token = new AbstractAuthenticationToken((Collection<? extends GrantedAuthority>) null) {
 			@Override
 			public Object getCredentials() {
 				return "";
@@ -78,24 +80,24 @@ public class ProviderManagerTests {
 
 	@Test
 	void authenticationSucceedsWithSupportedTokenAndReturnsExpectedObject() {
-		Authentication a = mock(Authentication.class);
+		Authentication a = new TestingAuthenticationToken("user", "pass", "FACTOR");
 		ProviderManager mgr = new ProviderManager(createProviderWhichReturns(a));
 		AuthenticationEventPublisher publisher = mock(AuthenticationEventPublisher.class);
 		mgr.setAuthenticationEventPublisher(publisher);
 		Authentication result = mgr.authenticate(a);
-		assertThat(result).isEqualTo(a);
+		assertThat(result.getPrincipal()).isEqualTo(a.getPrincipal());
 		verify(publisher).publishAuthenticationSuccess(result);
 	}
 
 	@Test
 	void authenticationSucceedsWhenFirstProviderReturnsNullButSecondAuthenticates() {
-		Authentication a = mock(Authentication.class);
+		Authentication a = new TestingAuthenticationToken("user", "pass", "FACTOR");
 		ProviderManager mgr = new ProviderManager(
 				Arrays.asList(createProviderWhichReturns(null), createProviderWhichReturns(a)));
 		AuthenticationEventPublisher publisher = mock(AuthenticationEventPublisher.class);
 		mgr.setAuthenticationEventPublisher(publisher);
 		Authentication result = mgr.authenticate(a);
-		assertThat(result).isSameAs(a);
+		assertThat(result.getPrincipal()).isEqualTo(a.getPrincipal());
 		verify(publisher).publishAuthenticationSuccess(result);
 	}
 
@@ -162,11 +164,12 @@ public class ProviderManagerTests {
 
 	@Test
 	void authenticationExceptionIsIgnoredIfLaterProviderAuthenticates() {
-		Authentication authReq = mock(Authentication.class);
+		Authentication result = new TestingAuthenticationToken("user", "pass", "FACTOR");
 		ProviderManager mgr = new ProviderManager(
 				createProviderWhichThrows(new BadCredentialsException("", new Throwable())),
-				createProviderWhichReturns(authReq));
-		assertThat(mgr.authenticate(mock(Authentication.class))).isSameAs(authReq);
+				createProviderWhichReturns(result));
+		Authentication request = new TestingAuthenticationToken("user", "pass");
+		assertThat(mgr.authenticate(request).getPrincipal()).isEqualTo(result.getPrincipal());
 	}
 
 	@Test

+ 19 - 0
core/src/test/java/org/springframework/security/authentication/TestingAuthenticationTokenTests.java

@@ -17,9 +17,11 @@
 package org.springframework.security.authentication;
 
 import java.util.Arrays;
+import java.util.Set;
 
 import org.junit.jupiter.api.Test;
 
+import org.springframework.security.core.authority.AuthorityUtils;
 import org.springframework.security.core.authority.SimpleGrantedAuthority;
 
 import static org.assertj.core.api.Assertions.assertThat;
@@ -49,4 +51,21 @@ public class TestingAuthenticationTokenTests {
 		assertThat(authenticated.isAuthenticated()).isTrue();
 	}
 
+	@Test
+	public void toBuilderWhenApplyThenCopies() {
+		TestingAuthenticationToken factorOne = new TestingAuthenticationToken("alice", "pass",
+				AuthorityUtils.createAuthorityList("FACTOR_ONE"));
+		TestingAuthenticationToken factorTwo = new TestingAuthenticationToken("bob", "ssap",
+				AuthorityUtils.createAuthorityList("FACTOR_TWO"));
+		TestingAuthenticationToken result = factorOne.toBuilder()
+			.authorities((a) -> a.addAll(factorTwo.getAuthorities()))
+			.principal(factorTwo.getPrincipal())
+			.credentials(factorTwo.getCredentials())
+			.build();
+		Set<String> authorities = AuthorityUtils.authorityListToSet(result.getAuthorities());
+		assertThat(result.getPrincipal()).isSameAs(factorTwo.getPrincipal());
+		assertThat(result.getCredentials()).isSameAs(factorTwo.getCredentials());
+		assertThat(authorities).containsExactlyInAnyOrder("FACTOR_ONE", "FACTOR_TWO");
+	}
+
 }

+ 19 - 0
core/src/test/java/org/springframework/security/authentication/UsernamePasswordAuthenticationTokenTests.java

@@ -16,6 +16,8 @@
 
 package org.springframework.security.authentication;
 
+import java.util.Set;
+
 import org.junit.jupiter.api.Test;
 
 import org.springframework.security.core.authority.AuthorityUtils;
@@ -85,4 +87,21 @@ public class UsernamePasswordAuthenticationTokenTests {
 		assertThat(grantedToken.isAuthenticated()).isTrue();
 	}
 
+	@Test
+	public void toBuilderWhenApplyThenCopies() {
+		UsernamePasswordAuthenticationToken factorOne = new UsernamePasswordAuthenticationToken("alice", "pass",
+				AuthorityUtils.createAuthorityList("FACTOR_ONE"));
+		UsernamePasswordAuthenticationToken factorTwo = new UsernamePasswordAuthenticationToken("bob", "ssap",
+				AuthorityUtils.createAuthorityList("FACTOR_TWO"));
+		UsernamePasswordAuthenticationToken result = factorOne.toBuilder()
+			.authorities((a) -> a.addAll(factorTwo.getAuthorities()))
+			.principal(factorTwo.getPrincipal())
+			.credentials(factorTwo.getCredentials())
+			.build();
+		Set<String> authorities = AuthorityUtils.authorityListToSet(result.getAuthorities());
+		assertThat(result.getPrincipal()).isEqualTo("bob");
+		assertThat(result.getCredentials()).isEqualTo("ssap");
+		assertThat(authorities).containsExactlyInAnyOrder("FACTOR_ONE", "FACTOR_TWO");
+	}
+
 }

+ 51 - 0
core/src/test/java/org/springframework/security/authentication/jaas/JaasAuthenticationTokenTests.java

@@ -0,0 +1,51 @@
+/*
+ * 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.authentication.jaas;
+
+import java.util.Set;
+
+import javax.security.auth.login.LoginContext;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.security.core.authority.AuthorityUtils;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+
+class JaasAuthenticationTokenTests {
+
+	@Test
+	void toBuilderWhenApplyThenCopies() {
+		JaasAuthenticationToken factorOne = new JaasAuthenticationToken("alice", "pass",
+				AuthorityUtils.createAuthorityList("FACTOR_ONE"), mock(LoginContext.class));
+		JaasAuthenticationToken factorTwo = new JaasAuthenticationToken("bob", "ssap",
+				AuthorityUtils.createAuthorityList("FACTOR_TWO"), mock(LoginContext.class));
+		JaasAuthenticationToken result = factorOne.toBuilder()
+			.authorities((a) -> a.addAll(factorTwo.getAuthorities()))
+			.principal(factorTwo.getPrincipal())
+			.credentials(factorTwo.getCredentials())
+			.loginContext(factorTwo.getLoginContext())
+			.build();
+		Set<String> authorities = AuthorityUtils.authorityListToSet(result.getAuthorities());
+		assertThat(result.getPrincipal()).isSameAs(factorTwo.getPrincipal());
+		assertThat(result.getCredentials()).isSameAs(factorTwo.getCredentials());
+		assertThat(result.getLoginContext()).isSameAs(factorTwo.getLoginContext());
+		assertThat(authorities).containsExactlyInAnyOrder("FACTOR_ONE", "FACTOR_TWO");
+	}
+
+}

+ 44 - 0
core/src/test/java/org/springframework/security/authentication/ott/OneTimeTokenAuthenticationTests.java

@@ -0,0 +1,44 @@
+/*
+ * 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.authentication.ott;
+
+import java.util.Set;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.security.core.authority.AuthorityUtils;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class OneTimeTokenAuthenticationTests {
+
+	@Test
+	void toBuilderWhenApplyThenCopies() {
+		OneTimeTokenAuthentication factorOne = new OneTimeTokenAuthentication("alice",
+				AuthorityUtils.createAuthorityList("FACTOR_ONE"));
+		OneTimeTokenAuthentication factorTwo = new OneTimeTokenAuthentication("bob",
+				AuthorityUtils.createAuthorityList("FACTOR_TWO"));
+		OneTimeTokenAuthentication result = factorOne.toBuilder()
+			.authorities((a) -> a.addAll(factorTwo.getAuthorities()))
+			.principal(factorTwo.getPrincipal())
+			.build();
+		Set<String> authorities = AuthorityUtils.authorityListToSet(result.getAuthorities());
+		assertThat(result.getPrincipal()).isSameAs(factorTwo.getPrincipal());
+		assertThat(authorities).containsExactlyInAnyOrder("FACTOR_ONE", "FACTOR_TWO");
+	}
+
+}

+ 19 - 0
core/src/test/java/org/springframework/security/authentication/rememberme/RememberMeAuthenticationTokenTests.java

@@ -18,6 +18,7 @@ package org.springframework.security.authentication.rememberme;
 
 import java.util.Arrays;
 import java.util.List;
+import java.util.Set;
 
 import org.junit.jupiter.api.Test;
 
@@ -25,6 +26,7 @@ import org.springframework.security.authentication.RememberMeAuthenticationToken
 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
 import org.springframework.security.core.GrantedAuthority;
 import org.springframework.security.core.authority.AuthorityUtils;
+import org.springframework.security.core.userdetails.PasswordEncodedUser;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
@@ -96,4 +98,21 @@ public class RememberMeAuthenticationTokenTests {
 		assertThat(!token.isAuthenticated()).isTrue();
 	}
 
+	@Test
+	public void toBuilderWhenApplyThenCopies() {
+		RememberMeAuthenticationToken factorOne = new RememberMeAuthenticationToken("key", PasswordEncodedUser.user(),
+				AuthorityUtils.createAuthorityList("FACTOR_ONE"));
+		RememberMeAuthenticationToken factorTwo = new RememberMeAuthenticationToken("yek", PasswordEncodedUser.admin(),
+				AuthorityUtils.createAuthorityList("FACTOR_TWO"));
+		RememberMeAuthenticationToken authentication = factorOne.toBuilder()
+			.authorities((a) -> a.addAll(factorTwo.getAuthorities()))
+			.key("yek")
+			.principal(factorTwo.getPrincipal())
+			.build();
+		Set<String> authorities = AuthorityUtils.authorityListToSet(authentication.getAuthorities());
+		assertThat(authentication.getKeyHash()).isEqualTo(factorTwo.getKeyHash());
+		assertThat(authentication.getPrincipal()).isEqualTo(factorTwo.getPrincipal());
+		assertThat(authorities).containsExactlyInAnyOrder("FACTOR_ONE", "FACTOR_TWO");
+	}
+
 }

+ 7 - 2
docs/modules/ROOT/pages/servlet/authentication/architecture.adoc

@@ -140,6 +140,11 @@ In many cases, this is cleared after the user is authenticated, to ensure that i
 * `authorities`: The <<servlet-authentication-granted-authority,`GrantedAuthority`>> instances are high-level permissions the user is granted.
 Two examples are roles and scopes.
 
+It is also equipped with a `Builder` that allows you to mutate an existing `Authentication` instance and potentially merge it with another.
+This is useful in scenarios like taking the authorities from one authentication step, like form login, and applying them to another, like one-time-token login, like so:
+
+include-code::./CopyAuthoritiesTests[tag=springSecurity,indent=0]
+
 [[servlet-authentication-granted-authority]]
 == GrantedAuthority
 javadoc:org.springframework.security.core.GrantedAuthority[] instances are high-level permissions that the user is granted.
@@ -231,8 +236,6 @@ In other cases, a client makes an unauthenticated request to a resource that the
 In this case, an implementation of `AuthenticationEntryPoint` is used to request credentials from the client.
 The `AuthenticationEntryPoint` implementation might perform a xref:servlet/authentication/passwords/form.adoc#servlet-authentication-form[redirect to a log in page], respond with an xref:servlet/authentication/passwords/basic.adoc#servlet-authentication-basic[WWW-Authenticate] header, or take other action.
 
-
-
 // FIXME: authenticationsuccesshandler
 // FIXME: authenticationfailurehandler
 
@@ -266,6 +269,8 @@ image:{icondir}/number_4.png[] If authentication is successful, then __Success__
 
 * `SessionAuthenticationStrategy` is notified of a new login.
 See the javadoc:org.springframework.security.web.authentication.session.SessionAuthenticationStrategy[] interface.
+* Any already-authenticated `Authentication` in the <<servlet-authentication-securitycontextholder>> is loaded and its
+authorities are added to the returned <<servlet-authentication-authentication>>.
 * The <<servlet-authentication-authentication>> is set on the <<servlet-authentication-securitycontextholder>>.
 Later, if you need to save the `SecurityContext` so that it can be automatically set on future requests, `SecurityContextRepository#saveContext` must be explicitly invoked.
 See the javadoc:org.springframework.security.web.context.SecurityContextHolderFilter[] class.

+ 2 - 0
docs/modules/ROOT/pages/servlet/authentication/passwords/basic.adoc

@@ -56,6 +56,8 @@ See the javadoc:org.springframework.security.web.AuthenticationEntryPoint[] inte
 
 image:{icondir}/number_4.png[] If authentication is successful, then __Success__.
 
+* Any already-authenticated `Authentication` in the xref:servlet/authentication/architecture.adoc#servlet-authentication-securitycontextholder[`SecurityContextHolder`] is loaded and its
+authorities are added to the returned xref:servlet/authentication/architecture.adoc#servlet-authentication-authentication[`Authentication`].
 . The xref:servlet/authentication/architecture.adoc#servlet-authentication-authentication[Authentication] is set on the xref:servlet/authentication/architecture.adoc#servlet-authentication-securitycontextholder[SecurityContextHolder].
 . `RememberMeServices.loginSuccess` is invoked.
 If remember me is not configured, this is a no-op.

+ 2 - 0
docs/modules/ROOT/pages/servlet/oauth2/resource-server/index.adoc

@@ -56,5 +56,7 @@ image:{icondir}/number_3.png[] If authentication fails, then __Failure__
 
 image:{icondir}/number_4.png[] If authentication is successful, then __Success__.
 
+* Any already-authenticated `Authentication` in the xref:servlet/authentication/architecture.adoc#servlet-authentication-securitycontextholder[`SecurityContextHolder`] is loaded and its
+authorities are added to the returned xref:servlet/authentication/architecture.adoc#servlet-authentication-authentication[`Authentication`].
 * The xref:servlet/authentication/architecture.adoc#servlet-authentication-authentication[Authentication] is set on the xref:servlet/authentication/architecture.adoc#servlet-authentication-securitycontextholder[SecurityContextHolder].
 * The `BearerTokenAuthenticationFilter` invokes `FilterChain.doFilter(request,response)` to continue with the rest of the application logic.

+ 1 - 0
docs/modules/ROOT/pages/whats-new.adoc

@@ -13,6 +13,7 @@ Each section that follows will indicate the more notable removals as well as the
 
 * Removed `AuthorizationManager#check` in favor of `AuthorizationManager#authorize`
 * Added xref:servlet/authorization/architecture.adoc#authz-authorization-manager-factory[`AuthorizationManagerFactory`] for creating `AuthorizationManager` instances in xref:servlet/authorization/authorize-http-requests.adoc#customizing-authorization-managers[request-based] and xref:servlet/authorization/method-security.adoc#customizing-authorization-managers[method-based] authorization components
+* Added `Authentication.Builder` for mutating and merging `Authentication` instances
 
 == Config
 

+ 41 - 0
docs/src/test/java/org/springframework/security/docs/servlet/authentication/servletauthenticationauthentication/CopyAuthoritiesTests.java

@@ -0,0 +1,41 @@
+package org.springframework.security.docs.servlet.authentication.servletauthenticationauthentication;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.SecurityAssertions;
+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.authority.AuthorityUtils;
+import org.springframework.security.core.context.SecurityContextHolder;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.mock;
+
+public class CopyAuthoritiesTests {
+	@Test
+	void toBuilderWhenApplyThenCopies() {
+		UsernamePasswordAuthenticationToken previous = new UsernamePasswordAuthenticationToken("alice", "pass",
+				AuthorityUtils.createAuthorityList("FACTOR_PASSWORD"));
+		SecurityContextHolder.getContext().setAuthentication(previous);
+		Authentication latest = new OneTimeTokenAuthentication("bob",
+				AuthorityUtils.createAuthorityList("FACTOR_OTT"));
+		AuthenticationManager authenticationManager = mock(AuthenticationManager.class);
+		given(authenticationManager.authenticate(any())).willReturn(latest);
+		Authentication authenticationRequest = new TestingAuthenticationToken("user", "pass");
+		// tag::springSecurity[]
+		Authentication lastestResult = authenticationManager.authenticate(authenticationRequest);
+		Authentication previousResult = SecurityContextHolder.getContext().getAuthentication();
+		if (previousResult != null && previousResult.isAuthenticated()) {
+			lastestResult = lastestResult.toBuilder()
+					.authorities((a) -> a.addAll(previous.getAuthorities()))
+					.build();
+		}
+		// end::springSecurity[]
+		SecurityAssertions.assertThat(lastestResult).hasAuthorities("FACTOR_PASSWORD", "FACTOR_OTT");
+		SecurityContextHolder.clearContext();
+	}
+}

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

@@ -0,0 +1,39 @@
+package org.springframework.security.kt.docs.servlet.authentication.servletauthenticationauthentication
+
+import org.junit.jupiter.api.Test
+import org.mockito.ArgumentMatchers
+import org.mockito.BDDMockito
+import org.mockito.Mockito
+import org.springframework.security.authentication.AuthenticationManager
+import org.springframework.security.authentication.SecurityAssertions
+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.authority.AuthorityUtils
+import org.springframework.security.core.context.SecurityContextHolder
+
+class CopyAuthoritiesTests {
+    @Test
+    fun toBuilderWhenApplyThenCopies() {
+        val previous: Authentication = UsernamePasswordAuthenticationToken("alice", "pass",
+            AuthorityUtils.createAuthorityList("FACTOR_PASSWORD"))
+        SecurityContextHolder.getContext().authentication = previous
+        var latest: Authentication = OneTimeTokenAuthentication("bob",
+            AuthorityUtils.createAuthorityList("FACTOR_OTT"))
+        val authenticationManager: AuthenticationManager = Mockito.mock(AuthenticationManager::class.java)
+        BDDMockito.given(authenticationManager.authenticate(ArgumentMatchers.any())).willReturn(latest)
+        val authenticationRequest: Authentication = TestingAuthenticationToken("user", "pass")
+        // tag::springSecurity[]
+        var latestResult: Authentication = authenticationManager.authenticate(authenticationRequest)
+        val previousResult = SecurityContextHolder.getContext().authentication;
+        if (previousResult?.isAuthenticated == true) {
+            latestResult = latestResult.toBuilder().authorities { a ->
+                a.addAll(previousResult.authorities)
+            }.build()
+        }
+        // end::springSecurity[]
+        SecurityAssertions.assertThat(latestResult).hasAuthorities("FACTOR_PASSWORD", "FACTOR_OTT")
+        SecurityContextHolder.clearContext()
+    }
+}

+ 3 - 1
oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/OAuth2AuthorizeRequest.java

@@ -16,6 +16,7 @@
 
 package org.springframework.security.oauth2.client;
 
+import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.LinkedHashMap;
@@ -25,6 +26,7 @@ import java.util.function.Consumer;
 import org.springframework.lang.Nullable;
 import org.springframework.security.authentication.AbstractAuthenticationToken;
 import org.springframework.security.core.Authentication;
+import org.springframework.security.core.GrantedAuthority;
 import org.springframework.security.oauth2.client.registration.ClientRegistration;
 import org.springframework.util.Assert;
 import org.springframework.util.CollectionUtils;
@@ -157,7 +159,7 @@ public final class OAuth2AuthorizeRequest {
 
 		private static Authentication createAuthentication(final String principalName) {
 			Assert.hasText(principalName, "principalName cannot be empty");
-			return new AbstractAuthenticationToken(null) {
+			return new AbstractAuthenticationToken((Collection<? extends GrantedAuthority>) null) {
 
 				@Override
 				public Object getCredentials() {

+ 59 - 0
oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthenticationToken.java

@@ -18,6 +18,8 @@ package org.springframework.security.oauth2.client.authentication;
 
 import java.util.Collection;
 
+import org.jspecify.annotations.Nullable;
+
 import org.springframework.security.authentication.AbstractAuthenticationToken;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.GrantedAuthority;
@@ -65,6 +67,14 @@ public class OAuth2AuthenticationToken extends AbstractAuthenticationToken {
 		this.setAuthenticated(true);
 	}
 
+	protected OAuth2AuthenticationToken(Builder<?> builder) {
+		super(builder);
+		Assert.notNull(builder.principal, "principal cannot be null");
+		Assert.hasText(builder.authorizedClientRegistrationId, "authorizedClientRegistrationId cannot be empty");
+		this.principal = builder.principal;
+		this.authorizedClientRegistrationId = builder.authorizedClientRegistrationId;
+	}
+
 	@Override
 	public OAuth2User getPrincipal() {
 		return this.principal;
@@ -85,4 +95,53 @@ public class OAuth2AuthenticationToken extends AbstractAuthenticationToken {
 		return this.authorizedClientRegistrationId;
 	}
 
+	@Override
+	public Builder<?> toBuilder() {
+		return new Builder<>(this);
+	}
+
+	/**
+	 * A builder of {@link OAuth2AuthenticationToken} instances
+	 *
+	 * @since 7.0
+	 */
+	public static class Builder<B extends Builder<B>> extends AbstractAuthenticationBuilder<B> {
+
+		private OAuth2User principal;
+
+		private String authorizedClientRegistrationId;
+
+		protected Builder(OAuth2AuthenticationToken token) {
+			super(token);
+			this.principal = token.principal;
+			this.authorizedClientRegistrationId = token.authorizedClientRegistrationId;
+		}
+
+		@Override
+		public B principal(@Nullable Object principal) {
+			Assert.isInstanceOf(OAuth2User.class, principal, "principal must be of type OAuth2User");
+			this.principal = (OAuth2User) principal;
+			return (B) this;
+		}
+
+		/**
+		 * Use this
+		 * {@link org.springframework.security.oauth2.client.registration.ClientRegistration}
+		 * {@code registrationId}.
+		 * @param authorizedClientRegistrationId the registration id to use
+		 * @return the {@link Builder} for further configurations
+		 * @see OAuth2AuthenticationToken#getAuthorizedClientRegistrationId
+		 */
+		public B authorizedClientRegistrationId(String authorizedClientRegistrationId) {
+			this.authorizedClientRegistrationId = authorizedClientRegistrationId;
+			return (B) this;
+		}
+
+		@Override
+		public OAuth2AuthenticationToken build() {
+			return new OAuth2AuthenticationToken(this);
+		}
+
+	}
+
 }

+ 3 - 1
oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunction.java

@@ -16,6 +16,7 @@
 
 package org.springframework.security.oauth2.client.web.reactive.function.client;
 
+import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.Locale;
@@ -36,6 +37,7 @@ import org.springframework.http.HttpStatusCode;
 import org.springframework.security.authentication.AbstractAuthenticationToken;
 import org.springframework.security.authentication.AnonymousAuthenticationToken;
 import org.springframework.security.core.Authentication;
+import org.springframework.security.core.GrantedAuthority;
 import org.springframework.security.core.authority.AuthorityUtils;
 import org.springframework.security.core.context.SecurityContextHolder;
 import org.springframework.security.core.context.SecurityContextHolderStrategy;
@@ -551,7 +553,7 @@ public final class ServletOAuth2AuthorizedClientExchangeFilterFunction implement
 
 	private static Authentication createAuthentication(final String principalName) {
 		Assert.hasText(principalName, "principalName cannot be empty");
-		return new AbstractAuthenticationToken(null) {
+		return new AbstractAuthenticationToken((Collection<? extends GrantedAuthority>) null) {
 
 			@Override
 			public Object getCredentials() {

+ 20 - 0
oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthenticationTokenTests.java

@@ -18,12 +18,15 @@ package org.springframework.security.oauth2.client.authentication;
 
 import java.util.Collection;
 import java.util.Collections;
+import java.util.Set;
 
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 
 import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.AuthorityUtils;
 import org.springframework.security.oauth2.core.user.OAuth2User;
+import org.springframework.security.oauth2.core.user.TestOAuth2Users;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
@@ -82,4 +85,21 @@ public class OAuth2AuthenticationTokenTests {
 		assertThat(authentication.isAuthenticated()).isEqualTo(true);
 	}
 
+	@Test
+	public void toBuilderWhenApplyThenCopies() {
+		OAuth2AuthenticationToken factorOne = new OAuth2AuthenticationToken(TestOAuth2Users.create(),
+				AuthorityUtils.createAuthorityList("FACTOR_ONE"), "alice");
+		OAuth2AuthenticationToken factorTwo = new OAuth2AuthenticationToken(TestOAuth2Users.create(),
+				AuthorityUtils.createAuthorityList("FACTOR_TWO"), "bob");
+		OAuth2AuthenticationToken result = factorOne.toBuilder()
+			.authorities((a) -> a.addAll(factorTwo.getAuthorities()))
+			.principal(factorTwo.getPrincipal())
+			.authorizedClientRegistrationId(factorTwo.getAuthorizedClientRegistrationId())
+			.build();
+		Set<String> authorities = AuthorityUtils.authorityListToSet(result.getAuthorities());
+		assertThat(result.getPrincipal()).isSameAs(factorTwo.getPrincipal());
+		assertThat(result.getAuthorizedClientRegistrationId()).isSameAs(factorTwo.getAuthorizedClientRegistrationId());
+		assertThat(authorities).containsExactlyInAnyOrder("FACTOR_ONE", "FACTOR_TWO");
+	}
+
 }

+ 60 - 0
oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/AbstractOAuth2TokenAuthenticationToken.java

@@ -19,6 +19,8 @@ package org.springframework.security.oauth2.server.resource.authentication;
 import java.util.Collection;
 import java.util.Map;
 
+import org.jspecify.annotations.Nullable;
+
 import org.springframework.security.authentication.AbstractAuthenticationToken;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.GrantedAuthority;
@@ -83,6 +85,15 @@ public abstract class AbstractOAuth2TokenAuthenticationToken<T extends OAuth2Tok
 		this.token = token;
 	}
 
+	protected AbstractOAuth2TokenAuthenticationToken(AbstractOAuth2TokenAuthenticationBuilder<T, ?> builder) {
+		super(builder);
+		Assert.notNull(builder.credentials, "token cannot be null");
+		Assert.notNull(builder.principal, "principal cannot be null");
+		this.principal = builder.principal;
+		this.credentials = builder.credentials;
+		this.token = builder.token;
+	}
+
 	@Override
 	public Object getPrincipal() {
 		return this.principal;
@@ -106,4 +117,53 @@ public abstract class AbstractOAuth2TokenAuthenticationToken<T extends OAuth2Tok
 	 */
 	public abstract Map<String, Object> getTokenAttributes();
 
+	/**
+	 * A builder for {@link AbstractOAuth2TokenAuthenticationToken} implementations
+	 *
+	 * @param <B>
+	 * @since 7.0
+	 */
+	public abstract static class AbstractOAuth2TokenAuthenticationBuilder<T extends OAuth2Token, B extends AbstractOAuth2TokenAuthenticationBuilder<T, B>>
+			extends AbstractAuthenticationBuilder<B> {
+
+		private Object principal;
+
+		private Object credentials;
+
+		private T token;
+
+		protected AbstractOAuth2TokenAuthenticationBuilder(AbstractOAuth2TokenAuthenticationToken<T> token) {
+			super(token);
+			this.principal = token.getPrincipal();
+			this.credentials = token.getCredentials();
+			this.token = token.getToken();
+		}
+
+		@Override
+		public B principal(@Nullable Object principal) {
+			Assert.notNull(principal, "principal cannot be null");
+			this.principal = principal;
+			return (B) this;
+		}
+
+		@Override
+		public B credentials(@Nullable Object credentials) {
+			Assert.notNull(credentials, "credentials cannot be null");
+			this.credentials = credentials;
+			return (B) this;
+		}
+
+		/**
+		 * The OAuth 2.0 Token to use
+		 * @param token the token to use
+		 * @return the {@link Builder} for further configurations
+		 */
+		public B token(T token) {
+			Assert.notNull(token, "token cannot be null");
+			this.token = token;
+			return (B) this;
+		}
+
+	}
+
 }

+ 75 - 0
oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/BearerTokenAuthentication.java

@@ -21,6 +21,9 @@ import java.util.Collections;
 import java.util.LinkedHashMap;
 import java.util.Map;
 
+import org.jspecify.annotations.Nullable;
+
+import org.springframework.security.core.Authentication;
 import org.springframework.security.core.GrantedAuthority;
 import org.springframework.security.core.Transient;
 import org.springframework.security.oauth2.core.OAuth2AccessToken;
@@ -56,9 +59,81 @@ public class BearerTokenAuthentication extends AbstractOAuth2TokenAuthentication
 		setAuthenticated(true);
 	}
 
+	protected BearerTokenAuthentication(Builder<?> builder) {
+		super(builder);
+		this.attributes = Collections.unmodifiableMap(new LinkedHashMap<>(builder.attributes));
+	}
+
 	@Override
 	public Map<String, Object> getTokenAttributes() {
 		return this.attributes;
 	}
 
+	@Override
+	public Builder<?> toBuilder() {
+		return new Builder<>(this);
+	}
+
+	/**
+	 * A builder preserving the concrete {@link Authentication} type
+	 *
+	 * @since 7.0
+	 */
+	public static class Builder<B extends Builder<B>>
+			extends AbstractOAuth2TokenAuthenticationBuilder<OAuth2AccessToken, B> {
+
+		private Map<String, Object> attributes;
+
+		protected Builder(BearerTokenAuthentication token) {
+			super(token);
+			this.attributes = token.getTokenAttributes();
+		}
+
+		/**
+		 * Use this principal. Must be of type {@link OAuth2AuthenticatedPrincipal}
+		 * @param principal the principal to use
+		 * @return the {@link Builder} for further configurations
+		 */
+		@Override
+		public B principal(@Nullable Object principal) {
+			Assert.isInstanceOf(OAuth2AuthenticatedPrincipal.class, principal,
+					"principal must be of type OAuth2AuthenticatedPrincipal");
+			this.attributes = ((OAuth2AuthenticatedPrincipal) principal).getAttributes();
+			return super.principal(principal);
+		}
+
+		/**
+		 * A synonym for {@link #token(OAuth2AccessToken)}
+		 * @param token the token to use
+		 * @return the {@link Builder} for further configurations
+		 */
+		@Override
+		public B credentials(@Nullable Object token) {
+			Assert.isInstanceOf(OAuth2AccessToken.class, token, "token must be of type OAuth2AccessToken");
+			return token((OAuth2AccessToken) token);
+		}
+
+		/**
+		 * Use this token. Must have a {@link OAuth2AccessToken#getTokenType()} as
+		 * {@link OAuth2AccessToken.TokenType#BEARER}.
+		 * @param token the token to use
+		 * @return the {@link Builder} for further configurations
+		 */
+		@Override
+		public B token(OAuth2AccessToken token) {
+			Assert.isTrue(token.getTokenType() == OAuth2AccessToken.TokenType.BEARER, "token must be a bearer token");
+			super.credentials(token);
+			return super.token(token);
+		}
+
+		/**
+		 * {@inheritDoc}
+		 */
+		@Override
+		public BearerTokenAuthentication build() {
+			return new BearerTokenAuthentication(this);
+		}
+
+	}
+
 }

+ 79 - 0
oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationToken.java

@@ -19,9 +19,13 @@ package org.springframework.security.oauth2.server.resource.authentication;
 import java.util.Collection;
 import java.util.Map;
 
+import org.jspecify.annotations.Nullable;
+
+import org.springframework.security.core.Authentication;
 import org.springframework.security.core.GrantedAuthority;
 import org.springframework.security.core.Transient;
 import org.springframework.security.oauth2.jwt.Jwt;
+import org.springframework.util.Assert;
 
 /**
  * An implementation of an {@link AbstractOAuth2TokenAuthenticationToken} representing a
@@ -71,6 +75,11 @@ public class JwtAuthenticationToken extends AbstractOAuth2TokenAuthenticationTok
 		this.name = name;
 	}
 
+	protected JwtAuthenticationToken(Builder<?> builder) {
+		super(builder);
+		this.name = builder.name;
+	}
+
 	@Override
 	public Map<String, Object> getTokenAttributes() {
 		return this.getToken().getClaims();
@@ -84,4 +93,74 @@ public class JwtAuthenticationToken extends AbstractOAuth2TokenAuthenticationTok
 		return this.name;
 	}
 
+	@Override
+	public Builder<?> toBuilder() {
+		return new Builder<>(this);
+	}
+
+	/**
+	 * A builder for {@link JwtAuthenticationToken} instances
+	 *
+	 * @since 7.0
+	 * @see Authentication.Builder
+	 */
+	public static class Builder<B extends Builder<B>> extends AbstractOAuth2TokenAuthenticationBuilder<Jwt, B> {
+
+		private String name;
+
+		protected Builder(JwtAuthenticationToken token) {
+			super(token);
+			this.name = token.getName();
+		}
+
+		/**
+		 * A synonym for {@link #token(Jwt)}
+		 * @return the {@link Builder} for further configurations
+		 */
+		@Override
+		public B principal(@Nullable Object principal) {
+			Assert.isInstanceOf(Jwt.class, principal, "principal must be of type Jwt");
+			return token((Jwt) principal);
+		}
+
+		/**
+		 * A synonym for {@link #token(Jwt)}
+		 * @return the {@link Builder} for further configurations
+		 */
+		@Override
+		public B credentials(@Nullable Object credentials) {
+			Assert.isInstanceOf(Jwt.class, credentials, "credentials must be of type Jwt");
+			return token((Jwt) credentials);
+		}
+
+		/**
+		 * Use this {@code token} as the token, principal, and credentials. Also sets the
+		 * {@code name} to {@link Jwt#getSubject}.
+		 * @param token the token to use
+		 * @return the {@link Builder} for further configurations
+		 */
+		@Override
+		public B token(Jwt token) {
+			super.principal(token);
+			super.credentials(token);
+			return super.token(token).name(token.getSubject());
+		}
+
+		/**
+		 * The name to use.
+		 * @param name the name to use
+		 * @return the {@link Builder} for further configurations
+		 */
+		public B name(String name) {
+			this.name = name;
+			return (B) this;
+		}
+
+		@Override
+		public JwtAuthenticationToken build() {
+			return new JwtAuthenticationToken(this);
+		}
+
+	}
+
 }

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

@@ -180,6 +180,12 @@ public class BearerTokenAuthenticationFilter extends OncePerRequestFilter {
 				BearerTokenError error = BearerTokenErrors.invalidToken("Invalid bearer token");
 				throw new OAuth2AuthenticationException(error);
 			}
+			Authentication current = this.securityContextHolderStrategy.getContext().getAuthentication();
+			if (current != null && current.isAuthenticated()) {
+				authenticationResult = authenticationResult.toBuilder()
+					.authorities((a) -> a.addAll(current.getAuthorities()))
+					.build();
+			}
 			SecurityContext context = this.securityContextHolderStrategy.createEmptyContext();
 			context.setAuthentication(authenticationResult);
 			this.securityContextHolderStrategy.setContext(context);

+ 22 - 0
oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/BearerTokenAuthenticationTests.java

@@ -23,6 +23,7 @@ import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 import net.minidev.json.JSONObject;
 import org.junit.jupiter.api.BeforeEach;
@@ -34,6 +35,7 @@ import org.springframework.security.oauth2.core.DefaultOAuth2AuthenticatedPrinci
 import org.springframework.security.oauth2.core.OAuth2AccessToken;
 import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
 import org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaimNames;
+import org.springframework.security.oauth2.core.TestOAuth2AuthenticatedPrincipals;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
@@ -151,4 +153,24 @@ public class BearerTokenAuthenticationTests {
 		token.toString();
 	}
 
+	@Test
+	public void toBuilderWhenApplyThenCopies() {
+		BearerTokenAuthentication factorOne = new BearerTokenAuthentication(TestOAuth2AuthenticatedPrincipals.active(),
+				this.token, AuthorityUtils.createAuthorityList("FACTOR_ONE"));
+		BearerTokenAuthentication factorTwo = new BearerTokenAuthentication(
+				TestOAuth2AuthenticatedPrincipals.active((m) -> m.put("k", "v")),
+				new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, "nekot", Instant.now(),
+						Instant.now().plusSeconds(3600)),
+				AuthorityUtils.createAuthorityList("FACTOR_TWO"));
+		BearerTokenAuthentication authentication = factorOne.toBuilder()
+			.authorities((a) -> a.addAll(factorTwo.getAuthorities()))
+			.principal(factorTwo.getPrincipal())
+			.token(factorTwo.getToken())
+			.build();
+		Set<String> authorities = AuthorityUtils.authorityListToSet(authentication.getAuthorities());
+		assertThat(authentication.getPrincipal()).isSameAs(factorTwo.getPrincipal());
+		assertThat(authentication.getToken()).isSameAs(factorTwo.getToken());
+		assertThat(authorities).containsExactlyInAnyOrder("FACTOR_ONE", "FACTOR_TWO");
+	}
+
 }

+ 19 - 1
oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationTokenTests.java

@@ -17,6 +17,7 @@
 package org.springframework.security.oauth2.server.resource.authentication;
 
 import java.util.Collection;
+import java.util.Set;
 
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
@@ -54,7 +55,7 @@ public class JwtAuthenticationTokenTests {
 
 	@Test
 	public void constructorWhenJwtIsNullThenThrowsException() {
-		assertThatIllegalArgumentException().isThrownBy(() -> new JwtAuthenticationToken(null))
+		assertThatIllegalArgumentException().isThrownBy(() -> new JwtAuthenticationToken((Jwt) null))
 			.withMessageContaining("token cannot be null");
 	}
 
@@ -115,6 +116,23 @@ public class JwtAuthenticationTokenTests {
 		assertThat(new JwtAuthenticationToken(jwt).getName()).isNull();
 	}
 
+	@Test
+	public void toBuilderWhenApplyThenCopies() {
+		JwtAuthenticationToken factorOne = new JwtAuthenticationToken(builder().claim("c", "v").build(),
+				AuthorityUtils.createAuthorityList("FACTOR_ONE"), "alice");
+		JwtAuthenticationToken factorTwo = new JwtAuthenticationToken(builder().claim("d", "w").build(),
+				AuthorityUtils.createAuthorityList("FACTOR_TWO"), "bob");
+		JwtAuthenticationToken result = factorOne.toBuilder()
+			.authorities((a) -> a.addAll(factorTwo.getAuthorities()))
+			.principal(factorTwo.getPrincipal())
+			.name(factorTwo.getName())
+			.build();
+		Set<String> authorities = AuthorityUtils.authorityListToSet(result.getAuthorities());
+		assertThat(result.getPrincipal()).isSameAs(factorTwo.getPrincipal());
+		assertThat(result.getName()).isSameAs(factorTwo.getName());
+		assertThat(authorities).containsExactlyInAnyOrder("FACTOR_ONE", "FACTOR_TWO");
+	}
+
 	private Jwt.Builder builder() {
 		return Jwt.withTokenValue("token").header("alg", JwsAlgorithms.RS256);
 	}

+ 23 - 0
oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/authentication/BearerTokenAuthenticationFilterTests.java

@@ -18,7 +18,9 @@ package org.springframework.security.oauth2.server.resource.web.authentication;
 
 import java.io.IOException;
 import java.util.Collections;
+import java.util.Set;
 
+import jakarta.servlet.Filter;
 import jakarta.servlet.ServletException;
 import jakarta.servlet.http.HttpServletRequest;
 import org.junit.jupiter.api.BeforeEach;
@@ -37,8 +39,11 @@ import org.springframework.security.authentication.AuthenticationManager;
 import org.springframework.security.authentication.AuthenticationManagerResolver;
 import org.springframework.security.authentication.AuthenticationServiceException;
 import org.springframework.security.authentication.TestingAuthenticationToken;
+import org.springframework.security.core.Authentication;
 import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.core.authority.AuthorityUtils;
 import org.springframework.security.core.context.SecurityContext;
+import org.springframework.security.core.context.SecurityContextHolder;
 import org.springframework.security.core.context.SecurityContextHolderStrategy;
 import org.springframework.security.core.context.SecurityContextImpl;
 import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
@@ -240,6 +245,7 @@ public class BearerTokenAuthenticationFilterTests {
 				new BearerTokenAuthenticationFilter(this.authenticationManager));
 		SecurityContextHolderStrategy strategy = mock(SecurityContextHolderStrategy.class);
 		given(strategy.createEmptyContext()).willReturn(new SecurityContextImpl());
+		given(strategy.getContext()).willReturn(new SecurityContextImpl());
 		filter.setSecurityContextHolderStrategy(strategy);
 		filter.doFilter(this.request, this.response, this.filterChain);
 		verify(strategy).setContext(any());
@@ -339,6 +345,23 @@ public class BearerTokenAuthenticationFilterTests {
 		// @formatter:on
 	}
 
+	@Test
+	void authenticateWhenPreviousAuthenticationThenApplies() throws Exception {
+		Authentication first = new TestingAuthenticationToken("user", "pass", "FACTOR_ONE");
+		Authentication second = new TestingAuthenticationToken("user", "pass", "FACTOR_TWO");
+		Filter filter = addMocks(new BearerTokenAuthenticationFilter(this.authenticationManager));
+		given(this.bearerTokenResolver.resolve(this.request)).willReturn("token");
+		given(this.authenticationManager.authenticate(any())).willReturn(second);
+
+		SecurityContextHolder.getContext().setAuthentication(first);
+		filter.doFilter(this.request, this.response, this.filterChain);
+		Authentication result = SecurityContextHolder.getContext().getAuthentication();
+		SecurityContextHolder.clearContext();
+
+		Set<String> authorities = AuthorityUtils.authorityListToSet(result.getAuthorities());
+		assertThat(authorities).containsExactlyInAnyOrder("FACTOR_ONE", "FACTOR_TWO");
+	}
+
 	private BearerTokenAuthenticationFilter addMocks(BearerTokenAuthenticationFilter filter) {
 		filter.setAuthenticationEntryPoint(this.authenticationEntryPoint);
 		filter.setBearerTokenResolver(this.bearerTokenResolver);

+ 65 - 0
saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AssertionAuthentication.java

@@ -19,7 +19,11 @@ package org.springframework.security.saml2.provider.service.authentication;
 import java.io.Serial;
 import java.util.Collection;
 
+import org.jspecify.annotations.Nullable;
+
 import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
+import org.springframework.util.Assert;
 
 /**
  * An authentication based off of a SAML 2.0 Assertion
@@ -53,6 +57,12 @@ public class Saml2AssertionAuthentication extends Saml2Authentication {
 		setAuthenticated(true);
 	}
 
+	protected Saml2AssertionAuthentication(Builder<?> builder) {
+		super(builder);
+		this.assertion = builder.assertion;
+		this.relyingPartyRegistrationId = builder.relyingPartyRegistrationId;
+	}
+
 	@Override
 	public Saml2ResponseAssertionAccessor getCredentials() {
 		return this.assertion;
@@ -62,4 +72,59 @@ public class Saml2AssertionAuthentication extends Saml2Authentication {
 		return this.relyingPartyRegistrationId;
 	}
 
+	@Override
+	public Builder<?> toBuilder() {
+		return new Builder<>(this);
+	}
+
+	/**
+	 * A builder of {@link Saml2AssertionAuthentication} instances
+	 *
+	 * @since 7.0
+	 */
+	public static class Builder<B extends Builder<B>> extends Saml2Authentication.Builder<B> {
+
+		private Saml2ResponseAssertionAccessor assertion;
+
+		private String relyingPartyRegistrationId;
+
+		protected Builder(Saml2AssertionAuthentication token) {
+			super(token);
+			this.assertion = token.assertion;
+			this.relyingPartyRegistrationId = token.relyingPartyRegistrationId;
+		}
+
+		/**
+		 * Use these credentials. They must be of type
+		 * {@link Saml2ResponseAssertionAccessor}.
+		 * @param credentials the credentials to use
+		 * @return the {@link Builder} for further configurations
+		 */
+		@Override
+		public B credentials(@Nullable Object credentials) {
+			Assert.isInstanceOf(Saml2ResponseAssertionAccessor.class, credentials,
+					"credentials must be of type Saml2ResponseAssertionAccessor");
+			saml2Response(((Saml2ResponseAssertionAccessor) credentials).getResponseValue());
+			this.assertion = (Saml2ResponseAssertionAccessor) credentials;
+			return (B) this;
+		}
+
+		/**
+		 * Use this registration id
+		 * @param relyingPartyRegistrationId the
+		 * {@link RelyingPartyRegistration#getRegistrationId} to use
+		 * @return the {@link Builder} for further configurations
+		 */
+		public B relyingPartyRegistrationId(String relyingPartyRegistrationId) {
+			this.relyingPartyRegistrationId = relyingPartyRegistrationId;
+			return (B) this;
+		}
+
+		@Override
+		public Saml2AssertionAuthentication build() {
+			return new Saml2AssertionAuthentication(this);
+		}
+
+	}
+
 }

+ 33 - 0
saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2Authentication.java

@@ -19,6 +19,8 @@ package org.springframework.security.saml2.provider.service.authentication;
 import java.io.Serial;
 import java.util.Collection;
 
+import org.jspecify.annotations.Nullable;
+
 import org.springframework.security.authentication.AbstractAuthenticationToken;
 import org.springframework.security.core.AuthenticatedPrincipal;
 import org.springframework.security.core.Authentication;
@@ -69,6 +71,12 @@ public class Saml2Authentication extends AbstractAuthenticationToken {
 		setAuthenticated(true);
 	}
 
+	Saml2Authentication(Builder<?> builder) {
+		super(builder);
+		this.principal = builder.principal;
+		this.saml2Response = builder.saml2Response;
+	}
+
 	@Override
 	public Object getPrincipal() {
 		return this.principal;
@@ -87,4 +95,29 @@ public class Saml2Authentication extends AbstractAuthenticationToken {
 		return getSaml2Response();
 	}
 
+	abstract static class Builder<B extends Builder<B>> extends AbstractAuthenticationBuilder<B> {
+
+		private Object principal;
+
+		String saml2Response;
+
+		Builder(Saml2Authentication token) {
+			super(token);
+			this.principal = token.principal;
+			this.saml2Response = token.saml2Response;
+		}
+
+		@Override
+		public B principal(@Nullable Object principal) {
+			Assert.notNull(principal, "principal cannot be null");
+			this.principal = principal;
+			return (B) this;
+		}
+
+		void saml2Response(String saml2Response) {
+			this.saml2Response = saml2Response;
+		}
+
+	}
+
 }

+ 49 - 0
saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/Saml2AssertionAuthenticationTests.java

@@ -0,0 +1,49 @@
+/*
+ * 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.saml2.provider.service.authentication;
+
+import java.util.Set;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.security.core.authority.AuthorityUtils;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class Saml2AssertionAuthenticationTests {
+
+	@Test
+	void toBuilderWhenApplyThenCopies() {
+		Saml2ResponseAssertion.Builder prototype = Saml2ResponseAssertion.withResponseValue("response");
+		Saml2AssertionAuthentication factorOne = new Saml2AssertionAuthentication("alice",
+				prototype.nameId("alice").build(), AuthorityUtils.createAuthorityList("FACTOR_ONE"), "alice");
+		Saml2AssertionAuthentication factorTwo = new Saml2AssertionAuthentication("bob",
+				prototype.nameId("bob").build(), AuthorityUtils.createAuthorityList("FACTOR_TWO"), "bob");
+		Saml2AssertionAuthentication result = factorOne.toBuilder()
+			.authorities((a) -> a.addAll(factorTwo.getAuthorities()))
+			.principal(factorTwo.getPrincipal())
+			.credentials(factorTwo.getCredentials())
+			.relyingPartyRegistrationId(factorTwo.getRelyingPartyRegistrationId())
+			.build();
+		Set<String> authorities = AuthorityUtils.authorityListToSet(result.getAuthorities());
+		assertThat(result.getPrincipal()).isSameAs(factorTwo.getPrincipal());
+		assertThat(result.getCredentials()).isSameAs(factorTwo.getCredentials());
+		assertThat(result.getRelyingPartyRegistrationId()).isSameAs(factorTwo.getRelyingPartyRegistrationId());
+		assertThat(authorities).containsExactlyInAnyOrder("FACTOR_ONE", "FACTOR_TWO");
+	}
+
+}

+ 6 - 0
web/src/main/java/org/springframework/security/web/authentication/AbstractAuthenticationProcessingFilter.java

@@ -248,6 +248,12 @@ public abstract class AbstractAuthenticationProcessingFilter extends GenericFilt
 				// return immediately as subclass has indicated that it hasn't completed
 				return;
 			}
+			Authentication current = this.securityContextHolderStrategy.getContext().getAuthentication();
+			if (current != null && current.isAuthenticated()) {
+				authenticationResult = authenticationResult.toBuilder()
+					.authorities((a) -> a.addAll(current.getAuthorities()))
+					.build();
+			}
 			this.sessionStrategy.onAuthentication(authenticationResult, request, response);
 			// Authentication success
 			if (this.continueChainBeforeSuccessfulAuthentication) {

+ 6 - 0
web/src/main/java/org/springframework/security/web/authentication/AuthenticationFilter.java

@@ -184,6 +184,12 @@ public class AuthenticationFilter extends OncePerRequestFilter {
 				filterChain.doFilter(request, response);
 				return;
 			}
+			Authentication current = this.securityContextHolderStrategy.getContext().getAuthentication();
+			if (current != null && current.isAuthenticated()) {
+				authenticationResult = authenticationResult.toBuilder()
+					.authorities((a) -> a.addAll(current.getAuthorities()))
+					.build();
+			}
 			HttpSession session = request.getSession(false);
 			if (session != null) {
 				request.changeSessionId();

+ 6 - 0
web/src/main/java/org/springframework/security/web/authentication/preauth/AbstractPreAuthenticatedProcessingFilter.java

@@ -204,6 +204,12 @@ public abstract class AbstractPreAuthenticatedProcessingFilter extends GenericFi
 					principal, credentials);
 			authenticationRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
 			Authentication authenticationResult = this.authenticationManager.authenticate(authenticationRequest);
+			Authentication current = this.securityContextHolderStrategy.getContext().getAuthentication();
+			if (current != null && current.isAuthenticated()) {
+				authenticationResult = authenticationResult.toBuilder()
+					.authorities((a) -> a.addAll(current.getAuthorities()))
+					.build();
+			}
 			successfulAuthentication(request, response, authenticationResult);
 		}
 		catch (AuthenticationException ex) {

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

@@ -22,6 +22,7 @@ import org.jspecify.annotations.Nullable;
 
 import org.springframework.security.authentication.AbstractAuthenticationToken;
 import org.springframework.security.core.GrantedAuthority;
+import org.springframework.util.Assert;
 
 /**
  * {@link org.springframework.security.core.Authentication} implementation for
@@ -46,7 +47,7 @@ public class PreAuthenticatedAuthenticationToken extends AbstractAuthenticationT
 	 * @param aCredentials The pre-authenticated credentials
 	 */
 	public PreAuthenticatedAuthenticationToken(Object aPrincipal, @Nullable Object aCredentials) {
-		super(null);
+		super((Collection<? extends GrantedAuthority>) null);
 		this.principal = aPrincipal;
 		this.credentials = aCredentials;
 	}
@@ -66,6 +67,12 @@ public class PreAuthenticatedAuthenticationToken extends AbstractAuthenticationT
 		setAuthenticated(true);
 	}
 
+	protected PreAuthenticatedAuthenticationToken(Builder<?> builder) {
+		super(builder);
+		this.principal = builder.principal;
+		this.credentials = builder.credentials;
+	}
+
 	/**
 	 * Get the credentials
 	 */
@@ -82,4 +89,46 @@ public class PreAuthenticatedAuthenticationToken extends AbstractAuthenticationT
 		return this.principal;
 	}
 
+	@Override
+	public Builder<?> toBuilder() {
+		return new Builder<>(this);
+	}
+
+	/**
+	 * A builder of {@link PreAuthenticatedAuthenticationToken} instances
+	 *
+	 * @since 7.0
+	 */
+	public static class Builder<B extends Builder<B>> extends AbstractAuthenticationBuilder<B> {
+
+		private Object principal;
+
+		private @Nullable Object credentials;
+
+		protected Builder(PreAuthenticatedAuthenticationToken token) {
+			super(token);
+			this.principal = token.principal;
+			this.credentials = token.credentials;
+		}
+
+		@Override
+		public B principal(@Nullable Object principal) {
+			Assert.notNull(principal, "principal cannot be null");
+			this.principal = principal;
+			return (B) this;
+		}
+
+		@Override
+		public B credentials(@Nullable Object credentials) {
+			this.credentials = credentials;
+			return (B) this;
+		}
+
+		@Override
+		public PreAuthenticatedAuthenticationToken build() {
+			return new PreAuthenticatedAuthenticationToken(this);
+		}
+
+	}
+
 }

+ 4 - 0
web/src/main/java/org/springframework/security/web/authentication/www/BasicAuthenticationFilter.java

@@ -186,6 +186,10 @@ public class BasicAuthenticationFilter extends OncePerRequestFilter {
 			this.logger.trace(LogMessage.format("Found username '%s' in Basic Authorization header", username));
 			if (authenticationIsRequired(username)) {
 				Authentication authResult = this.authenticationManager.authenticate(authRequest);
+				Authentication current = this.securityContextHolderStrategy.getContext().getAuthentication();
+				if (current != null && current.isAuthenticated()) {
+					authResult = authResult.toBuilder().authorities((a) -> a.addAll(current.getAuthorities())).build();
+				}
 				SecurityContext context = this.securityContextHolderStrategy.createEmptyContext();
 				context.setAuthentication(authResult);
 				this.securityContextHolderStrategy.setContext(context);

+ 14 - 0
web/src/main/java/org/springframework/security/web/server/authentication/AuthenticationWebFilter.java

@@ -122,12 +122,26 @@ public class AuthenticationWebFilter implements WebFilter {
 			.flatMap((authenticationManager) -> authenticationManager.authenticate(token))
 			.switchIfEmpty(Mono
 				.defer(() -> Mono.error(new IllegalStateException("No provider found for " + token.getClass()))))
+			.flatMap(this::applyCurrentAuthenication)
 			.flatMap(
 					(authentication) -> onAuthenticationSuccess(authentication, new WebFilterExchange(exchange, chain)))
 			.doOnError(AuthenticationException.class,
 					(ex) -> logger.debug(LogMessage.format("Authentication failed: %s", ex.getMessage()), ex));
 	}
 
+	private Mono<Authentication> applyCurrentAuthenication(Authentication result) {
+		return ReactiveSecurityContextHolder.getContext().map((context) -> {
+			Authentication current = context.getAuthentication();
+			if (current == null) {
+				return result;
+			}
+			if (!current.isAuthenticated()) {
+				return result;
+			}
+			return result.toBuilder().authorities((a) -> a.addAll(current.getAuthorities())).build();
+		}).switchIfEmpty(Mono.just(result));
+	}
+
 	protected Mono<Void> onAuthenticationSuccess(Authentication authentication, WebFilterExchange webFilterExchange) {
 		ServerWebExchange exchange = webFilterExchange.getExchange();
 		SecurityContextImpl securityContext = new SecurityContextImpl();

+ 1 - 0
web/src/test/java/org/springframework/security/web/authentication/AuthenticationFilterTests.java

@@ -144,6 +144,7 @@ public class AuthenticationFilterTests {
 				this.authenticationConverter);
 		SecurityContextHolderStrategy strategy = mock(SecurityContextHolderStrategy.class);
 		given(strategy.createEmptyContext()).willReturn(new SecurityContextImpl());
+		given(strategy.getContext()).willReturn(new SecurityContextImpl());
 		filter.setSecurityContextHolderStrategy(strategy);
 		MockHttpServletRequest request = new MockHttpServletRequest("GET", "/");
 		MockHttpServletResponse response = new MockHttpServletResponse();

+ 18 - 0
web/src/test/java/org/springframework/security/web/authentication/preauth/PreAuthenticatedAuthenticationTokenTests.java

@@ -18,6 +18,7 @@ package org.springframework.security.web.authentication.preauth;
 
 import java.util.Collection;
 import java.util.List;
+import java.util.Set;
 
 import org.junit.jupiter.api.Test;
 
@@ -73,4 +74,21 @@ public class PreAuthenticatedAuthenticationTokenTests {
 			.isTrue();
 	}
 
+	@Test
+	public void toBuilderWhenApplyThenCopies() {
+		PreAuthenticatedAuthenticationToken factorOne = new PreAuthenticatedAuthenticationToken("alice", "pass",
+				AuthorityUtils.createAuthorityList("FACTOR_ONE"));
+		PreAuthenticatedAuthenticationToken factorTwo = new PreAuthenticatedAuthenticationToken("bob", "ssap",
+				AuthorityUtils.createAuthorityList("FACTOR_TWO"));
+		PreAuthenticatedAuthenticationToken result = factorOne.toBuilder()
+			.authorities((a) -> a.addAll(factorTwo.getAuthorities()))
+			.principal(factorTwo.getPrincipal())
+			.credentials(factorTwo.getCredentials())
+			.build();
+		Set<String> authorities = AuthorityUtils.authorityListToSet(result.getAuthorities());
+		assertThat(result.getPrincipal()).isSameAs(factorTwo.getPrincipal());
+		assertThat(result.getCredentials()).isSameAs(factorTwo.getCredentials());
+		assertThat(authorities).containsExactlyInAnyOrder("FACTOR_ONE", "FACTOR_TWO");
+	}
+
 }

+ 4 - 2
web/src/test/java/org/springframework/security/web/context/HttpSessionSecurityContextRepositoryTests.java

@@ -21,6 +21,7 @@ import java.lang.annotation.ElementType;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.lang.annotation.Target;
+import java.util.Collection;
 import java.util.Collections;
 
 import jakarta.servlet.Filter;
@@ -46,6 +47,7 @@ import org.springframework.security.authentication.TestAuthentication;
 import org.springframework.security.authentication.TestingAuthenticationToken;
 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
 import org.springframework.security.core.Authentication;
+import org.springframework.security.core.GrantedAuthority;
 import org.springframework.security.core.Transient;
 import org.springframework.security.core.authority.AuthorityUtils;
 import org.springframework.security.core.context.SecurityContext;
@@ -810,7 +812,7 @@ public class HttpSessionSecurityContextRepositoryTests {
 	private static class SomeTransientAuthentication extends AbstractAuthenticationToken {
 
 		SomeTransientAuthentication() {
-			super(null);
+			super((Collection<? extends GrantedAuthority>) null);
 		}
 
 		@Override
@@ -840,7 +842,7 @@ public class HttpSessionSecurityContextRepositoryTests {
 	private static class SomeOtherTransientAuthentication extends AbstractAuthenticationToken {
 
 		SomeOtherTransientAuthentication() {
-			super(null);
+			super((Collection<? extends GrantedAuthority>) null);
 		}
 
 		@Override

+ 44 - 0
webauthn/src/main/java/org/springframework/security/web/webauthn/authentication/WebAuthnAuthentication.java

@@ -48,6 +48,11 @@ public class WebAuthnAuthentication extends AbstractAuthenticationToken {
 		super.setAuthenticated(true);
 	}
 
+	private WebAuthnAuthentication(Builder<?> builder) {
+		super(builder);
+		this.principal = builder.principal;
+	}
+
 	@Override
 	public void setAuthenticated(boolean authenticated) {
 		Assert.isTrue(!authenticated, "Cannot set this token to trusted");
@@ -69,4 +74,43 @@ public class WebAuthnAuthentication extends AbstractAuthenticationToken {
 		return this.principal.getName();
 	}
 
+	@Override
+	public Builder<?> toBuilder() {
+		return new Builder<>(this);
+	}
+
+	/**
+	 * A builder of {@link WebAuthnAuthentication} instances
+	 *
+	 * @since 7.0
+	 */
+	public static final class Builder<B extends Builder<B>> extends AbstractAuthenticationBuilder<B> {
+
+		private PublicKeyCredentialUserEntity principal;
+
+		private Builder(WebAuthnAuthentication token) {
+			super(token);
+			this.principal = token.principal;
+		}
+
+		/**
+		 * Use this principal. It must be of type {@link PublicKeyCredentialUserEntity}
+		 * @param principal the principal to use
+		 * @return the {@link Builder} for further configurations
+		 */
+		@Override
+		public B principal(@Nullable Object principal) {
+			Assert.isInstanceOf(PublicKeyCredentialUserEntity.class, principal,
+					"principal must be of type PublicKeyCredentialUserEntity");
+			this.principal = (PublicKeyCredentialUserEntity) principal;
+			return (B) this;
+		}
+
+		@Override
+		public WebAuthnAuthentication build() {
+			return new WebAuthnAuthentication(this);
+		}
+
+	}
+
 }

+ 18 - 0
webauthn/src/test/java/org/springframework/security/web/webauthn/authentication/WebAuthnAuthenticationTests.java

@@ -17,6 +17,7 @@
 package org.springframework.security.web.webauthn.authentication;
 
 import java.util.List;
+import java.util.Set;
 
 import org.junit.jupiter.api.Test;
 
@@ -55,4 +56,21 @@ class WebAuthnAuthenticationTests {
 		assertThat(authentication.isAuthenticated()).isFalse();
 	}
 
+	@Test
+	void toBuilderWhenApplyThenCopies() {
+		PublicKeyCredentialUserEntity alice = TestPublicKeyCredentialUserEntities.userEntity().build();
+		WebAuthnAuthentication factorOne = new WebAuthnAuthentication(alice,
+				AuthorityUtils.createAuthorityList("FACTOR_ONE"));
+		PublicKeyCredentialUserEntity bob = TestPublicKeyCredentialUserEntities.userEntity().build();
+		WebAuthnAuthentication factorTwo = new WebAuthnAuthentication(bob,
+				AuthorityUtils.createAuthorityList("FACTOR_TWO"));
+		WebAuthnAuthentication result = factorOne.toBuilder()
+			.authorities((a) -> a.addAll(factorTwo.getAuthorities()))
+			.principal(factorTwo.getPrincipal())
+			.build();
+		Set<String> authorities = AuthorityUtils.authorityListToSet(result.getAuthorities());
+		assertThat(result.getPrincipal()).isSameAs(factorTwo.getPrincipal());
+		assertThat(authorities).containsExactlyInAnyOrder("FACTOR_ONE", "FACTOR_TWO");
+	}
+
 }