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

Add Authentication.Builder

This commit adds a new default method to Authentication
for the purposes of creating a Builder based on the current
authentication, allowing other authentications to be
applied to it as a composite.

It also adds Builders for each one of the authentication
result classes.

Issue gh-17861
Josh Cummings 1 сар өмнө
parent
commit
a201a2b862
27 өөрчлөгдсөн 1016 нэмэгдсэн , 1 устгасан
  1. 69 0
      cas/src/main/java/org/springframework/security/cas/authentication/CasAuthenticationToken.java
  2. 20 0
      cas/src/test/java/org/springframework/security/cas/authentication/CasAuthenticationTokenTests.java
  3. 34 0
      core/src/main/java/org/springframework/security/authentication/AbstractAuthenticationToken.java
  4. 47 0
      core/src/main/java/org/springframework/security/authentication/RememberMeAuthenticationToken.java
  5. 48 0
      core/src/main/java/org/springframework/security/authentication/TestingAuthenticationToken.java
  6. 44 0
      core/src/main/java/org/springframework/security/authentication/UsernamePasswordAuthenticationToken.java
  7. 46 0
      core/src/main/java/org/springframework/security/authentication/jaas/JaasAuthenticationToken.java
  8. 42 0
      core/src/main/java/org/springframework/security/authentication/ott/OneTimeTokenAuthentication.java
  9. 53 1
      core/src/main/java/org/springframework/security/core/Authentication.java
  10. 53 0
      core/src/main/java/org/springframework/security/core/NoopAuthenticationBuilder.java
  11. 61 0
      core/src/test/java/org/springframework/security/authentication/AbstractAuthenticationBuilderTests.java
  12. 15 0
      core/src/test/java/org/springframework/security/authentication/TestingAuthenticationTokenTests.java
  13. 16 0
      core/src/test/java/org/springframework/security/authentication/UsernamePasswordAuthenticationTokenTests.java
  14. 46 0
      core/src/test/java/org/springframework/security/authentication/jaas/JaasAuthenticationTokenTests.java
  15. 41 0
      core/src/test/java/org/springframework/security/authentication/ott/OneTimeTokenAuthenticationTests.java
  16. 42 0
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthenticationToken.java
  17. 16 0
      oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthenticationTokenTests.java
  18. 43 0
      oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/BearerTokenAuthentication.java
  19. 42 0
      oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationToken.java
  20. 18 0
      oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/BearerTokenAuthenticationTests.java
  21. 14 0
      oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationTokenTests.java
  22. 55 0
      saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AssertionAuthentication.java
  23. 44 0
      saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/Saml2AssertionAuthenticationTests.java
  24. 43 0
      web/src/main/java/org/springframework/security/web/authentication/preauth/PreAuthenticatedAuthenticationToken.java
  25. 14 0
      web/src/test/java/org/springframework/security/web/authentication/preauth/PreAuthenticatedAuthenticationTokenTests.java
  26. 35 0
      webauthn/src/main/java/org/springframework/security/web/webauthn/authentication/WebAuthnAuthentication.java
  27. 15 0
      webauthn/src/test/java/org/springframework/security/web/webauthn/authentication/WebAuthnAuthenticationTests.java

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

@@ -20,8 +20,10 @@ import java.io.Serializable;
 import java.util.Collection;
 
 import org.apereo.cas.client.validation.Assertion;
+import org.jspecify.annotations.NonNull;
 
 import org.springframework.security.authentication.AbstractAuthenticationToken;
+import org.springframework.security.core.Authentication;
 import org.springframework.security.core.GrantedAuthority;
 import org.springframework.security.core.userdetails.UserDetails;
 import org.springframework.util.Assert;
@@ -153,6 +155,11 @@ public class CasAuthenticationToken extends AbstractAuthenticationToken implemen
 		return this.userDetails;
 	}
 
+	@Override
+	public Builder toBuilder() {
+		return new Builder().apply(this);
+	}
+
 	@Override
 	public String toString() {
 		StringBuilder sb = new StringBuilder();
@@ -162,4 +169,66 @@ public class CasAuthenticationToken extends AbstractAuthenticationToken implemen
 		return (sb.toString());
 	}
 
