diff --git a/cas/src/main/java/org/springframework/security/cas/authentication/CasAuthenticationToken.java b/cas/src/main/java/org/springframework/security/cas/authentication/CasAuthenticationToken.java index e19f8bd33c..9fd4ba204e 100644 --- a/cas/src/main/java/org/springframework/security/cas/authentication/CasAuthenticationToken.java +++ b/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 authorities) { + return new CasAuthenticationToken(this.keyHash, this.principal, this.credentials, authorities, + this.userDetails, this.assertion); + } + + } + } diff --git a/cas/src/test/java/org/springframework/security/cas/authentication/CasAuthenticationTokenTests.java b/cas/src/test/java/org/springframework/security/cas/authentication/CasAuthenticationTokenTests.java index aa0048d349..bdd26916a6 100644 --- a/cas/src/test/java/org/springframework/security/cas/authentication/CasAuthenticationTokenTests.java +++ b/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 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"); + } + } diff --git a/core/src/main/java/org/springframework/security/authentication/AbstractAuthenticationToken.java b/core/src/main/java/org/springframework/security/authentication/AbstractAuthenticationToken.java index 7ec4167160..243a3f8fb8 100644 --- a/core/src/main/java/org/springframework/security/authentication/AbstractAuthenticationToken.java +++ b/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> + implements Builder { + + private final Collection authorities = new HashSet<>(); + + protected AbstractAuthenticationBuilder() { + + } + + @Override + public B authorities(Consumer> 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 authorities); + + } + } diff --git a/core/src/main/java/org/springframework/security/authentication/RememberMeAuthenticationToken.java b/core/src/main/java/org/springframework/security/authentication/RememberMeAuthenticationToken.java index 5c17618cef..fa1972b3c0 100644 --- a/core/src/main/java/org/springframework/security/authentication/RememberMeAuthenticationToken.java +++ b/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 Authentication. @@ -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 { + + 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 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); + } + + } + } diff --git a/core/src/main/java/org/springframework/security/authentication/TestingAuthenticationToken.java b/core/src/main/java/org/springframework/security/authentication/TestingAuthenticationToken.java index abfc6560f4..657b596741 100644 --- a/core/src/main/java/org/springframework/security/authentication/TestingAuthenticationToken.java +++ b/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 { + + 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 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); + } + + } + } diff --git a/core/src/main/java/org/springframework/security/authentication/UsernamePasswordAuthenticationToken.java b/core/src/main/java/org/springframework/security/authentication/UsernamePasswordAuthenticationToken.java index c25b4a9ce0..33d6b13e02 100644 --- a/core/src/main/java/org/springframework/security/authentication/UsernamePasswordAuthenticationToken.java +++ b/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> + extends AbstractAuthenticationBuilder { + + 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 authorities) { + Assert.notNull(this.principal, "principal cannot be null"); + return (A) new UsernamePasswordAuthenticationToken(this.principal, this.credentials, authorities); + } + + } + } diff --git a/core/src/main/java/org/springframework/security/authentication/jaas/JaasAuthenticationToken.java b/core/src/main/java/org/springframework/security/authentication/jaas/JaasAuthenticationToken.java index 314f79e563..52d8392709 100644 --- a/core/src/main/java/org/springframework/security/authentication/jaas/JaasAuthenticationToken.java +++ b/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 { + + 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 authorities) { + UsernamePasswordAuthenticationToken token = super.build(authorities); + Assert.notNull(this.loginContext, "loginContext cannot be null"); + return new JaasAuthenticationToken(token.getPrincipal(), token.getCredentials(), + (List) token.getAuthorities(), this.loginContext); + } + + } + } diff --git a/core/src/main/java/org/springframework/security/authentication/ott/OneTimeTokenAuthentication.java b/core/src/main/java/org/springframework/security/authentication/ott/OneTimeTokenAuthentication.java index fc0c806463..2ccf53b27a 100644 --- a/core/src/main/java/org/springframework/security/authentication/ott/OneTimeTokenAuthentication.java +++ b/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 { + + 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 authorities) { + Assert.notNull(this.principal, "principal cannot be null"); + return new OneTimeTokenAuthentication(this.principal, authorities); + } + + } + } diff --git a/core/src/main/java/org/springframework/security/core/Authentication.java b/core/src/main/java/org/springframework/security/core/Authentication.java index c8515b25fc..b07bdd977f 100644 --- a/core/src/main/java/org/springframework/security/core/Authentication.java +++ b/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 AuthenticationManager 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. *

