Browse Source

Polish Builders

- Added remaining properties
- Removed apply method since Spring Security isn't using
it right now
- Made builders extensible since the authentications are
extensible

Issue gh-17861
Josh Cummings 3 weeks ago
parent
commit
a0fe6a5fee
37 changed files with 609 additions and 326 deletions
  1. 40 27
      cas/src/main/java/org/springframework/security/cas/authentication/CasAuthenticationToken.java
  2. 50 1
      cas/src/main/java/org/springframework/security/cas/authentication/CasServiceTicketAuthenticationToken.java
  3. 8 1
      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. 27 15
      core/src/main/java/org/springframework/security/authentication/AbstractAuthenticationToken.java
  7. 22 18
      core/src/main/java/org/springframework/security/authentication/RememberMeAuthenticationToken.java
  8. 26 21
      core/src/main/java/org/springframework/security/authentication/TestingAuthenticationToken.java
  9. 22 17
      core/src/main/java/org/springframework/security/authentication/UsernamePasswordAuthenticationToken.java
  10. 17 19
      core/src/main/java/org/springframework/security/authentication/jaas/JaasAuthenticationToken.java
  11. 18 19
      core/src/main/java/org/springframework/security/authentication/ott/OneTimeTokenAuthentication.java
  12. 14 26
      core/src/main/java/org/springframework/security/core/Authentication.java
  13. 26 10
      core/src/main/java/org/springframework/security/core/NoopAuthenticationBuilder.java
  14. 15 15
      core/src/test/java/org/springframework/security/authentication/AbstractAuthenticationBuilderTests.java
  15. 5 1
      core/src/test/java/org/springframework/security/authentication/TestingAuthenticationTokenTests.java
  16. 8 5
      core/src/test/java/org/springframework/security/authentication/UsernamePasswordAuthenticationTokenTests.java
  17. 6 1
      core/src/test/java/org/springframework/security/authentication/jaas/JaasAuthenticationTokenTests.java
  18. 4 1
      core/src/test/java/org/springframework/security/authentication/ott/OneTimeTokenAuthenticationTests.java
  19. 3 1
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/OAuth2AuthorizeRequest.java
  20. 25 16
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthenticationToken.java
  21. 3 1
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunction.java
  22. 5 1
      oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthenticationTokenTests.java
  23. 54 0
      oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/AbstractOAuth2TokenAuthenticationToken.java
  24. 28 21
      oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/BearerTokenAuthentication.java
  25. 15 20
      oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationToken.java
  26. 6 0
      oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/authentication/BearerTokenAuthenticationFilter.java
  27. 5 1
      oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/BearerTokenAuthenticationTests.java
  28. 6 2
      oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationTokenTests.java
  29. 23 0
      oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/authentication/BearerTokenAuthenticationFilterTests.java
  30. 26 28
      saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AssertionAuthentication.java
  31. 33 0
      saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2Authentication.java
  32. 6 1
      saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/Saml2AssertionAuthenticationTests.java
  33. 25 18
      web/src/main/java/org/springframework/security/web/authentication/preauth/PreAuthenticatedAuthenticationToken.java
  34. 5 1
      web/src/test/java/org/springframework/security/web/authentication/preauth/PreAuthenticatedAuthenticationTokenTests.java
  35. 4 2
      web/src/test/java/org/springframework/security/web/context/HttpSessionSecurityContextRepositoryTests.java
  36. 17 13
      webauthn/src/main/java/org/springframework/security/web/webauthn/authentication/WebAuthnAuthentication.java
  37. 4 1
      webauthn/src/test/java/org/springframework/security/web/webauthn/authentication/WebAuthnAuthenticationTests.java

+ 40 - 27
cas/src/main/java/org/springframework/security/cas/authentication/CasAuthenticationToken.java

@@ -20,7 +20,7 @@ import java.io.Serializable;
 import java.util.Collection;
 
 import org.apereo.cas.client.validation.Assertion;
-import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
 
 import org.springframework.security.authentication.AbstractAuthenticationToken;
 import org.springframework.security.core.Authentication;
@@ -106,6 +106,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();
@@ -156,8 +169,8 @@ public class CasAuthenticationToken extends AbstractAuthenticationToken implemen
 	}
 
 	@Override