+	/**
+	 * A builder preserving the concrete {@link Authentication} type
+	 *
+	 * @since 7.0
+	 */
+	public static final class Builder extends AbstractAuthenticationBuilder<@NonNull CasAuthenticationToken, Builder> {
+
+		private Integer keyHash;
+
+		private Object principal;
+
+		private Object credentials;
+
+		private UserDetails userDetails;
+
+		private Assertion assertion;
+
+		private Builder() {
+
+		}
+
+		public Builder apply(CasAuthenticationToken authentication) {
+			return super.apply(authentication).keyHash(authentication.keyHash)
+				.principal(authentication.principal)
+				.credentials(authentication.credentials)
+				.userDetails(authentication.userDetails)
+				.assertion(authentication.assertion);
+		}
+
+		public Builder keyHash(Integer keyHash) {
+			this.keyHash = keyHash;
+			return this;
+		}
+
+		public Builder principal(Object principal) {
+			this.principal = principal;
+			return this;
+		}
+
+		public Builder credentials(Object credentials) {
+			this.credentials = credentials;
+			return this;
+		}
+
+		public Builder userDetails(UserDetails userDetails) {
+			this.userDetails = userDetails;
+			return this;
+		}
+
+		public Builder assertion(Assertion assertion) {
+			this.assertion = assertion;
+			return this;
+		}
+
+		@Override
+		protected @NonNull CasAuthenticationToken build(Collection<GrantedAuthority> authorities) {
+			return new CasAuthenticationToken(this.keyHash, this.principal, this.credentials, authorities,
+					this.userDetails, this.assertion);
+		}
+
+	}
+
 }

+ 20 - 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,22 @@ 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().apply(factorTwo).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");
+	}
+
 }

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

@@ -20,6 +20,8 @@ import java.security.Principal;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.HashSet;
+import java.util.function.Consumer;
 
 import org.jspecify.annotations.Nullable;
 
@@ -185,4 +187,36 @@ public abstract class AbstractAuthenticationToken implements Authentication, Cre
 		return sb.toString();
 	}
 
+	protected abstract static class AbstractAuthenticationBuilder<A extends Authentication, B extends AbstractAuthenticationBuilder<A, B>>
+			implements Builder<A, B> {
+
+		private final Collection<GrantedAuthority> authorities = new HashSet<>();
+
+		protected AbstractAuthenticationBuilder() {
+
+		}
+
+		@Override
+		public B authorities(Consumer<Collection<GrantedAuthority>> authorities) {
+			authorities.accept(this.authorities);
+			return (B) this;
+		}
+
+		@Override
+		public A build() {
+			return build(this.authorities);
+		}
+
+		@Override
+		public B apply(Authentication token) {
+			Assert.isTrue(token.isAuthenticated(), "cannot mutate an unauthenticated token");
+			Assert.notNull(token.getPrincipal(), "principal cannot be null");
+			this.authorities.addAll(token.getAuthorities());
+			return (B) this;
+		}
+
+		protected abstract A build(Collection<GrantedAuthority> authorities);
+
+	}
+
 }

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

@@ -18,7 +18,11 @@ package org.springframework.security.authentication;
 
 import java.util.Collection;
 
+import org.jspecify.annotations.Nullable;
+
+import org.springframework.security.core.Authentication;
 import org.springframework.security.core.GrantedAuthority;
+import org.springframework.util.Assert;
 
 /**
  * Represents a remembered <code>Authentication</code>.
@@ -88,6 +92,11 @@ public class RememberMeAuthenticationToken extends AbstractAuthenticationToken {
 		return this.principal;
 	}
 
+	@Override
+	public Builder toBuilder() {
+		return new Builder().apply(this);
+	}
+
 	@Override
 	public boolean equals(Object obj) {
 		if (!super.equals(obj)) {
@@ -106,4 +115,42 @@ public class RememberMeAuthenticationToken extends AbstractAuthenticationToken {
 		return result;
 	}
 
+	/**
+	 * A builder preserving the concrete {@link Authentication} type
+	 *
+	 * @since 7.0
+	 */
+	public static final class Builder extends AbstractAuthenticationBuilder<RememberMeAuthenticationToken, Builder> {
+
+		private @Nullable Integer keyHash;
+
+		private @Nullable Object principal;
+
+		private Builder() {
+
+		}
+
+		public Builder apply(RememberMeAuthenticationToken token) {
+			return super.apply(token).keyHash(token.getKeyHash()).principal(token.getPrincipal());
+		}
+
+		public Builder principal(Object principal) {
+			this.principal = principal;
+			return this;
+		}
+
+		public Builder keyHash(int keyHash) {
+			this.keyHash = keyHash;
+			return this;
+		}
+
+		@Override
+		protected RememberMeAuthenticationToken build(Collection<GrantedAuthority> authorities) {
+			Assert.notNull(this.keyHash, "keyHash cannot be null");
+			Assert.notNull(this.principal, "principal cannot be null");
+			return new RememberMeAuthenticationToken(this.keyHash, this.principal, authorities);
+		}
+
+	}
+
 }