* @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 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
the type of {@link Authentication} + * @author Josh Cummings + * @since 7.0 + */ + interface Builder> { + + /** + * Apply this {@link Authentication} to the builder. + *

+ * 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> authorities); + + /** + * Build an {@link Authentication} instance + * @return the {@link Authentication} instance + */ + A build(); + + } + } diff --git a/core/src/main/java/org/springframework/security/core/NoopAuthenticationBuilder.java b/core/src/main/java/org/springframework/security/core/NoopAuthenticationBuilder.java new file mode 100644 index 0000000000..163574b2db --- /dev/null +++ b/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 the type of {@link Authentication} + * @author Josh Cummings + * @since 7.0 + */ +class NoopAuthenticationBuilder + implements Authentication.Builder> { + + 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 authorities(Consumer> authorities) { + return this; + } + + @Override + public A build() { + return this.original; + } + +} diff --git a/core/src/test/java/org/springframework/security/authentication/AbstractAuthenticationBuilderTests.java b/core/src/test/java/org/springframework/security/authentication/AbstractAuthenticationBuilderTests.java new file mode 100644 index 0000000000..f3babe70f8 --- /dev/null +++ b/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 authorities = AuthorityUtils.authorityListToSet(result.getAuthorities()); + assertThat(authorities).containsExactlyInAnyOrder("FACTOR_ONE", "FACTOR_TWO"); + } + + private static final class TestAbstractAuthenticationBuilder + extends AbstractAuthenticationBuilder { + + @Override + protected Authentication build(Collection authorities) { + return new TestingAuthenticationToken("user", "password", authorities); + } + + } + +} diff --git a/core/src/test/java/org/springframework/security/authentication/TestingAuthenticationTokenTests.java b/core/src/test/java/org/springframework/security/authentication/TestingAuthenticationTokenTests.java index cfab36a2e1..2c17042a7d 100644 --- a/core/src/test/java/org/springframework/security/authentication/TestingAuthenticationTokenTests.java +++ b/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 authorities = AuthorityUtils.authorityListToSet(result.getAuthorities()); + assertThat(result.getPrincipal()).isSameAs(factorTwo.getPrincipal()); + assertThat(result.getCredentials()).isSameAs(factorTwo.getCredentials()); + assertThat(authorities).containsExactlyInAnyOrder("FACTOR_ONE", "FACTOR_TWO"); + } + } diff --git a/core/src/test/java/org/springframework/security/authentication/UsernamePasswordAuthenticationTokenTests.java b/core/src/test/java/org/springframework/security/authentication/UsernamePasswordAuthenticationTokenTests.java index 0e25aef80d..334e9bfc6b 100644 --- a/core/src/test/java/org/springframework/security/authentication/UsernamePasswordAuthenticationTokenTests.java +++ b/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 authorities = AuthorityUtils.authorityListToSet(authentication.getAuthorities()); + assertThat(authentication.getPrincipal()).isEqualTo("bob"); + assertThat(authentication.getCredentials()).isEqualTo("ssap"); + assertThat(authorities).containsExactlyInAnyOrder("FACTOR_ONE", "FACTOR_TWO"); + } + } diff --git a/core/src/test/java/org/springframework/security/authentication/jaas/JaasAuthenticationTokenTests.java b/core/src/test/java/org/springframework/security/authentication/jaas/JaasAuthenticationTokenTests.java new file mode 100644 index 0000000000..f307c2d380 --- /dev/null +++ b/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 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"); + } + +} diff --git a/core/src/test/java/org/springframework/security/authentication/ott/OneTimeTokenAuthenticationTests.java b/core/src/test/java/org/springframework/security/authentication/ott/OneTimeTokenAuthenticationTests.java new file mode 100644 index 0000000000..c8012b82f5 --- /dev/null +++ b/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 authorities = AuthorityUtils.authorityListToSet(result.getAuthorities()); + assertThat(result.getPrincipal()).isSameAs(factorTwo.getPrincipal()); + assertThat(authorities).containsExactlyInAnyOrder("FACTOR_ONE", "FACTOR_TWO"); + } + +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthenticationToken.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthenticationToken.java index c766522199..6b5ac6119b 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthenticationToken.java +++ b/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 { + + 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 authorities) { + return new OAuth2AuthenticationToken(this.principal, authorities, this.authorizedClientRegistrationId); + } + + } + } diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthenticationTokenTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthenticationTokenTests.java index 4c590fcaf4..839cd7ce7b 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthenticationTokenTests.java +++ b/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 authorities = AuthorityUtils.authorityListToSet(result.getAuthorities()); + assertThat(result.getPrincipal()).isSameAs(factorTwo.getPrincipal()); + assertThat(result.getAuthorizedClientRegistrationId()).isSameAs(factorTwo.getAuthorizedClientRegistrationId()); + assertThat(authorities).containsExactlyInAnyOrder("FACTOR_ONE", "FACTOR_TWO"); + } + } diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/BearerTokenAuthentication.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/BearerTokenAuthentication.java index f3dfb83270..6f8606df2e 100644 --- a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/BearerTokenAuthentication.java +++ b/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 { + + 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 authorities) { + return new BearerTokenAuthentication(this.principal, this.token, authorities); + } + + } + } diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationToken.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationToken.java index 43cc749d9d..dfb23244b3 100644 --- a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationToken.java +++ b/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 { + + 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 authorities) { + return new JwtAuthenticationToken(this.jwt, authorities, this.name); + } + + } + } diff --git a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/BearerTokenAuthenticationTests.java b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/BearerTokenAuthenticationTests.java index 39360f862d..eab8208656 100644 --- a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/BearerTokenAuthenticationTests.java +++ b/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 authorities = AuthorityUtils.authorityListToSet(authentication.getAuthorities()); + assertThat(authentication.getPrincipal()).isSameAs(factorTwo.getPrincipal()); + assertThat(authentication.getToken()).isSameAs(factorTwo.getToken()); + assertThat(authorities).containsExactlyInAnyOrder("FACTOR_ONE", "FACTOR_TWO"); + } + } diff --git a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationTokenTests.java b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationTokenTests.java index d6af03cc3b..7780c05c77 100644 --- a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/authentication/JwtAuthenticationTokenTests.java +++ b/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 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); } diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AssertionAuthentication.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AssertionAuthentication.java index 3b528c04a3..3b2aa37a2a 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2AssertionAuthentication.java +++ b/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 authorities) { + return new Saml2AssertionAuthentication(this.principal, this.assertion, authorities, + this.relyingPartyRegistrationId); + } + + } + } diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/Saml2AssertionAuthenticationTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/Saml2AssertionAuthenticationTests.java new file mode 100644 index 0000000000..bfa0875d06 --- /dev/null +++ b/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 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"); + } + +} diff --git a/web/src/main/java/org/springframework/security/web/authentication/preauth/PreAuthenticatedAuthenticationToken.java b/web/src/main/java/org/springframework/security/web/authentication/preauth/PreAuthenticatedAuthenticationToken.java index fefda2ca49..cb5c523590 100755 --- a/web/src/main/java/org/springframework/security/web/authentication/preauth/PreAuthenticatedAuthenticationToken.java +++ b/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 { + + 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 authorities) { + return new PreAuthenticatedAuthenticationToken(this.principal, this.credentials, authorities); + } + + } + } diff --git a/web/src/test/java/org/springframework/security/web/authentication/preauth/PreAuthenticatedAuthenticationTokenTests.java b/web/src/test/java/org/springframework/security/web/authentication/preauth/PreAuthenticatedAuthenticationTokenTests.java index 99825bd7d0..606dd50f03 100644 --- a/web/src/test/java/org/springframework/security/web/authentication/preauth/PreAuthenticatedAuthenticationTokenTests.java +++ b/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 authorities = AuthorityUtils.authorityListToSet(result.getAuthorities()); + assertThat(result.getPrincipal()).isSameAs(factorTwo.getPrincipal()); + assertThat(result.getCredentials()).isSameAs(factorTwo.getCredentials()); + assertThat(authorities).containsExactlyInAnyOrder("FACTOR_ONE", "FACTOR_TWO"); + } + } diff --git a/webauthn/src/main/java/org/springframework/security/web/webauthn/authentication/WebAuthnAuthentication.java b/webauthn/src/main/java/org/springframework/security/web/webauthn/authentication/WebAuthnAuthentication.java index 42007f9ece..9855d17a3e 100644 --- a/webauthn/src/main/java/org/springframework/security/web/webauthn/authentication/WebAuthnAuthentication.java +++ b/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 { + + 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 authorities) { + return new WebAuthnAuthentication(this.principal, authorities); + } + + } + } diff --git a/webauthn/src/test/java/org/springframework/security/web/webauthn/authentication/WebAuthnAuthenticationTests.java b/webauthn/src/test/java/org/springframework/security/web/webauthn/authentication/WebAuthnAuthenticationTests.java index 8ad6e92ea4..7b3bc6f37a 100644 --- a/webauthn/src/test/java/org/springframework/security/web/webauthn/authentication/WebAuthnAuthenticationTests.java +++ b/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 authorities = AuthorityUtils.authorityListToSet(result.getAuthorities()); + assertThat(result.getPrincipal()).isSameAs(factorTwo.getPrincipal()); + assertThat(authorities).containsExactlyInAnyOrder("FACTOR_ONE", "FACTOR_TWO"); + } + }