-	public Builder toBuilder() {
-		return new Builder().apply(this);
+	public Builder<?> toBuilder() {
+		return new Builder<>(this);
 	}
 
 	@Override
@@ -174,7 +187,7 @@ public class CasAuthenticationToken extends AbstractAuthenticationToken implemen
 	 *
 	 * @since 7.0
 	 */
-	public static final class Builder extends AbstractAuthenticationBuilder<@NonNull CasAuthenticationToken, Builder> {
+	public static class Builder<B extends Builder<B>> extends AbstractAuthenticationBuilder<Object, Object, B> {
 
 		private Integer keyHash;
 
@@ -186,47 +199,47 @@ public class CasAuthenticationToken extends AbstractAuthenticationToken implemen
 
 		private Assertion assertion;
 
-		private Builder() {
-
+		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;
 		}
 
-		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) {
+		public B keyHash(Integer keyHash) {
 			this.keyHash = keyHash;
-			return this;
+			return (B) this;
 		}
 
-		public Builder principal(Object principal) {
+		@Override
+		public B principal(@Nullable Object principal) {
+			Assert.notNull(principal, "principal cannot be null");
 			this.principal = principal;
-			return this;
+			return (B) this;
 		}
 
-		public Builder credentials(Object credentials) {
+		@Override
+		public B credentials(@Nullable Object credentials) {
+			Assert.notNull(credentials, "credentials cannot be null");
 			this.credentials = credentials;
-			return this;
+			return (B) this;
 		}
 
-		public Builder userDetails(UserDetails userDetails) {
+		public B userDetails(UserDetails userDetails) {
 			this.userDetails = userDetails;
-			return this;
+			return (B) this;
 		}
 
-		public Builder assertion(Assertion assertion) {
+		public B assertion(Assertion assertion) {
 			this.assertion = assertion;
-			return this;
+			return (B) this;
 		}
 
 		@Override
-		protected @NonNull CasAuthenticationToken build(Collection<GrantedAuthority> authorities) {
-			return new CasAuthenticationToken(this.keyHash, this.principal, this.credentials, authorities,
-					this.userDetails, this.assertion);
+		public CasAuthenticationToken build() {
+			return new CasAuthenticationToken(this);
 		}
 
 	}

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

@@ -22,6 +22,7 @@ import java.util.Collection;
 import org.jspecify.annotations.Nullable;
 
 import org.springframework.security.authentication.AbstractAuthenticationToken;
+import org.springframework.security.core.Authentication;
 import org.springframework.security.core.GrantedAuthority;
 import org.springframework.util.Assert;
 
@@ -52,7 +53,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 +76,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 +117,46 @@ public class CasServiceTicketAuthenticationToken extends AbstractAuthenticationT
 		this.credentials = null;
 	}
 
+	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 AbstractAuthenticationBuilder<String, Object, 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 String 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 CasServiceTicketAuthenticationToken build() {
+			return new CasServiceTicketAuthenticationToken(this);
+		}
+
+	}
+
 }

+ 8 - 1
cas/src/test/java/org/springframework/security/cas/authentication/CasAuthenticationTokenTests.java

@@ -165,7 +165,14 @@ public class CasAuthenticationTokenTests {
 		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();
+		CasAuthenticationToken authentication = factorOne.toBuilder()
+			.authorities((a) -> a.addAll(factorTwo.getAuthorities()))
+			.keyHash(factorTwo.getKeyHash())
+			.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());

+ 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

+ 27 - 15
core/src/main/java/org/springframework/security/authentication/AbstractAuthenticationToken.java

@@ -20,7 +20,7 @@ import java.security.Principal;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.HashSet;
+import java.util.LinkedHashSet;
 import java.util.function.Consumer;
 
 import org.jspecify.annotations.Nullable;
@@ -43,6 +43,8 @@ import org.springframework.util.Assert;
  */
 public abstract class AbstractAuthenticationToken implements Authentication, CredentialsContainer {
 
+	private static final long serialVersionUID = -3194696462184782834L;
+
 	private final Collection<GrantedAuthority> authorities;
 
 	private @Nullable Object details;
@@ -65,6 +67,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;
@@ -187,36 +195,40 @@ 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> {
+	protected abstract static class AbstractAuthenticationBuilder<P, C, B extends AbstractAuthenticationBuilder<P, C, B>>
+			implements Authentication.Builder<P, C, B> {
 
-		private final Collection<GrantedAuthority> authorities = new HashSet<>();
+		protected boolean authenticated;
 
-		protected AbstractAuthenticationBuilder() {
+		protected @Nullable Object details;
 
+		protected 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 authorities(Consumer<Collection<GrantedAuthority>> authorities) {
-			authorities.accept(this.authorities);
+		public B authenticated(boolean authenticated) {
+			this.authenticated = authenticated;
 			return (B) this;
 		}
 
 		@Override
-		public A build() {
-			return build(this.authorities);
+		public B details(@Nullable Object details) {
+			this.details = details;
+			return (B) this;
 		}
 
 		@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());
+		public B authorities(Consumer<Collection<GrantedAuthority>> authorities) {
+			authorities.accept(this.authorities);
+			this.authenticated = true;
 			return (B) this;
 		}
 
-		protected abstract A build(Collection<GrantedAuthority> authorities);
-
 	}
 
 }

+ 22 - 18
core/src/main/java/org/springframework/security/authentication/RememberMeAuthenticationToken.java

@@ -74,6 +74,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
@@ -94,7 +100,7 @@ public class RememberMeAuthenticationToken extends AbstractAuthenticationToken {
 
 	@Override
 	public Builder toBuilder() {
-		return new Builder().apply(this);
+		return new Builder(this);
 	}
 
 	@Override
@@ -120,35 +126,33 @@ public class RememberMeAuthenticationToken extends AbstractAuthenticationToken {
 	 *
 	 * @since 7.0
 	 */
-	public static final class Builder extends AbstractAuthenticationBuilder<RememberMeAuthenticationToken, Builder> {
-
-		private @Nullable Integer keyHash;
+	public static class Builder<B extends Builder<B>> extends AbstractAuthenticationBuilder<Object, Object, B> {
 
-		private @Nullable Object principal;
+		private Integer keyHash;
 
-		private Builder() {
+		private Object principal;
 
+		protected Builder(RememberMeAuthenticationToken token) {
+			super(token);
+			this.keyHash = token.getKeyHash();
+			this.principal = token.getPrincipal();
 		}
 
-		public Builder apply(RememberMeAuthenticationToken token) {
-			return super.apply(token).keyHash(token.getKeyHash()).principal(token.getPrincipal());
-		}
-
-		public Builder principal(Object principal) {
+		@Override
+		public B principal(@Nullable Object principal) {
+			Assert.notNull(principal, "principal cannot be null");
 			this.principal = principal;
-			return this;
+			return (B) this;
 		}
 
-		public Builder keyHash(int keyHash) {
+		public B keyHash(int keyHash) {
 			this.keyHash = keyHash;
-			return this;
+			return (B) 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);
+		public RememberMeAuthenticationToken build() {
+			return new RememberMeAuthenticationToken(this);
 		}
 
 	}

+ 26 - 21
core/src/main/java/org/springframework/security/authentication/TestingAuthenticationToken.java

@@ -43,7 +43,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;
 	}
@@ -65,6 +65,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;
@@ -76,8 +82,8 @@ public class TestingAuthenticationToken extends AbstractAuthenticationToken {
 	}
 
 	@Override
-	public Builder toBuilder() {
-		return new Builder().apply(this);
+	public Builder<?> toBuilder() {
+		return new Builder<>(this);
 	}
 
 	/**
@@ -85,36 +91,35 @@ public class TestingAuthenticationToken extends AbstractAuthenticationToken {
 	 *
 	 * @since 7.0
 	 */
-	public static final class Builder extends AbstractAuthenticationBuilder<TestingAuthenticationToken, Builder> {
+	public static class Builder<B extends Builder<B>> extends AbstractAuthenticationBuilder<Object, Object, B> {
 
-		private @Nullable Object principal;
+		private Object principal;
 
-		private @Nullable Object credentials;
-
-		private Builder() {
+		private Object credentials;
 
+		protected Builder(TestingAuthenticationToken token) {
+			super(token);
+			this.principal = token.principal;
+			this.credentials = token.credentials;
 		}
 
-		public Builder apply(TestingAuthenticationToken authentication) {
-			return super.apply(authentication).principal(authentication.getPrincipal())
-				.credentials(authentication.getCredentials());
-		}
-
-		public Builder principal(Object principal) {
+		@Override
+		public B principal(@Nullable Object principal) {
+			Assert.notNull(principal, "principal cannot be null");
 			this.principal = principal;
-			return this;
+			return (B) this;
 		}
 
-		public Builder credentials(Object credentials) {
+		@Override
+		public B credentials(@Nullable Object credentials) {
+			Assert.notNull(credentials, "credentials cannot be null");
 			this.credentials = credentials;
-			return this;
+			return (B) 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);
+		public TestingAuthenticationToken build() {
+			return new TestingAuthenticationToken(this);
 		}
 
 	}

+ 22 - 17
core/src/main/java/org/springframework/security/authentication/UsernamePasswordAuthenticationToken.java

@@ -51,7 +51,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);
@@ -74,6 +74,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>.
@@ -126,8 +132,8 @@ public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationT
 	}
 
 	@Override
-	public Builder<?, ?> toBuilder() {
-		return new Builder<>().apply(this);
+	public Builder<?> toBuilder() {
+		return new Builder<>(this);
 	}
 
 	/**
@@ -135,35 +141,34 @@ public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationT
 	 *
 	 * @since 7.0
 	 */
-	public static class Builder<A extends UsernamePasswordAuthenticationToken, B extends Builder<A, B>>
-			extends AbstractAuthenticationBuilder<A, B> {
-
-		private @Nullable Object principal;
+	public static class Builder<B extends Builder<B>> extends AbstractAuthenticationBuilder<Object, Object, B> {
 
-		private @Nullable Object credentials;
+		protected @Nullable Object principal;
 
-		protected Builder() {
-		}
+		protected @Nullable Object credentials;
 
-		public B apply(UsernamePasswordAuthenticationToken authentication) {
-			return super.apply(authentication).principal(authentication.getPrincipal())
-				.credentials(authentication.getCredentials());
+		protected Builder(UsernamePasswordAuthenticationToken token) {
+			super(token);
+			this.principal = token.principal;
+			this.credentials = token.credentials;
 		}
 
-		public B principal(Object principal) {
+		@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
-		protected A build(Collection<GrantedAuthority> authorities) {
-			Assert.notNull(this.principal, "principal cannot be null");
-			return (A) new UsernamePasswordAuthenticationToken(this.principal, this.credentials, authorities);
+		public UsernamePasswordAuthenticationToken build() {
+			return new UsernamePasswordAuthenticationToken(this);
 		}
 
 	}

+ 17 - 19
core/src/main/java/org/springframework/security/authentication/jaas/JaasAuthenticationToken.java

@@ -16,7 +16,6 @@
 
 package org.springframework.security.authentication.jaas;
 
-import java.util.Collection;
 import java.util.List;
 
 import javax.security.auth.login.LoginContext;
@@ -51,13 +50,18 @@ 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().apply(this);
+	public Builder<?> toBuilder() {
+		return new Builder<>(this);
 	}
 
 	/**
@@ -65,17 +69,13 @@ public class JaasAuthenticationToken extends UsernamePasswordAuthenticationToken
 	 *
 	 * @since 7.0
 	 */
-	public static final class Builder
-			extends UsernamePasswordAuthenticationToken.Builder<JaasAuthenticationToken, Builder> {
-
-		private @Nullable LoginContext loginContext;
+	public static class Builder<B extends Builder<B>> extends UsernamePasswordAuthenticationToken.Builder<B> {
 
-		private Builder() {
-
-		}
+		private LoginContext loginContext;
 
-		public Builder apply(JaasAuthenticationToken authentication) {
-			return super.apply(authentication).loginContext(authentication.getLoginContext());
+		protected Builder(JaasAuthenticationToken token) {
+			super(token);
+			this.loginContext = token.getLoginContext();
 		}
 
 		/**
@@ -83,17 +83,15 @@ public class JaasAuthenticationToken extends UsernamePasswordAuthenticationToken
 		 * @param loginContext the {@link LoginContext} to use
 		 * @return the {@link Builder} for further configuration
 		 */
-		public Builder loginContext(LoginContext loginContext) {
+		public B loginContext(LoginContext loginContext) {
 			this.loginContext = loginContext;
-			return this;
+			return (B) 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);
+		public JaasAuthenticationToken build() {
+			Assert.notNull(this.principal, "principal cannot be null");
+			return new JaasAuthenticationToken(this);
 		}
 
 	}

+ 18 - 19
core/src/main/java/org/springframework/security/authentication/ott/OneTimeTokenAuthentication.java

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

+ 14 - 26
core/src/main/java/org/springframework/security/core/Authentication.java

@@ -16,7 +16,6 @@
 
 package org.springframework.security.core;
 
-import java.io.Serial;
 import java.io.Serializable;
 import java.security.Principal;
 import java.util.Collection;
@@ -26,7 +25,6 @@ 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
@@ -57,9 +55,6 @@ import org.springframework.util.Assert;
  */
 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
@@ -148,43 +143,36 @@ public interface Authentication extends Principal, Serializable {
 	 * instance
 	 * @since 7.0
 	 */
-	default Builder<?, ?> toBuilder() {
-		return new NoopAuthenticationBuilder<>(this);
+	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>> {
+	interface Builder<P, C, B extends Builder<P, C, 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()));
+		B authorities(Consumer<Collection<GrantedAuthority>> authorities);
+
+		default B credentials(@Nullable C credentials) {
+			throw new UnsupportedOperationException(
+					String.format("%s does not store credentials", this.getClass().getSimpleName()));
 		}
 
-		/**
-		 * 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);
+		B details(@Nullable Object details);
+
+		B principal(@Nullable P principal);
+
+		B authenticated(boolean authenticated);
 
 		/**
 		 * Build an {@link Authentication} instance
 		 * @return the {@link Authentication} instance
 		 */
-		A build();
+		Authentication build();
 
 	}
 

+ 26 - 10
core/src/main/java/org/springframework/security/core/NoopAuthenticationBuilder.java

@@ -19,34 +19,50 @@ package org.springframework.security.core;
 import java.util.Collection;
 import java.util.function.Consumer;
 
-import org.springframework.util.Assert;
+import org.jspecify.annotations.Nullable;
 
 /**
  * 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>> {
+class NoopAuthenticationBuilder implements Authentication.Builder<Object, Object, NoopAuthenticationBuilder> {
 
-	private A original;
+	private Authentication original;
 
-	NoopAuthenticationBuilder(A authentication) {
-		Assert.isTrue(authentication.isAuthenticated(), "cannot mutate an unauthenticated token");
-		Assert.notNull(authentication.getPrincipal(), "principal cannot be null");
+	NoopAuthenticationBuilder(Authentication authentication) {
 		this.original = authentication;
 	}
 
 	@Override
-	public NoopAuthenticationBuilder<A> authorities(Consumer<Collection<GrantedAuthority>> authorities) {
+	public NoopAuthenticationBuilder authenticated(boolean authenticated) {
 		return this;
 	}
 
 	@Override
-	public A build() {
+	public NoopAuthenticationBuilder principal(@Nullable Object principal) {
+		return this;
+	}
+
+	@Override
+	public NoopAuthenticationBuilder details(@Nullable Object details) {
+		return this;
+	}
+
+	@Override
+	public NoopAuthenticationBuilder credentials(@Nullable Object credentials) {
+		return this;
+	}
+
+	@Override
+	public NoopAuthenticationBuilder authorities(Consumer<Collection<GrantedAuthority>> authorities) {
+		return this;
+	}
+
+	@Override
+	public Authentication build() {
 		return this.original;
 	}
 

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

@@ -16,44 +16,44 @@
 
 package org.springframework.security.authentication;
 
-import java.util.Collection;
 import java.util.Set;
 
+import org.jspecify.annotations.Nullable;
 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();
+		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 AbstractAuthenticationBuilder<Authentication, TestAbstractAuthenticationBuilder> {
+			extends AbstractAuthenticationBuilder<Object, Object, TestAbstractAuthenticationBuilder> {
+
+		private TestAbstractAuthenticationBuilder(TestingAuthenticationToken token) {
+			super(token);
+		}
+
+		@Override
+		public TestAbstractAuthenticationBuilder principal(@Nullable Object principal) {
+			return this;
+		}
 
 		@Override
-		protected Authentication build(Collection<GrantedAuthority> authorities) {
-			return new TestingAuthenticationToken("user", "password", authorities);
+		public TestingAuthenticationToken build() {
+			return new TestingAuthenticationToken("user", "password", this.authorities);
 		}
 
 	}

+ 5 - 1
core/src/test/java/org/springframework/security/authentication/TestingAuthenticationTokenTests.java

@@ -57,7 +57,11 @@ public class TestingAuthenticationTokenTests {
 				AuthorityUtils.createAuthorityList("FACTOR_ONE"));
 		TestingAuthenticationToken factorTwo = new TestingAuthenticationToken("bob", "ssap",
 				AuthorityUtils.createAuthorityList("FACTOR_TWO"));
-		TestingAuthenticationToken result = factorOne.toBuilder().apply(factorTwo).build();
+		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());

+ 8 - 5
core/src/test/java/org/springframework/security/authentication/UsernamePasswordAuthenticationTokenTests.java

@@ -20,7 +20,6 @@ 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;
@@ -94,10 +93,14 @@ public class UsernamePasswordAuthenticationTokenTests {
 				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");
+		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");
 	}
 

+ 6 - 1
core/src/test/java/org/springframework/security/authentication/jaas/JaasAuthenticationTokenTests.java

@@ -35,7 +35,12 @@ class JaasAuthenticationTokenTests {
 				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();
+		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());

+ 4 - 1
core/src/test/java/org/springframework/security/authentication/ott/OneTimeTokenAuthenticationTests.java

@@ -32,7 +32,10 @@ class OneTimeTokenAuthenticationTests {
 				AuthorityUtils.createAuthorityList("FACTOR_ONE"));
 		OneTimeTokenAuthentication factorTwo = new OneTimeTokenAuthentication("bob",
 				AuthorityUtils.createAuthorityList("FACTOR_TWO"));
-		OneTimeTokenAuthentication result = factorOne.toBuilder().apply(factorTwo).build();
+		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");

+ 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() {

+ 25 - 16
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;
@@ -86,8 +96,8 @@ public class OAuth2AuthenticationToken extends AbstractAuthenticationToken {
 	}
 
 	@Override
-	public Builder toBuilder() {
-		return new Builder().apply(this);
+	public Builder<?> toBuilder() {
+		return new Builder<>(this);
 	}
 
 	/**
@@ -95,34 +105,33 @@ public class OAuth2AuthenticationToken extends AbstractAuthenticationToken {
 	 *
 	 * @since 7.0
 	 */
-	public static final class Builder extends AbstractAuthenticationBuilder<OAuth2AuthenticationToken, Builder> {
+	public static class Builder<B extends Builder<B>> extends AbstractAuthenticationBuilder<OAuth2User, Object, B> {
 
 		private OAuth2User principal;
 
 		private String authorizedClientRegistrationId;
 
-		private Builder() {
-
+		protected Builder(OAuth2AuthenticationToken token) {
+			super(token);
+			this.principal = token.principal;
+			this.authorizedClientRegistrationId = token.authorizedClientRegistrationId;
 		}
 
-		public Builder apply(OAuth2AuthenticationToken authentication) {
-			return super.apply(authentication).principal(authentication.getPrincipal())
-				.authorizedClientRegistrationId(authentication.authorizedClientRegistrationId);
-		}
-
-		public Builder principal(OAuth2User principal) {
+		@Override
+		public B principal(@Nullable OAuth2User principal) {
+			Assert.notNull(principal, "principal cannot be null");
 			this.principal = principal;
-			return this;
+			return (B) this;
 		}
 
-		public Builder authorizedClientRegistrationId(String authorizedClientRegistrationId) {
+		public B authorizedClientRegistrationId(String authorizedClientRegistrationId) {
 			this.authorizedClientRegistrationId = authorizedClientRegistrationId;
-			return this;
+			return (B) this;
 		}
 
 		@Override
-		protected OAuth2AuthenticationToken build(Collection<GrantedAuthority> authorities) {
-			return new OAuth2AuthenticationToken(this.principal, authorities, this.authorizedClientRegistrationId);
+		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() {

+ 5 - 1
oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthenticationTokenTests.java

@@ -91,7 +91,11 @@ public class OAuth2AuthenticationTokenTests {
 				AuthorityUtils.createAuthorityList("FACTOR_ONE"), "alice");
 		OAuth2AuthenticationToken factorTwo = new OAuth2AuthenticationToken(TestOAuth2Users.create(),
 				AuthorityUtils.createAuthorityList("FACTOR_TWO"), "bob");
-		OAuth2AuthenticationToken result = factorOne.toBuilder().apply(factorTwo).build();
+		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());

+ 54 - 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,47 @@ public abstract class AbstractOAuth2TokenAuthenticationToken<T extends OAuth2Tok
 	 */
 	public abstract Map<String, Object> getTokenAttributes();
 
+	/**
+	 * A builder preserving the concrete {@link Authentication} type
+	 *
+	 * @since 7.0
+	 */
+	public abstract static class AbstractOAuth2TokenAuthenticationBuilder<T extends OAuth2Token, B extends AbstractOAuth2TokenAuthenticationBuilder<T, B>>
+			extends AbstractAuthenticationBuilder<Object, Object, 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;
+		}
+
+		public B token(T token) {
+			Assert.notNull(token, "credentials cannot be null");
+			this.token = token;
+			return (B) this;
+		}
+
+	}
+
 }

+ 28 - 21
oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/BearerTokenAuthentication.java

@@ -21,6 +21,8 @@ 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;
@@ -57,14 +59,19 @@ 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().apply(this);
+	public Builder<?> toBuilder() {
+		return new Builder<>(this);
 	}
 
 	/**
@@ -72,34 +79,34 @@ public class BearerTokenAuthentication extends AbstractOAuth2TokenAuthentication
 	 *
 	 * @since 7.0
 	 */
-	public static final class Builder extends AbstractAuthenticationBuilder<BearerTokenAuthentication, Builder> {
-
-		private OAuth2AuthenticatedPrincipal principal;
-
-		private OAuth2AccessToken token;
+	public static class Builder<B extends Builder<B>>
+			extends AbstractOAuth2TokenAuthenticationBuilder<OAuth2AccessToken, B> {
 
-		private Builder() {
+		private Map<String, Object> attributes;
 
+		protected Builder(BearerTokenAuthentication token) {
+			super(token);
+			this.attributes = token.getTokenAttributes();
 		}
 
-		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;
+		@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);
 		}
 
-		public Builder credentials(OAuth2AccessToken credentials) {
-			this.token = credentials;
-			return this;
+		@Override
+		public B token(OAuth2AccessToken token) {
+			Assert.isTrue(token.getTokenType() == OAuth2AccessToken.TokenType.BEARER,
+					"credentials must be a bearer token");
+			return super.token(token);
 		}
 
 		@Override
-		protected BearerTokenAuthentication build(Collection<GrantedAuthority> authorities) {
-			return new BearerTokenAuthentication(this.principal, this.token, authorities);
+		public BearerTokenAuthentication build() {
+			return new BearerTokenAuthentication(this);
 		}
 
 	}

+ 15 - 20
oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationToken.java

@@ -72,6 +72,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();
@@ -86,8 +91,8 @@ public class JwtAuthenticationToken extends AbstractOAuth2TokenAuthenticationTok
 	}
 
 	@Override
-	public Builder toBuilder() {
-		return new Builder().apply(this);
+	public Builder<?> toBuilder() {
+		return new Builder<>(this);
 	}
 
 	/**
@@ -95,33 +100,23 @@ public class JwtAuthenticationToken extends AbstractOAuth2TokenAuthenticationTok
 	 *
 	 * @since 7.0
 	 */
-	public static final class Builder extends AbstractAuthenticationBuilder<JwtAuthenticationToken, Builder> {
-
-		private Jwt jwt;
+	public static class Builder<B extends Builder<B>> extends AbstractOAuth2TokenAuthenticationBuilder<Jwt, B> {
 
 		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;
+		protected Builder(JwtAuthenticationToken token) {
+			super(token);
+			this.name = token.getName();
 		}
 
-		public Builder name(String name) {
+		public B name(String name) {
 			this.name = name;
-			return this;
+			return (B) this;
 		}
 
 		@Override
-		protected JwtAuthenticationToken build(Collection<GrantedAuthority> authorities) {
-			return new JwtAuthenticationToken(this.jwt, authorities, this.name);
+		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);

+ 5 - 1
oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/BearerTokenAuthenticationTests.java

@@ -162,7 +162,11 @@ public class BearerTokenAuthenticationTests {
 				new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, "nekot", Instant.now(),
 						Instant.now().plusSeconds(3600)),
 				AuthorityUtils.createAuthorityList("FACTOR_TWO"));
-		BearerTokenAuthentication authentication = factorOne.toBuilder().apply(factorTwo).build();
+		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());

+ 6 - 2
oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationTokenTests.java

@@ -55,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");
 	}
 
@@ -122,7 +122,11 @@ public class JwtAuthenticationTokenTests {
 				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();
+		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());

+ 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);

+ 26 - 28
saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AssertionAuthentication.java

@@ -19,10 +19,11 @@ package org.springframework.security.saml2.provider.service.authentication;
 import java.io.Serial;
 import java.util.Collection;
 
-import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
 
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.GrantedAuthority;
+import org.springframework.util.Assert;
 
 /**
  * An authentication based off of a SAML 2.0 Assertion
@@ -56,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;
@@ -66,8 +73,8 @@ public class Saml2AssertionAuthentication extends Saml2Authentication {
 	}
 
 	@Override
-	public Builder toBuilder() {
-		return new Builder().apply(this);
+	public Builder<?> toBuilder() {
+		return new Builder<>(this);
 	}
 
 	/**
@@ -75,44 +82,35 @@ public class Saml2AssertionAuthentication extends Saml2Authentication {
 	 *
 	 * @since 7.0
 	 */
-	public static final class Builder
-			extends AbstractAuthenticationBuilder<@NonNull Saml2AssertionAuthentication, @NonNull Builder> {
-
-		private Object principal;
+	public static class Builder<B extends Builder<B>>
+			extends Saml2Authentication.Builder<Saml2ResponseAssertionAccessor, B> {
 
 		private Saml2ResponseAssertionAccessor assertion;
 
 		private String relyingPartyRegistrationId;
 
-		private Builder() {
-
+		protected Builder(Saml2AssertionAuthentication token) {
+			super(token);
+			this.assertion = token.assertion;
+			this.relyingPartyRegistrationId = token.relyingPartyRegistrationId;
 		}
 
-		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;
+		@Override
+		public B credentials(@Nullable Saml2ResponseAssertionAccessor credentials) {
+			saml2Response(credentials.getResponseValue());
+			Assert.notNull(credentials, "assertion cannot be null");
+			this.assertion = credentials;
+			return (B) this;
 		}
 
-		public Builder relyingPartyRegistrationId(String relyingPartyRegistrationId) {
+		public B relyingPartyRegistrationId(String relyingPartyRegistrationId) {
 			this.relyingPartyRegistrationId = relyingPartyRegistrationId;
-			return this;
+			return (B) this;
 		}
 
 		@Override
-		protected Saml2AssertionAuthentication build(Collection<GrantedAuthority> authorities) {
-			return new Saml2AssertionAuthentication(this.principal, this.assertion, authorities,
-					this.relyingPartyRegistrationId);
+		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<C, B extends Builder<C, B>> extends AbstractAuthenticationBuilder<Object, C, 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;
+		}
+
+	}
+
 }

+ 6 - 1
saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/Saml2AssertionAuthenticationTests.java

@@ -33,7 +33,12 @@ class Saml2AssertionAuthenticationTests {
 				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();
+		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());

+ 25 - 18
web/src/main/java/org/springframework/security/web/authentication/preauth/PreAuthenticatedAuthenticationToken.java

@@ -23,6 +23,7 @@ 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.util.Assert;
 
 /**
  * {@link org.springframework.security.core.Authentication} implementation for
@@ -47,7 +48,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;
 	}
@@ -67,6 +68,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
 	 */
@@ -84,8 +91,8 @@ public class PreAuthenticatedAuthenticationToken extends AbstractAuthenticationT
 	}
 
 	@Override
-	public Builder toBuilder() {
-		return new Builder().apply(this);
+	public Builder<?> toBuilder() {
+		return new Builder<>(this);
 	}
 
 	/**
@@ -93,34 +100,34 @@ public class PreAuthenticatedAuthenticationToken extends AbstractAuthenticationT
 	 *
 	 * @since 7.0
 	 */
-	public static final class Builder
-			extends AbstractAuthenticationBuilder<PreAuthenticatedAuthenticationToken, Builder> {
+	public static class Builder<B extends Builder<B>> extends AbstractAuthenticationBuilder<Object, Object, B> {
 
 		private Object principal;
 
-		private Object credentials;
-
-		private Builder() {
+		private @Nullable Object credentials;
 
+		protected Builder(PreAuthenticatedAuthenticationToken token) {
+			super(token);
+			this.principal = token.principal;
+			this.credentials = token.credentials;
 		}
 
-		public Builder apply(PreAuthenticatedAuthenticationToken token) {
-			return super.apply(token).principal(token.getPrincipal()).credentials(token.getCredentials());
-		}
-
-		public Builder principal(Object principal) {
+		@Override
+		public B principal(@Nullable Object principal) {
+			Assert.notNull(principal, "principal cannot be null");
 			this.principal = principal;
-			return this;
+			return (B) this;
 		}
 
-		public Builder credentials(Object credentials) {
+		@Override
+		public B credentials(@Nullable Object credentials) {
 			this.credentials = credentials;
-			return this;
+			return (B) this;
 		}
 
 		@Override
-		protected PreAuthenticatedAuthenticationToken build(Collection<GrantedAuthority> authorities) {
-			return new PreAuthenticatedAuthenticationToken(this.principal, this.credentials, authorities);
+		public PreAuthenticatedAuthenticationToken build() {
+			return new PreAuthenticatedAuthenticationToken(this);
 		}
 
 	}

+ 5 - 1
web/src/test/java/org/springframework/security/web/authentication/preauth/PreAuthenticatedAuthenticationTokenTests.java

@@ -80,7 +80,11 @@ public class PreAuthenticatedAuthenticationTokenTests {
 				AuthorityUtils.createAuthorityList("FACTOR_ONE"));
 		PreAuthenticatedAuthenticationToken factorTwo = new PreAuthenticatedAuthenticationToken("bob", "ssap",
 				AuthorityUtils.createAuthorityList("FACTOR_TWO"));
-		PreAuthenticatedAuthenticationToken result = factorOne.toBuilder().apply(factorTwo).build();
+		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());

+ 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

+ 17 - 13
webauthn/src/main/java/org/springframework/security/web/webauthn/authentication/WebAuthnAuthentication.java

@@ -49,6 +49,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");
@@ -71,8 +76,8 @@ public class WebAuthnAuthentication extends AbstractAuthenticationToken {
 	}
 
 	@Override
-	public Builder toBuilder() {
-		return new Builder().apply(this);
+	public Builder<?> toBuilder() {
+		return new Builder<>(this);
 	}
 
 	/**
@@ -80,26 +85,25 @@ public class WebAuthnAuthentication extends AbstractAuthenticationToken {
 	 *
 	 * @since 7.0
 	 */
-	public static final class Builder extends AbstractAuthenticationBuilder<WebAuthnAuthentication, Builder> {
+	public static final class Builder<B extends Builder<B>>
+			extends AbstractAuthenticationBuilder<PublicKeyCredentialUserEntity, Object, B> {
 
 		private PublicKeyCredentialUserEntity principal;
 
-		private Builder() {
-
+		private Builder(WebAuthnAuthentication token) {
+			super(token);
 		}
 
-		public Builder apply(WebAuthnAuthentication authentication) {
-			return super.apply(authentication).principal(authentication.getPrincipal());
-		}
-
-		public Builder principal(PublicKeyCredentialUserEntity principal) {
+		@Override
+		public B principal(@Nullable PublicKeyCredentialUserEntity principal) {
+			Assert.notNull(principal, "principal cannot be null");
 			this.principal = principal;
-			return this;
+			return (B) this;
 		}
 
 		@Override
-		protected WebAuthnAuthentication build(Collection<GrantedAuthority> authorities) {
-			return new WebAuthnAuthentication(this.principal, authorities);
+		public WebAuthnAuthentication build() {
+			return new WebAuthnAuthentication(this);
 		}
 
 	}

+ 4 - 1
webauthn/src/test/java/org/springframework/security/web/webauthn/authentication/WebAuthnAuthenticationTests.java

@@ -64,7 +64,10 @@ class WebAuthnAuthenticationTests {
 		PublicKeyCredentialUserEntity bob = TestPublicKeyCredentialUserEntities.userEntity().build();
 		WebAuthnAuthentication factorTwo = new WebAuthnAuthentication(bob,
 				AuthorityUtils.createAuthorityList("FACTOR_TWO"));
-		WebAuthnAuthentication result = factorOne.toBuilder().apply(factorTwo).build();
+		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");