+ 48 - 0
core/src/main/java/org/springframework/security/authentication/TestingAuthenticationToken.java

@@ -19,8 +19,12 @@ package org.springframework.security.authentication;
 import java.util.Collection;
 import java.util.List;
 
+import org.jspecify.annotations.Nullable;
+
+import org.springframework.security.core.Authentication;
 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
@@ -71,4 +75,48 @@ public class TestingAuthenticationToken extends AbstractAuthenticationToken {
 		return this.principal;
 	}
 
+	@Override
+	public Builder toBuilder() {
+		return new Builder().apply(this);
+	}
+
+	/**
+	 * A builder preserving the concrete {@link Authentication} type
+	 *
+	 * @since 7.0
+	 */
+	public static final class Builder extends AbstractAuthenticationBuilder<TestingAuthenticationToken, Builder> {
+
+		private @Nullable Object principal;
+
+		private @Nullable Object credentials;
+
+		private Builder() {
+
+		}
+
+		public Builder apply(TestingAuthenticationToken authentication) {
+			return super.apply(authentication).principal(authentication.getPrincipal())
+				.credentials(authentication.getCredentials());
+		}
+
+		public Builder principal(Object principal) {
+			this.principal = principal;
+			return this;
+		}
+
+		public Builder credentials(Object credentials) {
+			this.credentials = credentials;
+			return this;
+		}
+
+		@Override
+		protected TestingAuthenticationToken build(Collection<GrantedAuthority> authorities) {
+			Assert.notNull(this.principal, "principal cannot be null");
+			Assert.notNull(this.credentials, "credentials cannot be null");
+			return new TestingAuthenticationToken(this.principal, this.credentials, authorities);
+		}
+
+	}
+
 }

+ 44 - 0
core/src/main/java/org/springframework/security/authentication/UsernamePasswordAuthenticationToken.java

@@ -20,6 +20,7 @@ import java.util.Collection;
 
 import org.jspecify.annotations.Nullable;
 
+import org.springframework.security.core.Authentication;
 import org.springframework.security.core.GrantedAuthority;
 import org.springframework.util.Assert;
 
@@ -124,4 +125,47 @@ public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationT
 		this.credentials = null;
 	}
 
+	@Override
+	public Builder<?, ?> toBuilder() {
+		return new Builder<>().apply(this);
+	}
+
+	/**
+	 * A builder preserving the concrete {@link Authentication} type
+	 *
+	 * @since 7.0
+	 */
+	public static class Builder<A extends UsernamePasswordAuthenticationToken, B extends Builder<A, B>>
+			extends AbstractAuthenticationBuilder<A, B> {
+
+		private @Nullable Object principal;
+
+		private @Nullable Object credentials;
+
+		protected Builder() {
+		}
+
+		public B apply(UsernamePasswordAuthenticationToken authentication) {
+			return super.apply(authentication).principal(authentication.getPrincipal())
+				.credentials(authentication.getCredentials());
+		}
+
+		public B principal(Object principal) {
+			this.principal = principal;
+			return (B) this;
+		}
+
+		public B credentials(@Nullable Object credentials) {
+			this.credentials = credentials;
+			return (B) this;
+		}
+
+		@Override
+		protected A build(Collection<GrantedAuthority> authorities) {
+			Assert.notNull(this.principal, "principal cannot be null");
+			return (A) new UsernamePasswordAuthenticationToken(this.principal, this.credentials, authorities);
+		}
+
+	}
+
 }

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

@@ -16,6 +16,7 @@
 
 package org.springframework.security.authentication.jaas;
 
+import java.util.Collection;
 import java.util.List;
 
 import javax.security.auth.login.LoginContext;
@@ -23,7 +24,9 @@ import javax.security.auth.login.LoginContext;
 import org.jspecify.annotations.Nullable;
 
 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
 import org.springframework.security.core.GrantedAuthority;
+import org.springframework.util.Assert;
 
 /**
  * UsernamePasswordAuthenticationToken extension to carry the Jaas LoginContext that the
@@ -52,4 +55,47 @@ public class JaasAuthenticationToken extends UsernamePasswordAuthenticationToken
 		return this.loginContext;
 	}
 
+	@Override
+	public Builder toBuilder() {
+		return new Builder().apply(this);
+	}
+
+	/**
+	 * A builder preserving the concrete {@link Authentication} type
+	 *
+	 * @since 7.0
+	 */
+	public static final class Builder
+			extends UsernamePasswordAuthenticationToken.Builder<JaasAuthenticationToken, Builder> {
+
+		private @Nullable LoginContext loginContext;
+
+		private Builder() {
+
+		}
+
+		public Builder apply(JaasAuthenticationToken authentication) {
+			return super.apply(authentication).loginContext(authentication.getLoginContext());
+		}
+
+		/**
+		 * Use this {@link LoginContext}
+		 * @param loginContext the {@link LoginContext} to use
+		 * @return the {@link Builder} for further configuration
+		 */
+		public Builder loginContext(LoginContext loginContext) {
+			this.loginContext = loginContext;
+			return this;
+		}
+
+		@Override
+		protected JaasAuthenticationToken build(Collection<GrantedAuthority> authorities) {
+			UsernamePasswordAuthenticationToken token = super.build(authorities);
+			Assert.notNull(this.loginContext, "loginContext cannot be null");
+			return new JaasAuthenticationToken(token.getPrincipal(), token.getCredentials(),
+					(List<GrantedAuthority>) token.getAuthorities(), this.loginContext);
+		}
+
+	}
+
 }

+ 42 - 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
@@ -53,4 +54,45 @@ public class OneTimeTokenAuthentication extends AbstractAuthenticationToken {
 		return null;
 	}
 
+	@Override
+	public Builder toBuilder() {
+		return new Builder().apply(this);
+	}
+
+	/**
+	 * A builder for constructing a {@link OneTimeTokenAuthentication} instance
+	 */
+	public static final class Builder extends AbstractAuthenticationBuilder<OneTimeTokenAuthentication, Builder> {
+
+		private @Nullable Object principal;
+
+		private Builder() {
+
+		}
+
+		/**
+		 * Apply this {@link OneTimeTokenAuthentication}
+		 * @return the {@link Builder} for further configuration
+		 */
+		public Builder apply(OneTimeTokenAuthentication authentication) {
+			return super.apply(authentication).principal(authentication.principal);
+		}
+
+		/**
+		 * Use this principal
+		 * @return the {@link Builder} for further configuration
+		 */
+		public Builder principal(Object principal) {
+			this.principal = principal;
+			return this;
+		}
+
+		@Override
+		protected OneTimeTokenAuthentication build(Collection<GrantedAuthority> authorities) {
+			Assert.notNull(this.principal, "principal cannot be null");
+			return new OneTimeTokenAuthentication(this.principal, authorities);
+		}
+
+	}
+
 }

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

@@ -16,14 +16,17 @@
 
 package org.springframework.security.core;
 
+import java.io.Serial;
 import java.io.Serializable;
 import java.security.Principal;
 import java.util.Collection;
+import java.util.function.Consumer;
 
 import org.jspecify.annotations.Nullable;
 
 import org.springframework.security.authentication.AuthenticationManager;
 import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.util.Assert;
 
 /**
  * Represents the token for an authentication request or for an authenticated principal
@@ -54,6 +57,9 @@ import org.springframework.security.core.context.SecurityContextHolder;
  */
 public interface Authentication extends Principal, Serializable {
 
+	@Serial
+	long serialVersionUID = -3884394378624019849L;
+
 	/**
 	 * Set by an <code>AuthenticationManager</code> to indicate the authorities that the
 	 * principal has been granted. Note that classes should not rely on this value as
@@ -64,7 +70,7 @@ public interface Authentication extends Principal, Serializable {
 	 * instance.
 	 * </p>
 	 * @return the authorities granted to the principal, or an empty collection if the
-	 * token has not been authenticated. Never null.
+	 * token has not been authenticated. Never null.Saml2AssertAu
 	 */
 	Collection<? extends GrantedAuthority> getAuthorities();
 
@@ -136,4 +142,50 @@ public interface Authentication extends Principal, Serializable {
 	 */
 	void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
 
+	/**
+	 * Return an {@link Builder} based on this instance
+	 * @return an {@link Builder} for building a new {@link Authentication} based on this
+	 * instance
+	 * @since 7.0
+	 */
+	default Builder<?, ?> toBuilder() {
+		return new NoopAuthenticationBuilder<>(this);
+	}
+
+	/**
+	 * A builder based on a given {@link Authentication} instance
+	 *
+	 * @param <A> the type of {@link Authentication}
+	 * @author Josh Cummings
+	 * @since 7.0
+	 */
+	interface Builder<A extends Authentication, B extends Builder<A, B>> {
+
+		/**
+		 * Apply this {@link Authentication} to the builder.
+		 * <p>
+		 * By default, this method adds the authorities from {@code authentication} to
+		 * this builder
+		 * @return the {@link Builder} for further configuration
+		 */
+		default B apply(Authentication authentication) {
+			Assert.isTrue(authentication.isAuthenticated(), "cannot apply an unauthenticated token");
+			return authorities((a) -> a.addAll(authentication.getAuthorities()));
+		}
+
+		/**
+		 * Apply these authorities to the builder.
+		 * @param authorities the authorities to apply
+		 * @return the {@link Builder} for further configuration
+		 */
+		B authorities(Consumer<Collection<GrantedAuthority>> authorities);
+
+		/**
+		 * Build an {@link Authentication} instance
+		 * @return the {@link Authentication} instance
+		 */
+		A build();
+
+	}
+
 }

+ 53 - 0
core/src/main/java/org/springframework/security/core/NoopAuthenticationBuilder.java

@@ -0,0 +1,53 @@
+/*
+ * 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.util.Collection;
+import java.util.function.Consumer;
+
+import org.springframework.util.Assert;
+
+/**
+ * An adapter implementation of {@link Authentication.Builder} that provides a no-op
+ * implementation for the principal, credentials, and authorities
+ *
+ * @param <A> the type of {@link Authentication}
+ * @author Josh Cummings
+ * @since 7.0
+ */
+class NoopAuthenticationBuilder<A extends Authentication>
+		implements Authentication.Builder<A, NoopAuthenticationBuilder<A>> {
+
+	private A original;
+
+	NoopAuthenticationBuilder(A authentication) {
+		Assert.isTrue(authentication.isAuthenticated(), "cannot mutate an unauthenticated token");
+		Assert.notNull(authentication.getPrincipal(), "principal cannot be null");
+		this.original = authentication;
+	}
+
+	@Override
+	public NoopAuthenticationBuilder<A> authorities(Consumer<Collection<GrantedAuthority>> authorities) {
+		return this;
+	}
+
+	@Override
+	public A build() {
+		return this.original;
+	}
+
+}

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

@@ -0,0 +1,61 @@
+/*
+ * 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.Collection;
+import java.util.Set;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.security.authentication.AbstractAuthenticationToken.AbstractAuthenticationBuilder;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.AuthorityUtils;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+
+class AbstractAuthenticationBuilderTests {
+
+	@Test
+	void applyWhenUnauthenticatedThenErrors() {
+		TestAbstractAuthenticationBuilder builder = new TestAbstractAuthenticationBuilder();
+		TestingAuthenticationToken unauthenticated = new TestingAuthenticationToken("user", "password");
+		assertThatIllegalArgumentException().isThrownBy(() -> builder.apply(unauthenticated));
+	}
+
+	@Test
+	void applyWhenAuthoritiesThenAdds() {
+		TestAbstractAuthenticationBuilder builder = new TestAbstractAuthenticationBuilder();
+		TestingAuthenticationToken factorOne = new TestingAuthenticationToken("user", "pass", "FACTOR_ONE");
+		TestingAuthenticationToken factorTwo = new TestingAuthenticationToken("user", "pass", "FACTOR_TWO");
+		Authentication result = builder.apply(factorOne).apply(factorTwo).build();
+		Set<String> authorities = AuthorityUtils.authorityListToSet(result.getAuthorities());
+		assertThat(authorities).containsExactlyInAnyOrder("FACTOR_ONE", "FACTOR_TWO");
+	}
+
+	private static final class TestAbstractAuthenticationBuilder
+			extends AbstractAuthenticationBuilder<Authentication, TestAbstractAuthenticationBuilder> {
+
+		@Override
+		protected Authentication build(Collection<GrantedAuthority> authorities) {
+			return new TestingAuthenticationToken("user", "password", authorities);
+		}
+
+	}
+
+}

+ 15 - 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,17 @@ 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().apply(factorTwo).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");
+	}
+
 }

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

@@ -16,8 +16,11 @@
 
 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;
@@ -85,4 +88,17 @@ 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"));
+		Authentication authentication = factorOne.toBuilder().apply(factorTwo).build();
+		Set<String> authorities = AuthorityUtils.authorityListToSet(authentication.getAuthorities());
+		assertThat(authentication.getPrincipal()).isEqualTo("bob");
+		assertThat(authentication.getCredentials()).isEqualTo("ssap");
+		assertThat(authorities).containsExactlyInAnyOrder("FACTOR_ONE", "FACTOR_TWO");
+	}
+
 }

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

@@ -0,0 +1,46 @@
+/*
+ * 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().apply(factorTwo).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");
+	}
+
+}

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

@@ -0,0 +1,41 @@
+/*
+ * 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().apply(factorTwo).build();
+		Set<String> authorities = AuthorityUtils.authorityListToSet(result.getAuthorities());
+		assertThat(result.getPrincipal()).isSameAs(factorTwo.getPrincipal());
+		assertThat(authorities).containsExactlyInAnyOrder("FACTOR_ONE", "FACTOR_TWO");
+	}
+
+}

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

@@ -85,4 +85,46 @@ public class OAuth2AuthenticationToken extends AbstractAuthenticationToken {
 		return this.authorizedClientRegistrationId;
 	}
 
+	@Override
+	public Builder toBuilder() {
+		return new Builder().apply(this);
+	}
+
+	/**
+	 * A builder preserving the concrete {@link Authentication} type
+	 *
+	 * @since 7.0
+	 */
+	public static final class Builder extends AbstractAuthenticationBuilder<OAuth2AuthenticationToken, Builder> {
+
+		private OAuth2User principal;
+
+		private String authorizedClientRegistrationId;
+
+		private Builder() {
+
+		}
+
+		public Builder apply(OAuth2AuthenticationToken authentication) {
+			return super.apply(authentication).principal(authentication.getPrincipal())
+				.authorizedClientRegistrationId(authentication.authorizedClientRegistrationId);
+		}
+
+		public Builder principal(OAuth2User principal) {
+			this.principal = principal;
+			return this;
+		}
+
+		public Builder authorizedClientRegistrationId(String authorizedClientRegistrationId) {
+			this.authorizedClientRegistrationId = authorizedClientRegistrationId;
+			return this;
+		}
+
+		@Override
+		protected OAuth2AuthenticationToken build(Collection<GrantedAuthority> authorities) {
+			return new OAuth2AuthenticationToken(this.principal, authorities, this.authorizedClientRegistrationId);
+		}
+
+	}
+
 }

+ 16 - 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,17 @@ 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().apply(factorTwo).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");
+	}
+
 }

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

@@ -21,6 +21,7 @@ import java.util.Collections;
 import java.util.LinkedHashMap;
 import java.util.Map;
 
+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;
@@ -61,4 +62,46 @@ public class BearerTokenAuthentication extends AbstractOAuth2TokenAuthentication
 		return this.attributes;
 	}
 
+	@Override
+	public Builder toBuilder() {
+		return new Builder().apply(this);
+	}
+
+	/**
+	 * A builder preserving the concrete {@link Authentication} type
+	 *
+	 * @since 7.0
+	 */
+	public static final class Builder extends AbstractAuthenticationBuilder<BearerTokenAuthentication, Builder> {
+
+		private OAuth2AuthenticatedPrincipal principal;
+
+		private OAuth2AccessToken token;
+
+		private Builder() {
+
+		}
+
+		public Builder apply(BearerTokenAuthentication authentication) {
+			return super.apply(authentication).principal((OAuth2AuthenticatedPrincipal) authentication.getPrincipal())
+				.credentials(authentication.getToken());
+		}
+
+		public Builder principal(OAuth2AuthenticatedPrincipal principal) {
+			this.principal = principal;
+			return this;
+		}
+
+		public Builder credentials(OAuth2AccessToken credentials) {
+			this.token = credentials;
+			return this;
+		}
+
+		@Override
+		protected BearerTokenAuthentication build(Collection<GrantedAuthority> authorities) {
+			return new BearerTokenAuthentication(this.principal, this.token, authorities);
+		}
+
+	}
+
 }

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

@@ -19,6 +19,7 @@ package org.springframework.security.oauth2.server.resource.authentication;
 import java.util.Collection;
 import java.util.Map;
 
+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;
@@ -84,4 +85,45 @@ public class JwtAuthenticationToken extends AbstractOAuth2TokenAuthenticationTok
 		return this.name;
 	}
 
+	@Override
+	public Builder toBuilder() {
+		return new Builder().apply(this);
+	}
+
+	/**
+	 * A builder preserving the concrete {@link Authentication} type
+	 *
+	 * @since 7.0
+	 */
+	public static final class Builder extends AbstractAuthenticationBuilder<JwtAuthenticationToken, Builder> {
+
+		private Jwt jwt;
+
+		private String name;
+
+		private Builder() {
+
+		}
+
+		public Builder apply(JwtAuthenticationToken token) {
+			return super.apply(token).jwt(token.getToken()).name(token.getName());
+		}
+
+		public Builder jwt(Jwt jwt) {
+			this.jwt = jwt;
+			return this;
+		}
+
+		public Builder name(String name) {
+			this.name = name;
+			return this;
+		}
+
+		@Override
+		protected JwtAuthenticationToken build(Collection<GrantedAuthority> authorities) {
+			return new JwtAuthenticationToken(this.jwt, authorities, this.name);
+		}
+
+	}
+
 }

+ 18 - 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,20 @@ 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().apply(factorTwo).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");
+	}
+
 }

+ 14 - 0
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;
@@ -115,6 +116,19 @@ 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().apply(factorTwo).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);
 	}

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

@@ -19,6 +19,9 @@ package org.springframework.security.saml2.provider.service.authentication;
 import java.io.Serial;
 import java.util.Collection;
 
+import org.jspecify.annotations.NonNull;
+
+import org.springframework.security.core.Authentication;
 import org.springframework.security.core.GrantedAuthority;
 
 /**
@@ -62,4 +65,56 @@ public class Saml2AssertionAuthentication extends Saml2Authentication {
 		return this.relyingPartyRegistrationId;
 	}
 
+	@Override
+	public Builder toBuilder() {
+		return new Builder().apply(this);
+	}
+
+	/**
+	 * A builder preserving the concrete {@link Authentication} type
+	 *
+	 * @since 7.0
+	 */
+	public static final class Builder
+			extends AbstractAuthenticationBuilder<@NonNull Saml2AssertionAuthentication, @NonNull Builder> {
+
+		private Object principal;
+
+		private Saml2ResponseAssertionAccessor assertion;
+
+		private String relyingPartyRegistrationId;
+
+		private Builder() {
+
+		}
+
+		public Builder apply(Saml2AssertionAuthentication authentication) {
+			return super.apply(authentication).principal(authentication.getPrincipal())
+				.assertion(authentication.assertion)
+				.relyingPartyRegistrationId(authentication.relyingPartyRegistrationId);
+		}
+
+		public Builder principal(Object principal) {
+			this.principal = principal;
+			return this;
+		}
+
+		public Builder assertion(Saml2ResponseAssertionAccessor assertion) {
+			this.assertion = assertion;
+			return this;
+		}
+
+		public Builder relyingPartyRegistrationId(String relyingPartyRegistrationId) {
+			this.relyingPartyRegistrationId = relyingPartyRegistrationId;
+			return this;
+		}
+
+		@Override
+		protected Saml2AssertionAuthentication build(Collection<GrantedAuthority> authorities) {
+			return new Saml2AssertionAuthentication(this.principal, this.assertion, authorities,
+					this.relyingPartyRegistrationId);
+		}
+
+	}
+
 }

+ 44 - 0
saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/Saml2AssertionAuthenticationTests.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.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("alice").build(), AuthorityUtils.createAuthorityList("FACTOR_TWO"), "bob");
+		Saml2AssertionAuthentication result = factorOne.toBuilder().apply(factorTwo).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");
+	}
+
+}

+ 43 - 0
web/src/main/java/org/springframework/security/web/authentication/preauth/PreAuthenticatedAuthenticationToken.java

@@ -21,6 +21,7 @@ import java.util.Collection;
 import org.jspecify.annotations.Nullable;
 
 import org.springframework.security.authentication.AbstractAuthenticationToken;
+import org.springframework.security.core.Authentication;
 import org.springframework.security.core.GrantedAuthority;
 
 /**
@@ -82,4 +83,46 @@ public class PreAuthenticatedAuthenticationToken extends AbstractAuthenticationT
 		return this.principal;
 	}
 
+	@Override
+	public Builder toBuilder() {
+		return new Builder().apply(this);
+	}
+
+	/**
+	 * A builder preserving the concrete {@link Authentication} type
+	 *
+	 * @since 7.0
+	 */
+	public static final class Builder
+			extends AbstractAuthenticationBuilder<PreAuthenticatedAuthenticationToken, Builder> {
+
+		private Object principal;
+
+		private Object credentials;
+
+		private Builder() {
+
+		}
+
+		public Builder apply(PreAuthenticatedAuthenticationToken token) {
+			return super.apply(token).principal(token.getPrincipal()).credentials(token.getCredentials());
+		}
+
+		public Builder principal(Object principal) {
+			this.principal = principal;
+			return this;
+		}
+
+		public Builder credentials(Object credentials) {
+			this.credentials = credentials;
+			return this;
+		}
+
+		@Override
+		protected PreAuthenticatedAuthenticationToken build(Collection<GrantedAuthority> authorities) {
+			return new PreAuthenticatedAuthenticationToken(this.principal, this.credentials, authorities);
+		}
+
+	}
+
 }

+ 14 - 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,17 @@ 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().apply(factorTwo).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");
+	}
+
 }

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

@@ -22,6 +22,7 @@ import java.util.Collection;
 import org.jspecify.annotations.Nullable;
 
 import org.springframework.security.authentication.AbstractAuthenticationToken;
+import org.springframework.security.core.Authentication;
 import org.springframework.security.core.GrantedAuthority;
 import org.springframework.security.web.webauthn.api.PublicKeyCredentialUserEntity;
 import org.springframework.util.Assert;
@@ -69,4 +70,38 @@ public class WebAuthnAuthentication extends AbstractAuthenticationToken {
 		return this.principal.getName();
 	}
 
+	@Override
+	public Builder toBuilder() {
+		return new Builder().apply(this);
+	}
+
+	/**
+	 * A builder preserving the concrete {@link Authentication} type
+	 *
+	 * @since 7.0
+	 */
+	public static final class Builder extends AbstractAuthenticationBuilder<WebAuthnAuthentication, Builder> {
+
+		private PublicKeyCredentialUserEntity principal;
+
+		private Builder() {
+
+		}
+
+		public Builder apply(WebAuthnAuthentication authentication) {
+			return super.apply(authentication).principal(authentication.getPrincipal());
+		}
+
+		public Builder principal(PublicKeyCredentialUserEntity principal) {
+			this.principal = principal;
+			return this;
+		}
+
+		@Override
+		protected WebAuthnAuthentication build(Collection<GrantedAuthority> authorities) {
+			return new WebAuthnAuthentication(this.principal, authorities);
+		}
+
+	}
+
 }

+ 15 - 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,18 @@ 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().apply(factorTwo).build();
+		Set<String> authorities = AuthorityUtils.authorityListToSet(result.getAuthorities());
+		assertThat(result.getPrincipal()).isSameAs(factorTwo.getPrincipal());
+		assertThat(authorities).containsExactlyInAnyOrder("FACTOR_ONE", "FACTOR_TWO");
+	}
+
 }