From a201a2b862e3e20bce63b59d44def565b893d273 Mon Sep 17 00:00:00 2001 From: Josh Cummings <3627351+jzheaux@users.noreply.github.com> Date: Fri, 22 Aug 2025 16:21:26 -0600 Subject: [PATCH 01/14] Add Authentication.Builder This commit adds a new default method to Authentication for the purposes of creating a Builder based on the current authentication, allowing other authentications to be applied to it as a composite. It also adds Builders for each one of the authentication result classes. Issue gh-17861 --- .../CasAuthenticationToken.java | 69 +++++++++++++++++++ .../CasAuthenticationTokenTests.java | 20 ++++++ .../AbstractAuthenticationToken.java | 34 +++++++++ .../RememberMeAuthenticationToken.java | 47 +++++++++++++ .../TestingAuthenticationToken.java | 48 +++++++++++++ .../UsernamePasswordAuthenticationToken.java | 44 ++++++++++++ .../jaas/JaasAuthenticationToken.java | 46 +++++++++++++ .../ott/OneTimeTokenAuthentication.java | 42 +++++++++++ .../security/core/Authentication.java | 54 ++++++++++++++- .../core/NoopAuthenticationBuilder.java | 53 ++++++++++++++ .../AbstractAuthenticationBuilderTests.java | 61 ++++++++++++++++ .../TestingAuthenticationTokenTests.java | 15 ++++ ...rnamePasswordAuthenticationTokenTests.java | 16 +++++ .../jaas/JaasAuthenticationTokenTests.java | 46 +++++++++++++ .../ott/OneTimeTokenAuthenticationTests.java | 41 +++++++++++ .../OAuth2AuthenticationToken.java | 42 +++++++++++ .../OAuth2AuthenticationTokenTests.java | 16 +++++ .../BearerTokenAuthentication.java | 43 ++++++++++++ .../JwtAuthenticationToken.java | 42 +++++++++++ .../BearerTokenAuthenticationTests.java | 18 +++++ .../JwtAuthenticationTokenTests.java | 14 ++++ .../Saml2AssertionAuthentication.java | 55 +++++++++++++++ .../Saml2AssertionAuthenticationTests.java | 44 ++++++++++++ .../PreAuthenticatedAuthenticationToken.java | 43 ++++++++++++ ...AuthenticatedAuthenticationTokenTests.java | 14 ++++ .../WebAuthnAuthentication.java | 35 ++++++++++ .../WebAuthnAuthenticationTests.java | 15 ++++ 27 files changed, 1016 insertions(+), 1 deletion(-) create mode 100644 core/src/main/java/org/springframework/security/core/NoopAuthenticationBuilder.java create mode 100644 core/src/test/java/org/springframework/security/authentication/AbstractAuthenticationBuilderTests.java create mode 100644 core/src/test/java/org/springframework/security/authentication/jaas/JaasAuthenticationTokenTests.java create mode 100644 core/src/test/java/org/springframework/security/authentication/ott/OneTimeTokenAuthenticationTests.java create mode 100644 saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/Saml2AssertionAuthenticationTests.java 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"); + } + } From 8468c6a80503383c82ccdc00f2892519558f08d3 Mon Sep 17 00:00:00 2001 From: Josh Cummings <3627351+jzheaux@users.noreply.github.com> Date: Fri, 22 Aug 2025 16:23:14 -0600 Subject: [PATCH 02/14] Propagate Previous Factor to Next One This commit allows looking up the current authentication and applying it to the latest authentication. This is specifically handy when collecting authorities gained from each authentication factor. Issue gh-17862 --- ...legatingReactiveAuthenticationManager.java | 16 +++++++++++- .../authentication/ProviderManager.java | 25 +++++++++++++++++++ ...ingReactiveAuthenticationManagerTests.java | 21 ++++++++++++++++ .../authentication/ProviderManagerTests.java | 20 +++++++++++++++ 4 files changed, 81 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/org/springframework/security/authentication/DelegatingReactiveAuthenticationManager.java b/core/src/main/java/org/springframework/security/authentication/DelegatingReactiveAuthenticationManager.java index 2b489dbbdb..1a40d210e0 100644 --- a/core/src/main/java/org/springframework/security/authentication/DelegatingReactiveAuthenticationManager.java +++ b/core/src/main/java/org/springframework/security/authentication/DelegatingReactiveAuthenticationManager.java @@ -27,6 +27,7 @@ import reactor.core.publisher.Mono; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.context.ReactiveSecurityContextHolder; import org.springframework.util.Assert; /** @@ -57,11 +58,24 @@ public class DelegatingReactiveAuthenticationManager implements ReactiveAuthenti @Override public Mono authenticate(Authentication authentication) { + return ReactiveSecurityContextHolder.getContext().flatMap((context) -> { + Mono result = doAuthenticate(authentication); + Authentication current = context.getAuthentication(); + if (current == null) { + return result; + } + if (!current.isAuthenticated()) { + return result; + } + return doAuthenticate(current).map((r) -> r.toBuilder().apply(current).build()); + }).switchIfEmpty(doAuthenticate(authentication)); + } + + private Mono doAuthenticate(Authentication authentication) { Flux result = Flux.fromIterable(this.delegates); Function> logging = (m) -> m.authenticate(authentication) .doOnError(AuthenticationException.class, (ex) -> ex.setAuthenticationRequest(authentication)) .doOnError(this.logger::debug); - return ((this.continueOnError) ? result.concatMapDelayError(logging) : result.concatMap(logging)).next(); } diff --git a/core/src/main/java/org/springframework/security/authentication/ProviderManager.java b/core/src/main/java/org/springframework/security/authentication/ProviderManager.java index d90bfe5bad..417d144849 100644 --- a/core/src/main/java/org/springframework/security/authentication/ProviderManager.java +++ b/core/src/main/java/org/springframework/security/authentication/ProviderManager.java @@ -33,6 +33,8 @@ import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.CredentialsContainer; import org.springframework.security.core.SpringSecurityMessageSource; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.context.SecurityContextHolderStrategy; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; @@ -92,6 +94,9 @@ public class ProviderManager implements AuthenticationManager, MessageSourceAwar private static final Log logger = LogFactory.getLog(ProviderManager.class); + private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder + .getContextHolderStrategy(); + private AuthenticationEventPublisher eventPublisher = new NullEventPublisher(); private List providers = Collections.emptyList(); @@ -209,6 +214,7 @@ public class ProviderManager implements AuthenticationManager, MessageSourceAwar lastException = ex; } } + result = applyPreviousAuthentication(result); if (result == null && this.parent != null) { // Allow the parent to try. try { @@ -265,6 +271,20 @@ public class ProviderManager implements AuthenticationManager, MessageSourceAwar throw lastException; } + private @Nullable Authentication applyPreviousAuthentication(@Nullable Authentication result) { + if (result == null) { + return null; + } + Authentication current = this.securityContextHolderStrategy.getContext().getAuthentication(); + if (current == null) { + return result; + } + if (!current.isAuthenticated()) { + return result; + } + return result.toBuilder().apply(current).build(); + } + @SuppressWarnings("deprecation") private void prepareException(AuthenticationException ex, Authentication auth) { ex.setAuthenticationRequest(auth); @@ -287,6 +307,11 @@ public class ProviderManager implements AuthenticationManager, MessageSourceAwar return this.providers; } + public void setSecurityContextHolderStrategy(SecurityContextHolderStrategy securityContextHolderStrategy) { + Assert.notNull(securityContextHolderStrategy, "securityContextHolderStrategy cannot be null"); + this.securityContextHolderStrategy = securityContextHolderStrategy; + } + @Override public void setMessageSource(MessageSource messageSource) { this.messages = new MessageSourceAccessor(messageSource); diff --git a/core/src/test/java/org/springframework/security/authentication/DelegatingReactiveAuthenticationManagerTests.java b/core/src/test/java/org/springframework/security/authentication/DelegatingReactiveAuthenticationManagerTests.java index d44278a60f..6c430bdf03 100644 --- a/core/src/test/java/org/springframework/security/authentication/DelegatingReactiveAuthenticationManagerTests.java +++ b/core/src/test/java/org/springframework/security/authentication/DelegatingReactiveAuthenticationManagerTests.java @@ -27,10 +27,13 @@ import reactor.test.StepVerifier; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.context.ReactiveSecurityContextHolder; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; /** * @author Rob Winch @@ -118,6 +121,24 @@ public class DelegatingReactiveAuthenticationManagerTests { assertThat(expected.getAuthenticationRequest()).isEqualTo(this.authentication); } + @Test + void authenticateWhenPreviousAuthenticationThenApplies() { + Authentication factorOne = new TestingAuthenticationToken("user", "pass", "FACTOR_ONE"); + Authentication factorTwo = new TestingAuthenticationToken("user", "pass", "FACTOR_TWO"); + ReactiveAuthenticationManager provider = mock(ReactiveAuthenticationManager.class); + given(provider.authenticate(any())).willReturn(Mono.just(factorTwo)); + ReactiveAuthenticationManager manager = new DelegatingReactiveAuthenticationManager(provider); + Authentication request = new TestingAuthenticationToken("user", "password"); + StepVerifier + .create(manager.authenticate(request) + .flatMapIterable(Authentication::getAuthorities) + .map(GrantedAuthority::getAuthority) + .contextWrite(ReactiveSecurityContextHolder.withAuthentication(factorOne))) + .expectNext("FACTOR_TWO") + .expectNext("FACTOR_ONE") + .verifyComplete(); + } + private DelegatingReactiveAuthenticationManager managerWithContinueOnError() { DelegatingReactiveAuthenticationManager manager = new DelegatingReactiveAuthenticationManager(this.delegate1, this.delegate2); diff --git a/core/src/test/java/org/springframework/security/authentication/ProviderManagerTests.java b/core/src/test/java/org/springframework/security/authentication/ProviderManagerTests.java index 7bb0c136bc..e1c69c5fe7 100644 --- a/core/src/test/java/org/springframework/security/authentication/ProviderManagerTests.java +++ b/core/src/test/java/org/springframework/security/authentication/ProviderManagerTests.java @@ -19,12 +19,16 @@ package org.springframework.security.authentication; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Set; import org.junit.jupiter.api.Test; import org.springframework.context.MessageSource; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.core.context.SecurityContextHolderStrategy; +import org.springframework.security.core.context.SecurityContextImpl; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; @@ -310,6 +314,22 @@ public class ProviderManagerTests { verifyNoMoreInteractions(publisher); // Child should not publish (duplicate event) } + @Test + void authenticateWhenPreviousAuthenticationThenApplies() { + Authentication factorOne = new TestingAuthenticationToken("user", "pass", "FACTOR_ONE"); + Authentication factorTwo = new TestingAuthenticationToken("user", "pass", "FACTOR_TWO"); + SecurityContextHolderStrategy securityContextHolderStrategy = mock(SecurityContextHolderStrategy.class); + given(securityContextHolderStrategy.getContext()).willReturn(new SecurityContextImpl(factorOne)); + AuthenticationProvider provider = mock(AuthenticationProvider.class); + given(provider.authenticate(any())).willReturn(factorTwo); + given(provider.supports(any())).willReturn(true); + ProviderManager manager = new ProviderManager(provider); + manager.setSecurityContextHolderStrategy(securityContextHolderStrategy); + Authentication request = new TestingAuthenticationToken("user", "password"); + Set authorities = AuthorityUtils.authorityListToSet(manager.authenticate(request).getAuthorities()); + assertThat(authorities).containsExactlyInAnyOrder("FACTOR_ONE", "FACTOR_TWO"); + } + private AuthenticationProvider createProviderWhichThrows(final AuthenticationException ex) { AuthenticationProvider provider = mock(AuthenticationProvider.class); given(provider.supports(any(Class.class))).willReturn(true); From 44fef786aaf9fe49b2d604179231a63849c4cefb Mon Sep 17 00:00:00 2001 From: Josh Cummings <3627351+jzheaux@users.noreply.github.com> Date: Fri, 22 Aug 2025 16:24:25 -0600 Subject: [PATCH 03/14] Pick Up SecurityContextHolderStrategy Bean This commit provides the SecurityContextHolderStrategy bean to ProviderManager instances that the HttpSecurity DSL constructs. Issue gh-17862 --- .../AuthenticationManagerBuilder.java | 30 +++++++++++++++++++ .../AuthenticationConfiguration.java | 2 ++ .../GlobalMethodSecurityConfiguration.java | 1 + .../HttpSecurityConfiguration.java | 2 ++ .../web/configurers/WebAuthnConfigurer.java | 6 ++-- .../AuthenticationManagerFactoryBean.java | 6 ++++ 6 files changed, 45 insertions(+), 2 deletions(-) diff --git a/config/src/main/java/org/springframework/security/config/annotation/authentication/builders/AuthenticationManagerBuilder.java b/config/src/main/java/org/springframework/security/config/annotation/authentication/builders/AuthenticationManagerBuilder.java index 64f5fd489b..961cf67c56 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/authentication/builders/AuthenticationManagerBuilder.java +++ b/config/src/main/java/org/springframework/security/config/annotation/authentication/builders/AuthenticationManagerBuilder.java @@ -18,10 +18,14 @@ package org.springframework.security.config.annotation.authentication.builders; import java.util.ArrayList; import java.util.List; +import java.util.stream.Stream; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.security.authentication.AuthenticationEventPublisher; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationProvider; @@ -37,6 +41,8 @@ import org.springframework.security.config.annotation.authentication.configurers import org.springframework.security.config.annotation.authentication.configurers.userdetails.DaoAuthenticationConfigurer; import org.springframework.security.config.annotation.authentication.configurers.userdetails.UserDetailsAwareConfigurer; import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.context.SecurityContextHolderStrategy; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.util.Assert; @@ -235,6 +241,10 @@ public class AuthenticationManagerBuilder if (this.eventPublisher != null) { providerManager.setAuthenticationEventPublisher(this.eventPublisher); } + SecurityContextHolderStrategy securityContextHolderStrategy = getBeanProvider( + SecurityContextHolderStrategy.class) + .getIfUnique(SecurityContextHolder::getContextHolderStrategy); + providerManager.setSecurityContextHolderStrategy(securityContextHolderStrategy); providerManager = postProcess(providerManager); return providerManager; } @@ -283,4 +293,24 @@ public class AuthenticationManagerBuilder return configurer; } + private ObjectProvider getBeanProvider(Class clazz) { + BeanFactory beanFactory = getSharedObject(BeanFactory.class); + return (beanFactory != null) ? beanFactory.getBeanProvider(clazz) : new SingleObjectProvider<>(null); + } + + private static final class SingleObjectProvider implements ObjectProvider { + + private final @Nullable O object; + + private SingleObjectProvider(@Nullable O object) { + this.object = object; + } + + @Override + public Stream stream() { + return Stream.ofNullable(this.object); + } + + } + } diff --git a/config/src/main/java/org/springframework/security/config/annotation/authentication/configuration/AuthenticationConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/authentication/configuration/AuthenticationConfiguration.java index 77b5f38986..72f9e8ac88 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/authentication/configuration/AuthenticationConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/authentication/configuration/AuthenticationConfiguration.java @@ -27,6 +27,7 @@ import org.apache.commons.logging.LogFactory; import org.springframework.aop.framework.ProxyFactoryBean; import org.springframework.aop.target.LazyInitTargetSource; +import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; @@ -83,6 +84,7 @@ public class AuthenticationConfiguration { AuthenticationEventPublisher authenticationEventPublisher = getAuthenticationEventPublisher(context); DefaultPasswordEncoderAuthenticationManagerBuilder result = new DefaultPasswordEncoderAuthenticationManagerBuilder( objectPostProcessor, defaultPasswordEncoder); + result.setSharedObject(BeanFactory.class, this.applicationContext); if (authenticationEventPublisher != null) { result.authenticationEventPublisher(authenticationEventPublisher); } diff --git a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/GlobalMethodSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/GlobalMethodSecurityConfiguration.java index 7b0fc0a13c..fe1c4028d0 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/GlobalMethodSecurityConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/GlobalMethodSecurityConfiguration.java @@ -318,6 +318,7 @@ public class GlobalMethodSecurityConfiguration implements ImportAware, SmartInit .postProcess(new DefaultAuthenticationEventPublisher()); this.auth = new AuthenticationManagerBuilder(this.objectPostProcessor); this.auth.authenticationEventPublisher(eventPublisher); + this.auth.setSharedObject(BeanFactory.class, this.context); configure(this.auth); this.authenticationManager = (this.disableAuthenticationRegistry) ? getAuthenticationConfiguration().getAuthenticationManager() : this.auth.build(); diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/HttpSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/HttpSecurityConfiguration.java index ad641ea656..791b45566c 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/HttpSecurityConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/HttpSecurityConfiguration.java @@ -21,6 +21,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; @@ -116,6 +117,7 @@ class HttpSecurityConfiguration { LazyPasswordEncoder passwordEncoder = new LazyPasswordEncoder(this.context); AuthenticationManagerBuilder authenticationBuilder = new DefaultPasswordEncoderAuthenticationManagerBuilder( this.objectPostProcessor, passwordEncoder); + authenticationBuilder.setSharedObject(BeanFactory.class, this.context); authenticationBuilder.parentAuthenticationManager(authenticationManager()); authenticationBuilder.authenticationEventPublisher(getAuthenticationEventPublisher()); HttpSecurity http = new HttpSecurity(this.objectPostProcessor, authenticationBuilder, createSharedObjects()); diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/WebAuthnConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/WebAuthnConfigurer.java index 7ec3279efb..c23fbc230c 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/WebAuthnConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/WebAuthnConfigurer.java @@ -162,8 +162,10 @@ public class WebAuthnConfigurer> WebAuthnRelyingPartyOperations rpOperations = webAuthnRelyingPartyOperations(userEntities, userCredentials); PublicKeyCredentialCreationOptionsRepository creationOptionsRepository = creationOptionsRepository(); WebAuthnAuthenticationFilter webAuthnAuthnFilter = new WebAuthnAuthenticationFilter(); - webAuthnAuthnFilter.setAuthenticationManager( - new ProviderManager(new WebAuthnAuthenticationProvider(rpOperations, userDetailsService))); + ProviderManager manager = new ProviderManager( + new WebAuthnAuthenticationProvider(rpOperations, userDetailsService)); + manager.setSecurityContextHolderStrategy(getSecurityContextHolderStrategy()); + webAuthnAuthnFilter.setAuthenticationManager(manager); WebAuthnRegistrationFilter webAuthnRegistrationFilter = new WebAuthnRegistrationFilter(userCredentials, rpOperations); PublicKeyCredentialCreationOptionsFilter creationOptionsFilter = new PublicKeyCredentialCreationOptionsFilter( diff --git a/config/src/main/java/org/springframework/security/config/authentication/AuthenticationManagerFactoryBean.java b/config/src/main/java/org/springframework/security/config/authentication/AuthenticationManagerFactoryBean.java index afa0b11bea..14e3b5f53b 100644 --- a/config/src/main/java/org/springframework/security/config/authentication/AuthenticationManagerFactoryBean.java +++ b/config/src/main/java/org/springframework/security/config/authentication/AuthenticationManagerFactoryBean.java @@ -30,6 +30,8 @@ import org.springframework.security.authentication.ObservationAuthenticationMana import org.springframework.security.authentication.ProviderManager; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.config.BeanIds; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.context.SecurityContextHolderStrategy; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.password.PasswordEncoder; @@ -72,6 +74,10 @@ public class AuthenticationManagerFactoryBean implements FactoryBean Date: Fri, 29 Aug 2025 13:13:09 -0600 Subject: [PATCH 04/14] 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 --- .../CasAuthenticationToken.java | 91 +++++++++++-------- .../CasServiceTicketAuthenticationToken.java | 51 ++++++++++- .../CasAuthenticationTokenTests.java | 9 +- ...onfigurerTransientAuthenticationTests.java | 5 +- ...entConfigTransientAuthenticationTests.java | 5 +- .../AbstractAuthenticationToken.java | 52 +++++++---- .../RememberMeAuthenticationToken.java | 52 ++++++----- .../TestingAuthenticationToken.java | 59 ++++++------ .../UsernamePasswordAuthenticationToken.java | 39 ++++---- .../jaas/JaasAuthenticationToken.java | 36 ++++---- .../ott/OneTimeTokenAuthentication.java | 37 ++++---- .../security/core/Authentication.java | 40 +++----- .../core/NoopAuthenticationBuilder.java | 36 ++++++-- .../AbstractAuthenticationBuilderTests.java | 30 +++--- .../TestingAuthenticationTokenTests.java | 6 +- ...rnamePasswordAuthenticationTokenTests.java | 13 ++- .../jaas/JaasAuthenticationTokenTests.java | 7 +- .../ott/OneTimeTokenAuthenticationTests.java | 5 +- .../oauth2/client/OAuth2AuthorizeRequest.java | 4 +- .../OAuth2AuthenticationToken.java | 53 ++++++----- ...uthorizedClientExchangeFilterFunction.java | 4 +- .../OAuth2AuthenticationTokenTests.java | 6 +- ...bstractOAuth2TokenAuthenticationToken.java | 54 +++++++++++ .../BearerTokenAuthentication.java | 57 +++++++----- .../JwtAuthenticationToken.java | 35 +++---- .../BearerTokenAuthenticationFilter.java | 6 ++ .../BearerTokenAuthenticationTests.java | 6 +- .../JwtAuthenticationTokenTests.java | 8 +- .../BearerTokenAuthenticationFilterTests.java | 23 +++++ .../Saml2AssertionAuthentication.java | 64 +++++++------ .../authentication/Saml2Authentication.java | 33 +++++++ .../Saml2AssertionAuthenticationTests.java | 7 +- .../PreAuthenticatedAuthenticationToken.java | 55 ++++++----- ...AuthenticatedAuthenticationTokenTests.java | 6 +- ...SessionSecurityContextRepositoryTests.java | 6 +- .../WebAuthnAuthentication.java | 36 ++++---- .../WebAuthnAuthenticationTests.java | 5 +- 37 files changed, 662 insertions(+), 379 deletions(-) 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 9fd4ba204e..68761fccf4 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,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> extends AbstractAuthenticationBuilder { 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; - } - - 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; + return (B) this; } @Override - protected @NonNull CasAuthenticationToken build(Collection authorities) { - return new CasAuthenticationToken(this.keyHash, this.principal, this.credentials, authorities, - this.userDetails, this.assertion); + 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 userDetails(UserDetails userDetails) { + this.userDetails = userDetails; + return (B) this; + } + + public B assertion(Assertion assertion) { + this.assertion = assertion; + return (B) this; + } + + @Override + public CasAuthenticationToken build() { + return new CasAuthenticationToken(this); } } diff --git a/cas/src/main/java/org/springframework/security/cas/authentication/CasServiceTicketAuthenticationToken.java b/cas/src/main/java/org/springframework/security/cas/authentication/CasServiceTicketAuthenticationToken.java index ac77f48c5b..4d791adccc 100644 --- a/cas/src/main/java/org/springframework/security/cas/authentication/CasServiceTicketAuthenticationToken.java +++ b/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) 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> extends AbstractAuthenticationBuilder { + + 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); + } + + } + } 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 bdd26916a6..d489d00ff3 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 @@ -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 authorities = AuthorityUtils.authorityListToSet(authentication.getAuthorities()); assertThat(authentication.getKeyHash()).isEqualTo(factorTwo.getKeyHash()); assertThat(authentication.getPrincipal()).isEqualTo(factorTwo.getPrincipal()); diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerTransientAuthenticationTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerTransientAuthenticationTests.java index 2616edb1fc..4dc7c3ee3e 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerTransientAuthenticationTests.java +++ b/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) null); } @Override diff --git a/config/src/test/java/org/springframework/security/config/http/SessionManagementConfigTransientAuthenticationTests.java b/config/src/test/java/org/springframework/security/config/http/SessionManagementConfigTransientAuthenticationTests.java index c2aeef4c0a..97c18412b7 100644 --- a/config/src/test/java/org/springframework/security/config/http/SessionManagementConfigTransientAuthenticationTests.java +++ b/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) null); } @Override 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 243a3f8fb8..02e60bf25e 100644 --- a/core/src/main/java/org/springframework/security/authentication/AbstractAuthenticationToken.java +++ b/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 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 getAuthorities() { return this.authorities; @@ -187,36 +195,40 @@ public abstract class AbstractAuthenticationToken implements Authentication, Cre return sb.toString(); } - protected abstract static class AbstractAuthenticationBuilder> - implements Builder { + protected abstract static class AbstractAuthenticationBuilder> + implements Authentication.Builder { - private final Collection authorities = new HashSet<>(); + protected boolean authenticated; - protected AbstractAuthenticationBuilder() { + protected @Nullable Object details; + protected final Collection authorities; + + protected AbstractAuthenticationBuilder(AbstractAuthenticationToken token) { + this.authorities = new LinkedHashSet<>(token.getAuthorities()); + this.authenticated = token.isAuthenticated(); + this.details = token.getDetails(); + } + + @Override + public B authenticated(boolean authenticated) { + this.authenticated = authenticated; + return (B) this; + } + + @Override + public B details(@Nullable Object details) { + this.details = details; + return (B) this; } @Override public B authorities(Consumer> authorities) { authorities.accept(this.authorities); + this.authenticated = true; 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 fa1972b3c0..5ff410ecec 100644 --- a/core/src/main/java/org/springframework/security/authentication/RememberMeAuthenticationToken.java +++ b/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 String * @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 { + public static class Builder> extends AbstractAuthenticationBuilder { - private @Nullable Integer keyHash; + private Integer keyHash; - private @Nullable Object principal; + private 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; + protected Builder(RememberMeAuthenticationToken token) { + super(token); + this.keyHash = token.getKeyHash(); + this.principal = token.getPrincipal(); } @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); + public B principal(@Nullable Object principal) { + Assert.notNull(principal, "principal cannot be null"); + this.principal = principal; + return (B) this; + } + + public B keyHash(int keyHash) { + this.keyHash = keyHash; + return (B) this; + } + + @Override + public RememberMeAuthenticationToken build() { + return new RememberMeAuthenticationToken(this); } } 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 657b596741..f001674ca4 100644 --- a/core/src/main/java/org/springframework/security/authentication/TestingAuthenticationToken.java +++ b/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) 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 { + public static class Builder> extends AbstractAuthenticationBuilder { - private @Nullable Object principal; + private Object principal; - private @Nullable Object credentials; + private 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; + protected Builder(TestingAuthenticationToken token) { + super(token); + this.principal = token.principal; + this.credentials = token.credentials; } @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); + public B principal(@Nullable Object principal) { + Assert.notNull(principal, "principal cannot be null"); + this.principal = principal; + return (B) this; + } + + @Override + public B credentials(@Nullable Object credentials) { + Assert.notNull(credentials, "credentials cannot be null"); + this.credentials = credentials; + return (B) this; + } + + @Override + public TestingAuthenticationToken build() { + return new TestingAuthenticationToken(this); } } 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 33d6b13e02..4bba35a27d 100644 --- a/core/src/main/java/org/springframework/security/authentication/UsernamePasswordAuthenticationToken.java +++ b/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) 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 UsernamePasswordAuthenticationToken. @@ -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> - extends AbstractAuthenticationBuilder { + public static class Builder> extends AbstractAuthenticationBuilder { - private @Nullable Object principal; + protected @Nullable Object principal; - private @Nullable Object credentials; + protected @Nullable Object credentials; - protected Builder() { + protected Builder(UsernamePasswordAuthenticationToken token) { + super(token); + this.principal = token.principal; + this.credentials = token.credentials; } - public B apply(UsernamePasswordAuthenticationToken authentication) { - return super.apply(authentication).principal(authentication.getPrincipal()) - .credentials(authentication.getCredentials()); - } - - 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 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); } } 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 52d8392709..a1dd08a37b 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,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 { + public static class Builder> extends UsernamePasswordAuthenticationToken.Builder { - private @Nullable LoginContext loginContext; + private LoginContext loginContext; - private Builder() { - - } - - 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 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); + public JaasAuthenticationToken build() { + Assert.notNull(this.principal, "principal cannot be null"); + return new JaasAuthenticationToken(this); } } 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 2ccf53b27a..8e73adf65b 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 @@ -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 { + public static class Builder> extends AbstractAuthenticationBuilder { - private @Nullable Object principal; + private 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); + 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 authorities) { - Assert.notNull(this.principal, "principal cannot be null"); - return new OneTimeTokenAuthentication(this.principal, authorities); + public OneTimeTokenAuthentication build() { + return new OneTimeTokenAuthentication(this); } } 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 b07bdd977f..4531842aab 100644 --- a/core/src/main/java/org/springframework/security/core/Authentication.java +++ b/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 AuthenticationManager 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 the type of {@link Authentication} * @author Josh Cummings * @since 7.0 */ - interface Builder> { + 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())); + B authorities(Consumer> 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> 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(); } diff --git a/core/src/main/java/org/springframework/security/core/NoopAuthenticationBuilder.java b/core/src/main/java/org/springframework/security/core/NoopAuthenticationBuilder.java index 163574b2db..982630c8e4 100644 --- a/core/src/main/java/org/springframework/security/core/NoopAuthenticationBuilder.java +++ b/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 the type of {@link Authentication} * @author Josh Cummings * @since 7.0 */ -class NoopAuthenticationBuilder - implements Authentication.Builder> { +class NoopAuthenticationBuilder implements Authentication.Builder { - 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 authorities(Consumer> 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> authorities) { + return this; + } + + @Override + public Authentication 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 index f3babe70f8..50b64ebe27 100644 --- a/core/src/test/java/org/springframework/security/authentication/AbstractAuthenticationBuilderTests.java +++ b/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 authorities = AuthorityUtils.authorityListToSet(result.getAuthorities()); assertThat(authorities).containsExactlyInAnyOrder("FACTOR_ONE", "FACTOR_TWO"); } private static final class TestAbstractAuthenticationBuilder - extends AbstractAuthenticationBuilder { + extends AbstractAuthenticationBuilder { + + private TestAbstractAuthenticationBuilder(TestingAuthenticationToken token) { + super(token); + } @Override - protected Authentication build(Collection authorities) { - return new TestingAuthenticationToken("user", "password", authorities); + public TestAbstractAuthenticationBuilder principal(@Nullable Object principal) { + return this; + } + + @Override + public TestingAuthenticationToken build() { + return new TestingAuthenticationToken("user", "password", this.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 2c17042a7d..6490f2bc3d 100644 --- a/core/src/test/java/org/springframework/security/authentication/TestingAuthenticationTokenTests.java +++ b/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 authorities = AuthorityUtils.authorityListToSet(result.getAuthorities()); assertThat(result.getPrincipal()).isSameAs(factorTwo.getPrincipal()); assertThat(result.getCredentials()).isSameAs(factorTwo.getCredentials()); 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 334e9bfc6b..d09bbdabf8 100644 --- a/core/src/test/java/org/springframework/security/authentication/UsernamePasswordAuthenticationTokenTests.java +++ b/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 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 authorities = AuthorityUtils.authorityListToSet(result.getAuthorities()); + assertThat(result.getPrincipal()).isEqualTo("bob"); + assertThat(result.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 index f307c2d380..a3409848f0 100644 --- a/core/src/test/java/org/springframework/security/authentication/jaas/JaasAuthenticationTokenTests.java +++ b/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 authorities = AuthorityUtils.authorityListToSet(result.getAuthorities()); assertThat(result.getPrincipal()).isSameAs(factorTwo.getPrincipal()); assertThat(result.getCredentials()).isSameAs(factorTwo.getCredentials()); 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 index c8012b82f5..aec07a280d 100644 --- a/core/src/test/java/org/springframework/security/authentication/ott/OneTimeTokenAuthenticationTests.java +++ b/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 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/OAuth2AuthorizeRequest.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/OAuth2AuthorizeRequest.java index 741189304c..82184888b3 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/OAuth2AuthorizeRequest.java +++ b/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) null) { @Override public Object getCredentials() { 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 6b5ac6119b..613e16afbf 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 @@ -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 { + public static 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; + protected Builder(OAuth2AuthenticationToken token) { + super(token); + this.principal = token.principal; + this.authorizedClientRegistrationId = token.authorizedClientRegistrationId; } @Override - protected OAuth2AuthenticationToken build(Collection authorities) { - return new OAuth2AuthenticationToken(this.principal, authorities, this.authorizedClientRegistrationId); + public B principal(@Nullable OAuth2User principal) { + Assert.notNull(principal, "principal cannot be null"); + this.principal = principal; + return (B) this; + } + + public B authorizedClientRegistrationId(String authorizedClientRegistrationId) { + this.authorizedClientRegistrationId = authorizedClientRegistrationId; + return (B) this; + } + + @Override + public OAuth2AuthenticationToken build() { + return new OAuth2AuthenticationToken(this); } } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunction.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunction.java index 47c27acbaa..6f7f8d3a12 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunction.java +++ b/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) null) { @Override public Object getCredentials() { 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 839cd7ce7b..2cd5a9d5e3 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 @@ -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 authorities = AuthorityUtils.authorityListToSet(result.getAuthorities()); assertThat(result.getPrincipal()).isSameAs(factorTwo.getPrincipal()); assertThat(result.getAuthorizedClientRegistrationId()).isSameAs(factorTwo.getAuthorizedClientRegistrationId()); diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/AbstractOAuth2TokenAuthenticationToken.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/AbstractOAuth2TokenAuthenticationToken.java index b8e2be61e0..68898af461 100644 --- a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/AbstractOAuth2TokenAuthenticationToken.java +++ b/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 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 getTokenAttributes(); + /** + * A builder preserving the concrete {@link Authentication} type + * + * @since 7.0 + */ + public abstract static class AbstractOAuth2TokenAuthenticationBuilder> + extends AbstractAuthenticationBuilder { + + private Object principal; + + private Object credentials; + + private T token; + + protected AbstractOAuth2TokenAuthenticationBuilder(AbstractOAuth2TokenAuthenticationToken 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; + } + + } + } 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 6f8606df2e..1fcbe71db5 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,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 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 { + public static class Builder> + extends AbstractOAuth2TokenAuthenticationBuilder { - private OAuth2AuthenticatedPrincipal principal; + private Map attributes; - 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; + protected Builder(BearerTokenAuthentication token) { + super(token); + this.attributes = token.getTokenAttributes(); } @Override - protected BearerTokenAuthentication build(Collection authorities) { - return new BearerTokenAuthentication(this.principal, this.token, authorities); + 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); + } + + @Override + public B token(OAuth2AccessToken token) { + Assert.isTrue(token.getTokenType() == OAuth2AccessToken.TokenType.BEARER, + "credentials must be a bearer token"); + return super.token(token); + } + + @Override + public BearerTokenAuthentication build() { + return new BearerTokenAuthentication(this); } } 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 dfb23244b3..5201e7ab9a 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 @@ -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 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 { - - private Jwt jwt; + public static class Builder> extends AbstractOAuth2TokenAuthenticationBuilder { private String name; - private Builder() { - + protected Builder(JwtAuthenticationToken token) { + super(token); + this.name = token.getName(); } - 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) { + public B name(String name) { this.name = name; - return this; + return (B) this; } @Override - protected JwtAuthenticationToken build(Collection authorities) { - return new JwtAuthenticationToken(this.jwt, authorities, this.name); + public JwtAuthenticationToken build() { + return new JwtAuthenticationToken(this); } } diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/authentication/BearerTokenAuthenticationFilter.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/authentication/BearerTokenAuthenticationFilter.java index 1deb3a7689..89adbf9f5c 100644 --- a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/authentication/BearerTokenAuthenticationFilter.java +++ b/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); 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 eab8208656..d1fef9d1f8 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 @@ -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 authorities = AuthorityUtils.authorityListToSet(authentication.getAuthorities()); assertThat(authentication.getPrincipal()).isSameAs(factorTwo.getPrincipal()); assertThat(authentication.getToken()).isSameAs(factorTwo.getToken()); 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 7780c05c77..2114c6f87b 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 @@ -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 authorities = AuthorityUtils.authorityListToSet(result.getAuthorities()); assertThat(result.getPrincipal()).isSameAs(factorTwo.getPrincipal()); assertThat(result.getName()).isSameAs(factorTwo.getName()); diff --git a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/authentication/BearerTokenAuthenticationFilterTests.java b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/authentication/BearerTokenAuthenticationFilterTests.java index 8f0f91093c..0fc4974a6c 100644 --- a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/authentication/BearerTokenAuthenticationFilterTests.java +++ b/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 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); 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 3b2aa37a2a..e86494f9fc 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,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> + extends Saml2Authentication.Builder { 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; + protected Builder(Saml2AssertionAuthentication token) { + super(token); + this.assertion = token.assertion; + this.relyingPartyRegistrationId = token.relyingPartyRegistrationId; } @Override - protected Saml2AssertionAuthentication build(Collection authorities) { - return new Saml2AssertionAuthentication(this.principal, this.assertion, authorities, - this.relyingPartyRegistrationId); + public B credentials(@Nullable Saml2ResponseAssertionAccessor credentials) { + saml2Response(credentials.getResponseValue()); + Assert.notNull(credentials, "assertion cannot be null"); + this.assertion = credentials; + return (B) this; + } + + public B relyingPartyRegistrationId(String relyingPartyRegistrationId) { + this.relyingPartyRegistrationId = relyingPartyRegistrationId; + return (B) this; + } + + @Override + public Saml2AssertionAuthentication build() { + return new Saml2AssertionAuthentication(this); } } diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2Authentication.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2Authentication.java index 82b4042c49..2a9fe34bd9 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2Authentication.java +++ b/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> extends AbstractAuthenticationBuilder { + + 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; + } + + } + } 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 index bfa0875d06..57b08dee02 100644 --- 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 @@ -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 authorities = AuthorityUtils.authorityListToSet(result.getAuthorities()); assertThat(result.getPrincipal()).isSameAs(factorTwo.getPrincipal()); assertThat(result.getCredentials()).isSameAs(factorTwo.getCredentials()); 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 cb5c523590..0f994bd751 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 @@ -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) 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 { + public static class Builder> extends AbstractAuthenticationBuilder { private Object principal; - private Object credentials; + private @Nullable 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; + protected Builder(PreAuthenticatedAuthenticationToken token) { + super(token); + this.principal = token.principal; + this.credentials = token.credentials; } @Override - protected PreAuthenticatedAuthenticationToken build(Collection authorities) { - return new PreAuthenticatedAuthenticationToken(this.principal, this.credentials, authorities); + public B principal(@Nullable Object principal) { + Assert.notNull(principal, "principal cannot be null"); + this.principal = principal; + return (B) this; + } + + @Override + public B credentials(@Nullable Object credentials) { + this.credentials = credentials; + return (B) this; + } + + @Override + public PreAuthenticatedAuthenticationToken build() { + return new PreAuthenticatedAuthenticationToken(this); } } 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 606dd50f03..fc111528ff 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 @@ -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 authorities = AuthorityUtils.authorityListToSet(result.getAuthorities()); assertThat(result.getPrincipal()).isSameAs(factorTwo.getPrincipal()); assertThat(result.getCredentials()).isSameAs(factorTwo.getCredentials()); diff --git a/web/src/test/java/org/springframework/security/web/context/HttpSessionSecurityContextRepositoryTests.java b/web/src/test/java/org/springframework/security/web/context/HttpSessionSecurityContextRepositoryTests.java index 979c6c8138..a27873d230 100644 --- a/web/src/test/java/org/springframework/security/web/context/HttpSessionSecurityContextRepositoryTests.java +++ b/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) null); } @Override @@ -840,7 +842,7 @@ public class HttpSessionSecurityContextRepositoryTests { private static class SomeOtherTransientAuthentication extends AbstractAuthenticationToken { SomeOtherTransientAuthentication() { - super(null); + super((Collection) null); } @Override 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 9855d17a3e..615781e2d1 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 @@ -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 { + 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; + private Builder(WebAuthnAuthentication token) { + super(token); } @Override - protected WebAuthnAuthentication build(Collection authorities) { - return new WebAuthnAuthentication(this.principal, authorities); + public B principal(@Nullable PublicKeyCredentialUserEntity principal) { + Assert.notNull(principal, "principal cannot be null"); + this.principal = principal; + return (B) this; + } + + @Override + public WebAuthnAuthentication build() { + return new WebAuthnAuthentication(this); } } 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 7b3bc6f37a..409e05623e 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 @@ -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 authorities = AuthorityUtils.authorityListToSet(result.getAuthorities()); assertThat(result.getPrincipal()).isSameAs(factorTwo.getPrincipal()); assertThat(authorities).containsExactlyInAnyOrder("FACTOR_ONE", "FACTOR_TWO"); From 3f774548d2359cd67aa9cfabf0e7245e802bbd1f Mon Sep 17 00:00:00 2001 From: Josh Cummings <3627351+jzheaux@users.noreply.github.com> Date: Fri, 29 Aug 2025 13:14:21 -0600 Subject: [PATCH 05/14] Move Authority Propagation Into Filters Given that the filters are the level at which the SecurityContextHolder is consulted, this commit moves the operation that ProviderManager was doing into each authentication filter. Issue gh-17862 --- .../AuthenticationManagerBuilder.java | 30 -------------- .../AuthenticationConfiguration.java | 2 - .../GlobalMethodSecurityConfiguration.java | 1 - .../HttpSecurityConfiguration.java | 2 - .../web/configurers/WebAuthnConfigurer.java | 6 +-- .../AuthenticationManagerFactoryBean.java | 6 --- .../AbstractAuthenticationToken.java | 2 + ...legatingReactiveAuthenticationManager.java | 15 ------- .../authentication/ProviderManager.java | 37 ++++-------------- ...ingReactiveAuthenticationManagerTests.java | 21 ---------- .../authentication/ProviderManagerTests.java | 39 ++++++------------- ...bstractAuthenticationProcessingFilter.java | 6 +++ .../authentication/AuthenticationFilter.java | 6 +++ ...tractPreAuthenticatedProcessingFilter.java | 6 +++ .../www/BasicAuthenticationFilter.java | 4 ++ .../AuthenticationWebFilter.java | 14 +++++++ .../AuthenticationFilterTests.java | 1 + 17 files changed, 60 insertions(+), 138 deletions(-) diff --git a/config/src/main/java/org/springframework/security/config/annotation/authentication/builders/AuthenticationManagerBuilder.java b/config/src/main/java/org/springframework/security/config/annotation/authentication/builders/AuthenticationManagerBuilder.java index 961cf67c56..64f5fd489b 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/authentication/builders/AuthenticationManagerBuilder.java +++ b/config/src/main/java/org/springframework/security/config/annotation/authentication/builders/AuthenticationManagerBuilder.java @@ -18,14 +18,10 @@ package org.springframework.security.config.annotation.authentication.builders; import java.util.ArrayList; import java.util.List; -import java.util.stream.Stream; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.jspecify.annotations.Nullable; -import org.springframework.beans.factory.BeanFactory; -import org.springframework.beans.factory.ObjectProvider; import org.springframework.security.authentication.AuthenticationEventPublisher; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationProvider; @@ -41,8 +37,6 @@ import org.springframework.security.config.annotation.authentication.configurers import org.springframework.security.config.annotation.authentication.configurers.userdetails.DaoAuthenticationConfigurer; import org.springframework.security.config.annotation.authentication.configurers.userdetails.UserDetailsAwareConfigurer; import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.core.context.SecurityContextHolderStrategy; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.util.Assert; @@ -241,10 +235,6 @@ public class AuthenticationManagerBuilder if (this.eventPublisher != null) { providerManager.setAuthenticationEventPublisher(this.eventPublisher); } - SecurityContextHolderStrategy securityContextHolderStrategy = getBeanProvider( - SecurityContextHolderStrategy.class) - .getIfUnique(SecurityContextHolder::getContextHolderStrategy); - providerManager.setSecurityContextHolderStrategy(securityContextHolderStrategy); providerManager = postProcess(providerManager); return providerManager; } @@ -293,24 +283,4 @@ public class AuthenticationManagerBuilder return configurer; } - private ObjectProvider getBeanProvider(Class clazz) { - BeanFactory beanFactory = getSharedObject(BeanFactory.class); - return (beanFactory != null) ? beanFactory.getBeanProvider(clazz) : new SingleObjectProvider<>(null); - } - - private static final class SingleObjectProvider implements ObjectProvider { - - private final @Nullable O object; - - private SingleObjectProvider(@Nullable O object) { - this.object = object; - } - - @Override - public Stream stream() { - return Stream.ofNullable(this.object); - } - - } - } diff --git a/config/src/main/java/org/springframework/security/config/annotation/authentication/configuration/AuthenticationConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/authentication/configuration/AuthenticationConfiguration.java index 72f9e8ac88..77b5f38986 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/authentication/configuration/AuthenticationConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/authentication/configuration/AuthenticationConfiguration.java @@ -27,7 +27,6 @@ import org.apache.commons.logging.LogFactory; import org.springframework.aop.framework.ProxyFactoryBean; import org.springframework.aop.target.LazyInitTargetSource; -import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; @@ -84,7 +83,6 @@ public class AuthenticationConfiguration { AuthenticationEventPublisher authenticationEventPublisher = getAuthenticationEventPublisher(context); DefaultPasswordEncoderAuthenticationManagerBuilder result = new DefaultPasswordEncoderAuthenticationManagerBuilder( objectPostProcessor, defaultPasswordEncoder); - result.setSharedObject(BeanFactory.class, this.applicationContext); if (authenticationEventPublisher != null) { result.authenticationEventPublisher(authenticationEventPublisher); } diff --git a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/GlobalMethodSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/GlobalMethodSecurityConfiguration.java index fe1c4028d0..7b0fc0a13c 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/GlobalMethodSecurityConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/GlobalMethodSecurityConfiguration.java @@ -318,7 +318,6 @@ public class GlobalMethodSecurityConfiguration implements ImportAware, SmartInit .postProcess(new DefaultAuthenticationEventPublisher()); this.auth = new AuthenticationManagerBuilder(this.objectPostProcessor); this.auth.authenticationEventPublisher(eventPublisher); - this.auth.setSharedObject(BeanFactory.class, this.context); configure(this.auth); this.authenticationManager = (this.disableAuthenticationRegistry) ? getAuthenticationConfiguration().getAuthenticationManager() : this.auth.build(); diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/HttpSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/HttpSecurityConfiguration.java index 791b45566c..ad641ea656 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/HttpSecurityConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/HttpSecurityConfiguration.java @@ -21,7 +21,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; @@ -117,7 +116,6 @@ class HttpSecurityConfiguration { LazyPasswordEncoder passwordEncoder = new LazyPasswordEncoder(this.context); AuthenticationManagerBuilder authenticationBuilder = new DefaultPasswordEncoderAuthenticationManagerBuilder( this.objectPostProcessor, passwordEncoder); - authenticationBuilder.setSharedObject(BeanFactory.class, this.context); authenticationBuilder.parentAuthenticationManager(authenticationManager()); authenticationBuilder.authenticationEventPublisher(getAuthenticationEventPublisher()); HttpSecurity http = new HttpSecurity(this.objectPostProcessor, authenticationBuilder, createSharedObjects()); diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/WebAuthnConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/WebAuthnConfigurer.java index c23fbc230c..7ec3279efb 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/WebAuthnConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/WebAuthnConfigurer.java @@ -162,10 +162,8 @@ public class WebAuthnConfigurer> WebAuthnRelyingPartyOperations rpOperations = webAuthnRelyingPartyOperations(userEntities, userCredentials); PublicKeyCredentialCreationOptionsRepository creationOptionsRepository = creationOptionsRepository(); WebAuthnAuthenticationFilter webAuthnAuthnFilter = new WebAuthnAuthenticationFilter(); - ProviderManager manager = new ProviderManager( - new WebAuthnAuthenticationProvider(rpOperations, userDetailsService)); - manager.setSecurityContextHolderStrategy(getSecurityContextHolderStrategy()); - webAuthnAuthnFilter.setAuthenticationManager(manager); + webAuthnAuthnFilter.setAuthenticationManager( + new ProviderManager(new WebAuthnAuthenticationProvider(rpOperations, userDetailsService))); WebAuthnRegistrationFilter webAuthnRegistrationFilter = new WebAuthnRegistrationFilter(userCredentials, rpOperations); PublicKeyCredentialCreationOptionsFilter creationOptionsFilter = new PublicKeyCredentialCreationOptionsFilter( diff --git a/config/src/main/java/org/springframework/security/config/authentication/AuthenticationManagerFactoryBean.java b/config/src/main/java/org/springframework/security/config/authentication/AuthenticationManagerFactoryBean.java index 14e3b5f53b..afa0b11bea 100644 --- a/config/src/main/java/org/springframework/security/config/authentication/AuthenticationManagerFactoryBean.java +++ b/config/src/main/java/org/springframework/security/config/authentication/AuthenticationManagerFactoryBean.java @@ -30,8 +30,6 @@ import org.springframework.security.authentication.ObservationAuthenticationMana import org.springframework.security.authentication.ProviderManager; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.config.BeanIds; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.core.context.SecurityContextHolderStrategy; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.password.PasswordEncoder; @@ -74,10 +72,6 @@ public class AuthenticationManagerFactoryBean implements FactoryBean authorities; diff --git a/core/src/main/java/org/springframework/security/authentication/DelegatingReactiveAuthenticationManager.java b/core/src/main/java/org/springframework/security/authentication/DelegatingReactiveAuthenticationManager.java index 1a40d210e0..c30fcbae3e 100644 --- a/core/src/main/java/org/springframework/security/authentication/DelegatingReactiveAuthenticationManager.java +++ b/core/src/main/java/org/springframework/security/authentication/DelegatingReactiveAuthenticationManager.java @@ -27,7 +27,6 @@ import reactor.core.publisher.Mono; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; -import org.springframework.security.core.context.ReactiveSecurityContextHolder; import org.springframework.util.Assert; /** @@ -58,20 +57,6 @@ public class DelegatingReactiveAuthenticationManager implements ReactiveAuthenti @Override public Mono authenticate(Authentication authentication) { - return ReactiveSecurityContextHolder.getContext().flatMap((context) -> { - Mono result = doAuthenticate(authentication); - Authentication current = context.getAuthentication(); - if (current == null) { - return result; - } - if (!current.isAuthenticated()) { - return result; - } - return doAuthenticate(current).map((r) -> r.toBuilder().apply(current).build()); - }).switchIfEmpty(doAuthenticate(authentication)); - } - - private Mono doAuthenticate(Authentication authentication) { Flux result = Flux.fromIterable(this.delegates); Function> logging = (m) -> m.authenticate(authentication) .doOnError(AuthenticationException.class, (ex) -> ex.setAuthenticationRequest(authentication)) diff --git a/core/src/main/java/org/springframework/security/authentication/ProviderManager.java b/core/src/main/java/org/springframework/security/authentication/ProviderManager.java index 417d144849..7167943a33 100644 --- a/core/src/main/java/org/springframework/security/authentication/ProviderManager.java +++ b/core/src/main/java/org/springframework/security/authentication/ProviderManager.java @@ -33,8 +33,6 @@ import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.CredentialsContainer; import org.springframework.security.core.SpringSecurityMessageSource; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.core.context.SecurityContextHolderStrategy; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; @@ -94,9 +92,6 @@ public class ProviderManager implements AuthenticationManager, MessageSourceAwar private static final Log logger = LogFactory.getLog(ProviderManager.class); - private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder - .getContextHolderStrategy(); - private AuthenticationEventPublisher eventPublisher = new NullEventPublisher(); private List providers = Collections.emptyList(); @@ -187,7 +182,7 @@ public class ProviderManager implements AuthenticationManager, MessageSourceAwar try { result = provider.authenticate(authentication); if (result != null) { - copyDetails(authentication, result); + result = copyDetails(authentication, result); break; } } @@ -214,7 +209,6 @@ public class ProviderManager implements AuthenticationManager, MessageSourceAwar lastException = ex; } } - result = applyPreviousAuthentication(result); if (result == null && this.parent != null) { // Allow the parent to try. try { @@ -271,20 +265,6 @@ public class ProviderManager implements AuthenticationManager, MessageSourceAwar throw lastException; } - private @Nullable Authentication applyPreviousAuthentication(@Nullable Authentication result) { - if (result == null) { - return null; - } - Authentication current = this.securityContextHolderStrategy.getContext().getAuthentication(); - if (current == null) { - return result; - } - if (!current.isAuthenticated()) { - return result; - } - return result.toBuilder().apply(current).build(); - } - @SuppressWarnings("deprecation") private void prepareException(AuthenticationException ex, Authentication auth) { ex.setAuthenticationRequest(auth); @@ -297,21 +277,20 @@ public class ProviderManager implements AuthenticationManager, MessageSourceAwar * @param source source authentication * @param dest the destination authentication object */ - private void copyDetails(Authentication source, Authentication dest) { - if ((dest instanceof AbstractAuthenticationToken token) && (dest.getDetails() == null)) { - token.setDetails(source.getDetails()); + private Authentication copyDetails(Authentication source, Authentication dest) { + if (source.getDetails() == null) { + return dest; } + if (dest.getDetails() != null) { + return dest; + } + return dest.toBuilder().details(source.getDetails()).build(); } public List getProviders() { return this.providers; } - public void setSecurityContextHolderStrategy(SecurityContextHolderStrategy securityContextHolderStrategy) { - Assert.notNull(securityContextHolderStrategy, "securityContextHolderStrategy cannot be null"); - this.securityContextHolderStrategy = securityContextHolderStrategy; - } - @Override public void setMessageSource(MessageSource messageSource) { this.messages = new MessageSourceAccessor(messageSource); diff --git a/core/src/test/java/org/springframework/security/authentication/DelegatingReactiveAuthenticationManagerTests.java b/core/src/test/java/org/springframework/security/authentication/DelegatingReactiveAuthenticationManagerTests.java index 6c430bdf03..d44278a60f 100644 --- a/core/src/test/java/org/springframework/security/authentication/DelegatingReactiveAuthenticationManagerTests.java +++ b/core/src/test/java/org/springframework/security/authentication/DelegatingReactiveAuthenticationManagerTests.java @@ -27,13 +27,10 @@ import reactor.test.StepVerifier; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.context.ReactiveSecurityContextHolder; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; /** * @author Rob Winch @@ -121,24 +118,6 @@ public class DelegatingReactiveAuthenticationManagerTests { assertThat(expected.getAuthenticationRequest()).isEqualTo(this.authentication); } - @Test - void authenticateWhenPreviousAuthenticationThenApplies() { - Authentication factorOne = new TestingAuthenticationToken("user", "pass", "FACTOR_ONE"); - Authentication factorTwo = new TestingAuthenticationToken("user", "pass", "FACTOR_TWO"); - ReactiveAuthenticationManager provider = mock(ReactiveAuthenticationManager.class); - given(provider.authenticate(any())).willReturn(Mono.just(factorTwo)); - ReactiveAuthenticationManager manager = new DelegatingReactiveAuthenticationManager(provider); - Authentication request = new TestingAuthenticationToken("user", "password"); - StepVerifier - .create(manager.authenticate(request) - .flatMapIterable(Authentication::getAuthorities) - .map(GrantedAuthority::getAuthority) - .contextWrite(ReactiveSecurityContextHolder.withAuthentication(factorOne))) - .expectNext("FACTOR_TWO") - .expectNext("FACTOR_ONE") - .verifyComplete(); - } - private DelegatingReactiveAuthenticationManager managerWithContinueOnError() { DelegatingReactiveAuthenticationManager manager = new DelegatingReactiveAuthenticationManager(this.delegate1, this.delegate2); diff --git a/core/src/test/java/org/springframework/security/authentication/ProviderManagerTests.java b/core/src/test/java/org/springframework/security/authentication/ProviderManagerTests.java index e1c69c5fe7..69fff2567c 100644 --- a/core/src/test/java/org/springframework/security/authentication/ProviderManagerTests.java +++ b/core/src/test/java/org/springframework/security/authentication/ProviderManagerTests.java @@ -18,17 +18,15 @@ package org.springframework.security.authentication; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.List; -import java.util.Set; import org.junit.jupiter.api.Test; import org.springframework.context.MessageSource; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; -import org.springframework.security.core.authority.AuthorityUtils; -import org.springframework.security.core.context.SecurityContextHolderStrategy; -import org.springframework.security.core.context.SecurityContextImpl; +import org.springframework.security.core.GrantedAuthority; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; @@ -51,7 +49,7 @@ public class ProviderManagerTests { @Test void authenticationFailsWithUnsupportedToken() { - Authentication token = new AbstractAuthenticationToken(null) { + Authentication token = new AbstractAuthenticationToken((Collection) null) { @Override public Object getCredentials() { return ""; @@ -82,24 +80,24 @@ public class ProviderManagerTests { @Test void authenticationSucceedsWithSupportedTokenAndReturnsExpectedObject() { - Authentication a = mock(Authentication.class); + Authentication a = new TestingAuthenticationToken("user", "pass", "FACTOR"); ProviderManager mgr = new ProviderManager(createProviderWhichReturns(a)); AuthenticationEventPublisher publisher = mock(AuthenticationEventPublisher.class); mgr.setAuthenticationEventPublisher(publisher); Authentication result = mgr.authenticate(a); - assertThat(result).isEqualTo(a); + assertThat(result.getPrincipal()).isEqualTo(a.getPrincipal()); verify(publisher).publishAuthenticationSuccess(result); } @Test void authenticationSucceedsWhenFirstProviderReturnsNullButSecondAuthenticates() { - Authentication a = mock(Authentication.class); + Authentication a = new TestingAuthenticationToken("user", "pass", "FACTOR"); ProviderManager mgr = new ProviderManager( Arrays.asList(createProviderWhichReturns(null), createProviderWhichReturns(a))); AuthenticationEventPublisher publisher = mock(AuthenticationEventPublisher.class); mgr.setAuthenticationEventPublisher(publisher); Authentication result = mgr.authenticate(a); - assertThat(result).isSameAs(a); + assertThat(result.getPrincipal()).isEqualTo(a.getPrincipal()); verify(publisher).publishAuthenticationSuccess(result); } @@ -166,11 +164,12 @@ public class ProviderManagerTests { @Test void authenticationExceptionIsIgnoredIfLaterProviderAuthenticates() { - Authentication authReq = mock(Authentication.class); + Authentication result = new TestingAuthenticationToken("user", "pass", "FACTOR"); ProviderManager mgr = new ProviderManager( createProviderWhichThrows(new BadCredentialsException("", new Throwable())), - createProviderWhichReturns(authReq)); - assertThat(mgr.authenticate(mock(Authentication.class))).isSameAs(authReq); + createProviderWhichReturns(result)); + Authentication request = new TestingAuthenticationToken("user", "pass"); + assertThat(mgr.authenticate(request).getPrincipal()).isEqualTo(result.getPrincipal()); } @Test @@ -314,22 +313,6 @@ public class ProviderManagerTests { verifyNoMoreInteractions(publisher); // Child should not publish (duplicate event) } - @Test - void authenticateWhenPreviousAuthenticationThenApplies() { - Authentication factorOne = new TestingAuthenticationToken("user", "pass", "FACTOR_ONE"); - Authentication factorTwo = new TestingAuthenticationToken("user", "pass", "FACTOR_TWO"); - SecurityContextHolderStrategy securityContextHolderStrategy = mock(SecurityContextHolderStrategy.class); - given(securityContextHolderStrategy.getContext()).willReturn(new SecurityContextImpl(factorOne)); - AuthenticationProvider provider = mock(AuthenticationProvider.class); - given(provider.authenticate(any())).willReturn(factorTwo); - given(provider.supports(any())).willReturn(true); - ProviderManager manager = new ProviderManager(provider); - manager.setSecurityContextHolderStrategy(securityContextHolderStrategy); - Authentication request = new TestingAuthenticationToken("user", "password"); - Set authorities = AuthorityUtils.authorityListToSet(manager.authenticate(request).getAuthorities()); - assertThat(authorities).containsExactlyInAnyOrder("FACTOR_ONE", "FACTOR_TWO"); - } - private AuthenticationProvider createProviderWhichThrows(final AuthenticationException ex) { AuthenticationProvider provider = mock(AuthenticationProvider.class); given(provider.supports(any(Class.class))).willReturn(true); diff --git a/web/src/main/java/org/springframework/security/web/authentication/AbstractAuthenticationProcessingFilter.java b/web/src/main/java/org/springframework/security/web/authentication/AbstractAuthenticationProcessingFilter.java index a817d7013e..6bbbb3f766 100644 --- a/web/src/main/java/org/springframework/security/web/authentication/AbstractAuthenticationProcessingFilter.java +++ b/web/src/main/java/org/springframework/security/web/authentication/AbstractAuthenticationProcessingFilter.java @@ -248,6 +248,12 @@ public abstract class AbstractAuthenticationProcessingFilter extends GenericFilt // return immediately as subclass has indicated that it hasn't completed return; } + Authentication current = this.securityContextHolderStrategy.getContext().getAuthentication(); + if (current != null && current.isAuthenticated()) { + authenticationResult = authenticationResult.toBuilder() + .authorities((a) -> a.addAll(current.getAuthorities())) + .build(); + } this.sessionStrategy.onAuthentication(authenticationResult, request, response); // Authentication success if (this.continueChainBeforeSuccessfulAuthentication) { diff --git a/web/src/main/java/org/springframework/security/web/authentication/AuthenticationFilter.java b/web/src/main/java/org/springframework/security/web/authentication/AuthenticationFilter.java index 5366ebd84b..8295a962bc 100644 --- a/web/src/main/java/org/springframework/security/web/authentication/AuthenticationFilter.java +++ b/web/src/main/java/org/springframework/security/web/authentication/AuthenticationFilter.java @@ -184,6 +184,12 @@ public class AuthenticationFilter extends OncePerRequestFilter { filterChain.doFilter(request, response); return; } + Authentication current = this.securityContextHolderStrategy.getContext().getAuthentication(); + if (current != null && current.isAuthenticated()) { + authenticationResult = authenticationResult.toBuilder() + .authorities((a) -> a.addAll(current.getAuthorities())) + .build(); + } HttpSession session = request.getSession(false); if (session != null) { request.changeSessionId(); diff --git a/web/src/main/java/org/springframework/security/web/authentication/preauth/AbstractPreAuthenticatedProcessingFilter.java b/web/src/main/java/org/springframework/security/web/authentication/preauth/AbstractPreAuthenticatedProcessingFilter.java index 8189cc2b1f..b0910c85ed 100755 --- a/web/src/main/java/org/springframework/security/web/authentication/preauth/AbstractPreAuthenticatedProcessingFilter.java +++ b/web/src/main/java/org/springframework/security/web/authentication/preauth/AbstractPreAuthenticatedProcessingFilter.java @@ -204,6 +204,12 @@ public abstract class AbstractPreAuthenticatedProcessingFilter extends GenericFi principal, credentials); authenticationRequest.setDetails(this.authenticationDetailsSource.buildDetails(request)); Authentication authenticationResult = this.authenticationManager.authenticate(authenticationRequest); + Authentication current = this.securityContextHolderStrategy.getContext().getAuthentication(); + if (current != null && current.isAuthenticated()) { + authenticationResult = authenticationResult.toBuilder() + .authorities((a) -> a.addAll(current.getAuthorities())) + .build(); + } successfulAuthentication(request, response, authenticationResult); } catch (AuthenticationException ex) { diff --git a/web/src/main/java/org/springframework/security/web/authentication/www/BasicAuthenticationFilter.java b/web/src/main/java/org/springframework/security/web/authentication/www/BasicAuthenticationFilter.java index 98fdd58da1..91f40b01b7 100644 --- a/web/src/main/java/org/springframework/security/web/authentication/www/BasicAuthenticationFilter.java +++ b/web/src/main/java/org/springframework/security/web/authentication/www/BasicAuthenticationFilter.java @@ -186,6 +186,10 @@ public class BasicAuthenticationFilter extends OncePerRequestFilter { this.logger.trace(LogMessage.format("Found username '%s' in Basic Authorization header", username)); if (authenticationIsRequired(username)) { Authentication authResult = this.authenticationManager.authenticate(authRequest); + Authentication current = this.securityContextHolderStrategy.getContext().getAuthentication(); + if (current != null && current.isAuthenticated()) { + authResult = authResult.toBuilder().authorities((a) -> a.addAll(current.getAuthorities())).build(); + } SecurityContext context = this.securityContextHolderStrategy.createEmptyContext(); context.setAuthentication(authResult); this.securityContextHolderStrategy.setContext(context); diff --git a/web/src/main/java/org/springframework/security/web/server/authentication/AuthenticationWebFilter.java b/web/src/main/java/org/springframework/security/web/server/authentication/AuthenticationWebFilter.java index 3bbbdb108c..2f83629181 100644 --- a/web/src/main/java/org/springframework/security/web/server/authentication/AuthenticationWebFilter.java +++ b/web/src/main/java/org/springframework/security/web/server/authentication/AuthenticationWebFilter.java @@ -122,12 +122,26 @@ public class AuthenticationWebFilter implements WebFilter { .flatMap((authenticationManager) -> authenticationManager.authenticate(token)) .switchIfEmpty(Mono .defer(() -> Mono.error(new IllegalStateException("No provider found for " + token.getClass())))) + .flatMap(this::applyCurrentAuthenication) .flatMap( (authentication) -> onAuthenticationSuccess(authentication, new WebFilterExchange(exchange, chain))) .doOnError(AuthenticationException.class, (ex) -> logger.debug(LogMessage.format("Authentication failed: %s", ex.getMessage()), ex)); } + private Mono applyCurrentAuthenication(Authentication result) { + return ReactiveSecurityContextHolder.getContext().map((context) -> { + Authentication current = context.getAuthentication(); + if (current == null) { + return result; + } + if (!current.isAuthenticated()) { + return result; + } + return result.toBuilder().authorities((a) -> a.addAll(current.getAuthorities())).build(); + }).switchIfEmpty(Mono.just(result)); + } + protected Mono onAuthenticationSuccess(Authentication authentication, WebFilterExchange webFilterExchange) { ServerWebExchange exchange = webFilterExchange.getExchange(); SecurityContextImpl securityContext = new SecurityContextImpl(); diff --git a/web/src/test/java/org/springframework/security/web/authentication/AuthenticationFilterTests.java b/web/src/test/java/org/springframework/security/web/authentication/AuthenticationFilterTests.java index 3851311bee..aaf17d62c9 100644 --- a/web/src/test/java/org/springframework/security/web/authentication/AuthenticationFilterTests.java +++ b/web/src/test/java/org/springframework/security/web/authentication/AuthenticationFilterTests.java @@ -144,6 +144,7 @@ public class AuthenticationFilterTests { this.authenticationConverter); SecurityContextHolderStrategy strategy = mock(SecurityContextHolderStrategy.class); given(strategy.createEmptyContext()).willReturn(new SecurityContextImpl()); + given(strategy.getContext()).willReturn(new SecurityContextImpl()); filter.setSecurityContextHolderStrategy(strategy); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/"); MockHttpServletResponse response = new MockHttpServletResponse(); From 4744752a1b777348e017159f2efdfaa467e373fe Mon Sep 17 00:00:00 2001 From: Josh Cummings <3627351+jzheaux@users.noreply.github.com> Date: Fri, 29 Aug 2025 14:57:01 -0600 Subject: [PATCH 06/14] Add Internal Authentication Implementations This commit allows a default implementation of Authentication.Builder that performs the builder operations. In this way, authorities and other previous authentication material can still be effectively be propagated in the event a custom authentication does not implement the method. Issue gh-17861 --- .../security/core/Authentication.java | 4 +- .../core/NoopAuthenticationBuilder.java | 69 --------- .../security/core/SimpleAuthentication.java | 143 ++++++++++++++++++ 3 files changed, 145 insertions(+), 71 deletions(-) delete mode 100644 core/src/main/java/org/springframework/security/core/NoopAuthenticationBuilder.java create mode 100644 core/src/main/java/org/springframework/security/core/SimpleAuthentication.java 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 4531842aab..6a46224dc8 100644 --- a/core/src/main/java/org/springframework/security/core/Authentication.java +++ b/core/src/main/java/org/springframework/security/core/Authentication.java @@ -65,7 +65,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.Saml2AssertAu + * token has not been authenticated. Never null. */ Collection getAuthorities(); @@ -144,7 +144,7 @@ public interface Authentication extends Principal, Serializable { * @since 7.0 */ default Builder toBuilder() { - return new NoopAuthenticationBuilder(this); + return new SimpleAuthentication.Builder(this); } /** diff --git a/core/src/main/java/org/springframework/security/core/NoopAuthenticationBuilder.java b/core/src/main/java/org/springframework/security/core/NoopAuthenticationBuilder.java deleted file mode 100644 index 982630c8e4..0000000000 --- a/core/src/main/java/org/springframework/security/core/NoopAuthenticationBuilder.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright 2004-present the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.security.core; - -import java.util.Collection; -import java.util.function.Consumer; - -import org.jspecify.annotations.Nullable; - -/** - * An adapter implementation of {@link Authentication.Builder} that provides a no-op - * implementation for the principal, credentials, and authorities - * - * @author Josh Cummings - * @since 7.0 - */ -class NoopAuthenticationBuilder implements Authentication.Builder { - - private Authentication original; - - NoopAuthenticationBuilder(Authentication authentication) { - this.original = authentication; - } - - @Override - public NoopAuthenticationBuilder authenticated(boolean authenticated) { - return this; - } - - @Override - 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> authorities) { - return this; - } - - @Override - public Authentication build() { - return this.original; - } - -} diff --git a/core/src/main/java/org/springframework/security/core/SimpleAuthentication.java b/core/src/main/java/org/springframework/security/core/SimpleAuthentication.java new file mode 100644 index 0000000000..ac301a9ff9 --- /dev/null +++ b/core/src/main/java/org/springframework/security/core/SimpleAuthentication.java @@ -0,0 +1,143 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.core; + +import java.io.Serial; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.function.Consumer; + +import org.jspecify.annotations.Nullable; + +@Transient +final class SimpleAuthentication implements Authentication { + + @Serial + private static final long serialVersionUID = 3194696462184782814L; + + private final @Nullable Object principal; + + private final @Nullable Object credentials; + + private final Collection authorities; + + private final @Nullable Object details; + + private final boolean authenticated; + + private SimpleAuthentication(Builder builder) { + this.principal = builder.principal; + this.credentials = builder.credentials; + this.authorities = builder.authorities; + this.details = builder.details; + this.authenticated = builder.authenticated; + } + + @Override + public Collection getAuthorities() { + return this.authorities; + } + + @Override + public @Nullable Object getCredentials() { + return this.credentials; + } + + @Override + public @Nullable Object getDetails() { + return this.details; + } + + @Override + public @Nullable Object getPrincipal() { + return this.principal; + } + + @Override + public boolean isAuthenticated() { + return this.authenticated; + } + + @Override + public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException { + throw new IllegalArgumentException( + "Instead of calling this setter, please call toBuilder to create a new instance"); + } + + @Override + public String getName() { + return (this.principal == null) ? "" : this.principal.toString(); + } + + static final class Builder implements Authentication.Builder { + + private final Collection authorities = new LinkedHashSet<>(); + + private @Nullable Object principal; + + private @Nullable Object credentials; + + private @Nullable Object details; + + private boolean authenticated; + + Builder(Authentication authentication) { + this.authorities.addAll(authentication.getAuthorities()); + this.principal = authentication.getPrincipal(); + this.credentials = authentication.getCredentials(); + this.details = authentication.getDetails(); + this.authenticated = authentication.isAuthenticated(); + } + + @Override + public Builder authorities(Consumer> authorities) { + authorities.accept(this.authorities); + return this; + } + + @Override + public Builder details(@Nullable Object details) { + this.details = details; + return this; + } + + @Override + public Builder principal(@Nullable Object principal) { + this.principal = principal; + return this; + } + + @Override + public Builder credentials(@Nullable Object credentials) { + this.credentials = credentials; + return this; + } + + @Override + public Builder authenticated(boolean authenticated) { + this.authenticated = authenticated; + return this; + } + + @Override + public Authentication build() { + return new SimpleAuthentication(this); + } + + } + +} From dd50dc0c40329fb7e58aa51ee0a5844e9feed6ef Mon Sep 17 00:00:00 2001 From: Josh Cummings <3627351+jzheaux@users.noreply.github.com> Date: Tue, 9 Sep 2025 09:03:57 -0600 Subject: [PATCH 07/14] Remove Generic Typing From Authentication.Builder It would be better to introduce parameter types for principal and credentials into Authentication.Builder at the same time as doing so for Authentication Issue gh-17861 --- .../cas/authentication/CasAuthenticationToken.java | 2 +- .../CasServiceTicketAuthenticationToken.java | 8 ++++---- .../authentication/AbstractAuthenticationToken.java | 6 +++--- .../RememberMeAuthenticationToken.java | 2 +- .../authentication/TestingAuthenticationToken.java | 2 +- .../UsernamePasswordAuthenticationToken.java | 2 +- .../ott/OneTimeTokenAuthentication.java | 2 +- .../security/core/Authentication.java | 8 ++++---- .../security/core/SimpleAuthentication.java | 10 +++++++++- .../AbstractAuthenticationBuilderTests.java | 2 +- .../authentication/OAuth2AuthenticationToken.java | 8 ++++---- .../AbstractOAuth2TokenAuthenticationToken.java | 2 +- .../authentication/Saml2AssertionAuthentication.java | 12 ++++++------ .../service/authentication/Saml2Authentication.java | 4 ++-- .../Saml2AssertionAuthenticationTests.java | 2 +- .../preauth/PreAuthenticatedAuthenticationToken.java | 2 +- .../authentication/WebAuthnAuthentication.java | 11 ++++++----- 17 files changed, 47 insertions(+), 38 deletions(-) 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 68761fccf4..17eb48e416 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 @@ -187,7 +187,7 @@ public class CasAuthenticationToken extends AbstractAuthenticationToken implemen * * @since 7.0 */ - public static class Builder> extends AbstractAuthenticationBuilder { + public static class Builder> extends AbstractAuthenticationBuilder { private Integer keyHash; diff --git a/cas/src/main/java/org/springframework/security/cas/authentication/CasServiceTicketAuthenticationToken.java b/cas/src/main/java/org/springframework/security/cas/authentication/CasServiceTicketAuthenticationToken.java index 4d791adccc..27a9996045 100644 --- a/cas/src/main/java/org/springframework/security/cas/authentication/CasServiceTicketAuthenticationToken.java +++ b/cas/src/main/java/org/springframework/security/cas/authentication/CasServiceTicketAuthenticationToken.java @@ -126,7 +126,7 @@ public class CasServiceTicketAuthenticationToken extends AbstractAuthenticationT * * @since 7.0 */ - public static class Builder> extends AbstractAuthenticationBuilder { + public static class Builder> extends AbstractAuthenticationBuilder { private String principal; @@ -139,9 +139,9 @@ public class CasServiceTicketAuthenticationToken extends AbstractAuthenticationT } @Override - public B principal(@Nullable String principal) { - Assert.notNull(principal, "principal cannot be null"); - this.principal = principal; + public B principal(@Nullable Object principal) { + Assert.isInstanceOf(String.class, principal, "principal must be of type String"); + this.principal = (String) principal; return (B) this; } 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 97fe8cd61d..8d3eb1e759 100644 --- a/core/src/main/java/org/springframework/security/authentication/AbstractAuthenticationToken.java +++ b/core/src/main/java/org/springframework/security/authentication/AbstractAuthenticationToken.java @@ -69,7 +69,7 @@ public abstract class AbstractAuthenticationToken implements Authentication, Cre this.authorities = Collections.unmodifiableList(new ArrayList<>(authorities)); } - protected AbstractAuthenticationToken(AbstractAuthenticationBuilder builder) { + protected AbstractAuthenticationToken(AbstractAuthenticationBuilder builder) { this(builder.authorities); this.authenticated = builder.authenticated; this.details = builder.details; @@ -197,8 +197,8 @@ public abstract class AbstractAuthenticationToken implements Authentication, Cre return sb.toString(); } - protected abstract static class AbstractAuthenticationBuilder> - implements Authentication.Builder { + protected abstract static class AbstractAuthenticationBuilder> + implements Authentication.Builder { protected boolean authenticated; 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 5ff410ecec..1ecb5f851f 100644 --- a/core/src/main/java/org/springframework/security/authentication/RememberMeAuthenticationToken.java +++ b/core/src/main/java/org/springframework/security/authentication/RememberMeAuthenticationToken.java @@ -126,7 +126,7 @@ public class RememberMeAuthenticationToken extends AbstractAuthenticationToken { * * @since 7.0 */ - public static class Builder> extends AbstractAuthenticationBuilder { + public static class Builder> extends AbstractAuthenticationBuilder { private Integer keyHash; 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 f001674ca4..f60bb3b1b3 100644 --- a/core/src/main/java/org/springframework/security/authentication/TestingAuthenticationToken.java +++ b/core/src/main/java/org/springframework/security/authentication/TestingAuthenticationToken.java @@ -91,7 +91,7 @@ public class TestingAuthenticationToken extends AbstractAuthenticationToken { * * @since 7.0 */ - public static class Builder> extends AbstractAuthenticationBuilder { + public static class Builder> extends AbstractAuthenticationBuilder { private Object principal; 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 4bba35a27d..024b6bb1ae 100644 --- a/core/src/main/java/org/springframework/security/authentication/UsernamePasswordAuthenticationToken.java +++ b/core/src/main/java/org/springframework/security/authentication/UsernamePasswordAuthenticationToken.java @@ -141,7 +141,7 @@ public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationT * * @since 7.0 */ - public static class Builder> extends AbstractAuthenticationBuilder { + public static class Builder> extends AbstractAuthenticationBuilder { protected @Nullable Object principal; 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 8e73adf65b..c2a8812724 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 @@ -67,7 +67,7 @@ public class OneTimeTokenAuthentication extends AbstractAuthenticationToken { /** * A builder for constructing a {@link OneTimeTokenAuthentication} instance */ - public static class Builder> extends AbstractAuthenticationBuilder { + public static class Builder> extends AbstractAuthenticationBuilder { private Object principal; 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 6a46224dc8..eea57506e9 100644 --- a/core/src/main/java/org/springframework/security/core/Authentication.java +++ b/core/src/main/java/org/springframework/security/core/Authentication.java @@ -143,7 +143,7 @@ public interface Authentication extends Principal, Serializable { * instance * @since 7.0 */ - default Builder toBuilder() { + default Builder toBuilder() { return new SimpleAuthentication.Builder(this); } @@ -153,18 +153,18 @@ public interface Authentication extends Principal, Serializable { * @author Josh Cummings * @since 7.0 */ - interface Builder> { + interface Builder> { B authorities(Consumer> authorities); - default B credentials(@Nullable C credentials) { + default B credentials(@Nullable Object credentials) { throw new UnsupportedOperationException( String.format("%s does not store credentials", this.getClass().getSimpleName())); } B details(@Nullable Object details); - B principal(@Nullable P principal); + B principal(@Nullable Object principal); B authenticated(boolean authenticated); diff --git a/core/src/main/java/org/springframework/security/core/SimpleAuthentication.java b/core/src/main/java/org/springframework/security/core/SimpleAuthentication.java index ac301a9ff9..367c01d162 100644 --- a/core/src/main/java/org/springframework/security/core/SimpleAuthentication.java +++ b/core/src/main/java/org/springframework/security/core/SimpleAuthentication.java @@ -21,6 +21,8 @@ import java.util.Collection; import java.util.LinkedHashSet; import java.util.function.Consumer; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.jspecify.annotations.Nullable; @Transient @@ -83,7 +85,9 @@ final class SimpleAuthentication implements Authentication { return (this.principal == null) ? "" : this.principal.toString(); } - static final class Builder implements Authentication.Builder { + static final class Builder implements Authentication.Builder { + + private final Log logger = LogFactory.getLog(getClass()); private final Collection authorities = new LinkedHashSet<>(); @@ -96,11 +100,15 @@ final class SimpleAuthentication implements Authentication { private boolean authenticated; Builder(Authentication authentication) { + this.logger.debug("Creating a builder which will result in exchanging an authentication of type " + + authentication.getClass() + " for " + SimpleAuthentication.class.getSimpleName() + ";" + + " consider implementing " + authentication.getClass().getSimpleName() + "#toBuilder"); this.authorities.addAll(authentication.getAuthorities()); this.principal = authentication.getPrincipal(); this.credentials = authentication.getCredentials(); this.details = authentication.getDetails(); this.authenticated = authentication.isAuthenticated(); + } @Override diff --git a/core/src/test/java/org/springframework/security/authentication/AbstractAuthenticationBuilderTests.java b/core/src/test/java/org/springframework/security/authentication/AbstractAuthenticationBuilderTests.java index 50b64ebe27..de88eaf431 100644 --- a/core/src/test/java/org/springframework/security/authentication/AbstractAuthenticationBuilderTests.java +++ b/core/src/test/java/org/springframework/security/authentication/AbstractAuthenticationBuilderTests.java @@ -40,7 +40,7 @@ class AbstractAuthenticationBuilderTests { } private static final class TestAbstractAuthenticationBuilder - extends AbstractAuthenticationBuilder { + extends AbstractAuthenticationBuilder { private TestAbstractAuthenticationBuilder(TestingAuthenticationToken token) { super(token); 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 613e16afbf..8e2796355c 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 @@ -105,7 +105,7 @@ public class OAuth2AuthenticationToken extends AbstractAuthenticationToken { * * @since 7.0 */ - public static class Builder> extends AbstractAuthenticationBuilder { + public static class Builder> extends AbstractAuthenticationBuilder { private OAuth2User principal; @@ -118,9 +118,9 @@ public class OAuth2AuthenticationToken extends AbstractAuthenticationToken { } @Override - public B principal(@Nullable OAuth2User principal) { - Assert.notNull(principal, "principal cannot be null"); - this.principal = principal; + public B principal(@Nullable Object principal) { + Assert.isInstanceOf(OAuth2User.class, principal, "principal must be of type OAuth2User"); + this.principal = (OAuth2User) principal; return (B) this; } diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/AbstractOAuth2TokenAuthenticationToken.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/AbstractOAuth2TokenAuthenticationToken.java index 68898af461..079f738460 100644 --- a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/AbstractOAuth2TokenAuthenticationToken.java +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/AbstractOAuth2TokenAuthenticationToken.java @@ -123,7 +123,7 @@ public abstract class AbstractOAuth2TokenAuthenticationToken> - extends AbstractAuthenticationBuilder { + extends AbstractAuthenticationBuilder { private Object principal; 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 e86494f9fc..22f8c64356 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 @@ -82,8 +82,7 @@ public class Saml2AssertionAuthentication extends Saml2Authentication { * * @since 7.0 */ - public static class Builder> - extends Saml2Authentication.Builder { + public static class Builder> extends Saml2Authentication.Builder { private Saml2ResponseAssertionAccessor assertion; @@ -96,10 +95,11 @@ public class Saml2AssertionAuthentication extends Saml2Authentication { } @Override - public B credentials(@Nullable Saml2ResponseAssertionAccessor credentials) { - saml2Response(credentials.getResponseValue()); - Assert.notNull(credentials, "assertion cannot be null"); - this.assertion = credentials; + public B credentials(@Nullable Object credentials) { + Assert.isInstanceOf(Saml2ResponseAssertionAccessor.class, credentials, + "credentials must be of type Saml2ResponseAssertionAccessor"); + saml2Response(((Saml2ResponseAssertionAccessor) credentials).getResponseValue()); + this.assertion = (Saml2ResponseAssertionAccessor) credentials; return (B) this; } diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2Authentication.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2Authentication.java index 2a9fe34bd9..d3e57fe3bd 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2Authentication.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2Authentication.java @@ -71,7 +71,7 @@ public class Saml2Authentication extends AbstractAuthenticationToken { setAuthenticated(true); } - Saml2Authentication(Builder builder) { + Saml2Authentication(Builder builder) { super(builder); this.principal = builder.principal; this.saml2Response = builder.saml2Response; @@ -95,7 +95,7 @@ public class Saml2Authentication extends AbstractAuthenticationToken { return getSaml2Response(); } - abstract static class Builder> extends AbstractAuthenticationBuilder { + abstract static class Builder> extends AbstractAuthenticationBuilder { private Object principal; 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 index 57b08dee02..d67ee3bc7c 100644 --- 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 @@ -32,7 +32,7 @@ class Saml2AssertionAuthenticationTests { 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"); + prototype.nameId("bob").build(), AuthorityUtils.createAuthorityList("FACTOR_TWO"), "bob"); Saml2AssertionAuthentication result = factorOne.toBuilder() .authorities((a) -> a.addAll(factorTwo.getAuthorities())) .principal(factorTwo.getPrincipal()) 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 0f994bd751..fef86d08ff 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 @@ -100,7 +100,7 @@ public class PreAuthenticatedAuthenticationToken extends AbstractAuthenticationT * * @since 7.0 */ - public static class Builder> extends AbstractAuthenticationBuilder { + public static class Builder> extends AbstractAuthenticationBuilder { private Object principal; 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 615781e2d1..6a2119dd1e 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 @@ -85,19 +85,20 @@ public class WebAuthnAuthentication extends AbstractAuthenticationToken { * * @since 7.0 */ - public static final class Builder> - extends AbstractAuthenticationBuilder { + public static final class Builder> extends AbstractAuthenticationBuilder { private PublicKeyCredentialUserEntity principal; private Builder(WebAuthnAuthentication token) { super(token); + this.principal = token.principal; } @Override - public B principal(@Nullable PublicKeyCredentialUserEntity principal) { - Assert.notNull(principal, "principal cannot be null"); - this.principal = principal; + public B principal(@Nullable Object principal) { + Assert.isInstanceOf(PublicKeyCredentialUserEntity.class, principal, + "principal must be of type PublicKeyCredentialUserEntity"); + this.principal = (PublicKeyCredentialUserEntity) principal; return (B) this; } From 18fbf889932b90f73c3f77df28abf1b433a76cda Mon Sep 17 00:00:00 2001 From: Josh Cummings <3627351+jzheaux@users.noreply.github.com> Date: Tue, 9 Sep 2025 14:05:34 -0600 Subject: [PATCH 08/14] Polish CAS Authentication Builder Issue gh-17861 --- .../CasAuthenticationToken.java | 22 +++++++++++++++---- .../CasServiceTicketAuthenticationToken.java | 3 +-- .../CasAuthenticationTokenTests.java | 2 +- 3 files changed, 20 insertions(+), 7 deletions(-) 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 17eb48e416..c05d5c5964 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 @@ -23,7 +23,6 @@ import org.apereo.cas.client.validation.Assertion; 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.core.userdetails.UserDetails; import org.springframework.util.Assert; @@ -183,7 +182,7 @@ public class CasAuthenticationToken extends AbstractAuthenticationToken implemen } /** - * A builder preserving the concrete {@link Authentication} type + * A builder of {@link CasAuthenticationToken} instances * * @since 7.0 */ @@ -208,8 +207,13 @@ public class CasAuthenticationToken extends AbstractAuthenticationToken implemen this.assertion = token.assertion; } - public B keyHash(Integer keyHash) { - this.keyHash = keyHash; + /** + * Use this key + * @param key the key to use + * @return the {@link Builder} for further configurations + */ + public B key(String key) { + this.keyHash = key.hashCode(); return (B) this; } @@ -227,11 +231,21 @@ public class CasAuthenticationToken extends AbstractAuthenticationToken implemen return (B) this; } + /** + * Use this {@link UserDetails} + * @param userDetails the {@link UserDetails} to use + * @return the {@link Builder} for further configurations + */ public B userDetails(UserDetails userDetails) { this.userDetails = userDetails; return (B) this; } + /** + * Use this {@link Assertion} + * @param assertion the {@link Assertion} to use + * @return the {@link Builder} for further configurations + */ public B assertion(Assertion assertion) { this.assertion = assertion; return (B) this; diff --git a/cas/src/main/java/org/springframework/security/cas/authentication/CasServiceTicketAuthenticationToken.java b/cas/src/main/java/org/springframework/security/cas/authentication/CasServiceTicketAuthenticationToken.java index 27a9996045..0337bafaf9 100644 --- a/cas/src/main/java/org/springframework/security/cas/authentication/CasServiceTicketAuthenticationToken.java +++ b/cas/src/main/java/org/springframework/security/cas/authentication/CasServiceTicketAuthenticationToken.java @@ -22,7 +22,6 @@ 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; @@ -122,7 +121,7 @@ public class CasServiceTicketAuthenticationToken extends AbstractAuthenticationT } /** - * A builder preserving the concrete {@link Authentication} type + * A builder of {@link CasServiceTicketAuthenticationToken} instances * * @since 7.0 */ 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 d489d00ff3..506497124d 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 @@ -167,7 +167,7 @@ public class CasAuthenticationTokenTests { AuthorityUtils.createAuthorityList("FACTOR_TWO"), PasswordEncodedUser.admin(), assertionTwo); CasAuthenticationToken authentication = factorOne.toBuilder() .authorities((a) -> a.addAll(factorTwo.getAuthorities())) - .keyHash(factorTwo.getKeyHash()) + .key("yek") .principal(factorTwo.getPrincipal()) .credentials(factorTwo.getCredentials()) .userDetails(factorTwo.getUserDetails()) From c66a0283322dc5a967639a54caccd53dca40f39c Mon Sep 17 00:00:00 2001 From: Josh Cummings <3627351+jzheaux@users.noreply.github.com> Date: Tue, 9 Sep 2025 14:06:03 -0600 Subject: [PATCH 09/14] Polish Core Authentication Builders Issue gh-17861 --- .../AbstractAuthenticationToken.java | 14 +++- .../RememberMeAuthenticationToken.java | 16 +++-- .../TestingAuthenticationToken.java | 3 +- .../UsernamePasswordAuthenticationToken.java | 7 +- .../jaas/JaasAuthenticationToken.java | 7 +- .../ott/OneTimeTokenAuthentication.java | 6 +- .../security/core/Authentication.java | 69 ++++++++++++++++++- .../AbstractAuthenticationBuilderTests.java | 11 +-- .../RememberMeAuthenticationTokenTests.java | 19 +++++ 9 files changed, 120 insertions(+), 32 deletions(-) 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 8d3eb1e759..18307f8b64 100644 --- a/core/src/main/java/org/springframework/security/authentication/AbstractAuthenticationToken.java +++ b/core/src/main/java/org/springframework/security/authentication/AbstractAuthenticationToken.java @@ -197,14 +197,22 @@ public abstract class AbstractAuthenticationToken implements Authentication, Cre return sb.toString(); } + /** + * A common abstract implementation of {@link Authentication.Builder}. It implements + * the builder methods that correspond to the {@link Authentication} methods that + * {@link AbstractAuthenticationToken} implements + * + * @param + * @since 7.0 + */ protected abstract static class AbstractAuthenticationBuilder> implements Authentication.Builder { - protected boolean authenticated; + private boolean authenticated; - protected @Nullable Object details; + private @Nullable Object details; - protected final Collection authorities; + private final Collection authorities; protected AbstractAuthenticationBuilder(AbstractAuthenticationToken token) { this.authorities = new LinkedHashSet<>(token.getAuthorities()); 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 1ecb5f851f..81d7edeafa 100644 --- a/core/src/main/java/org/springframework/security/authentication/RememberMeAuthenticationToken.java +++ b/core/src/main/java/org/springframework/security/authentication/RememberMeAuthenticationToken.java @@ -20,7 +20,6 @@ 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; @@ -99,8 +98,8 @@ public class RememberMeAuthenticationToken extends AbstractAuthenticationToken { } @Override - public Builder toBuilder() { - return new Builder(this); + public Builder toBuilder() { + return new Builder<>(this); } @Override @@ -122,7 +121,7 @@ public class RememberMeAuthenticationToken extends AbstractAuthenticationToken { } /** - * A builder preserving the concrete {@link Authentication} type + * A builder of {@link RememberMeAuthenticationToken} instances * * @since 7.0 */ @@ -145,8 +144,13 @@ public class RememberMeAuthenticationToken extends AbstractAuthenticationToken { return (B) this; } - public B keyHash(int keyHash) { - this.keyHash = keyHash; + /** + * Use this key + * @param key the key to use + * @return the {@link Builder} for further configurations + */ + public B key(String key) { + this.keyHash = key.hashCode(); return (B) this; } 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 f60bb3b1b3..70778db2a4 100644 --- a/core/src/main/java/org/springframework/security/authentication/TestingAuthenticationToken.java +++ b/core/src/main/java/org/springframework/security/authentication/TestingAuthenticationToken.java @@ -21,7 +21,6 @@ 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; @@ -87,7 +86,7 @@ public class TestingAuthenticationToken extends AbstractAuthenticationToken { } /** - * A builder preserving the concrete {@link Authentication} type + * A builder of {@link TestingAuthenticationToken} instances * * @since 7.0 */ 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 024b6bb1ae..c63e5dfb34 100644 --- a/core/src/main/java/org/springframework/security/authentication/UsernamePasswordAuthenticationToken.java +++ b/core/src/main/java/org/springframework/security/authentication/UsernamePasswordAuthenticationToken.java @@ -20,7 +20,6 @@ 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; @@ -137,15 +136,15 @@ public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationT } /** - * A builder preserving the concrete {@link Authentication} type + * A builder of {@link UsernamePasswordAuthenticationToken} instances * * @since 7.0 */ public static class Builder> extends AbstractAuthenticationBuilder { - protected @Nullable Object principal; + private @Nullable Object principal; - protected @Nullable Object credentials; + private @Nullable Object credentials; protected Builder(UsernamePasswordAuthenticationToken token) { super(token); 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 a1dd08a37b..9e1200a58f 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 @@ -23,9 +23,7 @@ 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 @@ -65,7 +63,7 @@ public class JaasAuthenticationToken extends UsernamePasswordAuthenticationToken } /** - * A builder preserving the concrete {@link Authentication} type + * A builder of {@link JaasAuthenticationToken} instances * * @since 7.0 */ @@ -81,7 +79,7 @@ public class JaasAuthenticationToken extends UsernamePasswordAuthenticationToken /** * Use this {@link LoginContext} * @param loginContext the {@link LoginContext} to use - * @return the {@link Builder} for further configuration + * @return the {@link Builder} for further configurations */ public B loginContext(LoginContext loginContext) { this.loginContext = loginContext; @@ -90,7 +88,6 @@ public class JaasAuthenticationToken extends UsernamePasswordAuthenticationToken @Override public JaasAuthenticationToken build() { - Assert.notNull(this.principal, "principal cannot be null"); return new JaasAuthenticationToken(this); } 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 c2a8812724..2b961f3c32 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 @@ -65,7 +65,9 @@ public class OneTimeTokenAuthentication extends AbstractAuthenticationToken { } /** - * A builder for constructing a {@link OneTimeTokenAuthentication} instance + * A builder of {@link OneTimeTokenAuthentication} instances + * + * @since 7.0 */ public static class Builder> extends AbstractAuthenticationBuilder { @@ -77,7 +79,7 @@ public class OneTimeTokenAuthentication extends AbstractAuthenticationToken { } /** - * Use this principal + * Use this principal. * @return the {@link Builder} for further configuration */ @Override 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 eea57506e9..af581bc9ba 100644 --- a/core/src/main/java/org/springframework/security/core/Authentication.java +++ b/core/src/main/java/org/springframework/security/core/Authentication.java @@ -138,7 +138,19 @@ public interface Authentication extends Principal, Serializable { void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException; /** - * Return an {@link Builder} based on this instance + * Return an {@link Builder} based on this instance. By default, returns a builder + * that builds a {@link SimpleAuthentication}. + *

+ * Although a {@code default} method, all {@link Authentication} implementations + * should implement this. The reason is to ensure that the {@link Authentication} type + * is preserved when {@link Builder#build} is invoked. This is especially important in + * the event that your authentication implementation contains custom fields. + *

+ *

+ * This isn't strictly necessary since it is recommended that applications code to the + * {@link Authentication} interface and that custom information is often contained in + * the {@link Authentication#getPrincipal} value. + *

* @return an {@link Builder} for building a new {@link Authentication} based on this * instance * @since 7.0 @@ -155,17 +167,72 @@ public interface Authentication extends Principal, Serializable { */ interface Builder> { + /** + * Mutate the authorities with this {@link Consumer}. + *

+ * Note that since a non-empty set of authorities implies an + * {@link Authentication} is authenticated, this method also marks the + * authentication as {@link #authenticated} by default. + *

+ * @param authorities a consumer that receives the full set of authorities + * @return the {@link Builder} for additional configuration + * @see Authentication#getAuthorities + */ B authorities(Consumer> authorities); + /** + * Use this credential. + *

+ * Note that since some credentials are insecure to store, this method is + * implemented as unsupported by default. Only implement or use this method if you + * support secure storage of the credential or if your implementation also + * implements {@link CredentialsContainer} and the credentials are thereby erased. + *

+ * @param credentials the credentials to use + * @return the {@link Builder} for additional configuration + * @see Authentication#getCredentials + */ default B credentials(@Nullable Object credentials) { throw new UnsupportedOperationException( String.format("%s does not store credentials", this.getClass().getSimpleName())); } + /** + * Use this details object. + *

+ * Implementations may choose to use these {@code details} in combination with any + * principal from the pre-existing {@link Authentication} instance. + *

+ * @param details the details to use + * @return the {@link Builder} for additional configuration + * @see Authentication#getDetails + */ B details(@Nullable Object details); + /** + * Use this principal. + *

+ * Note that in many cases, the principal is strongly-typed. Implementations may + * choose to do a type check and are not necessarily expected to allow any object + * as a principal. + *

+ *

+ * Implementations may choose to use this {@code principal} in combination with + * any principal from the pre-existing {@link Authentication} instance. + *

+ * @param principal the principal to use + * @return the {@link Builder} for additional configuration + * @see Authentication#getPrincipal + */ B principal(@Nullable Object principal); + /** + * Mark this authentication as authenticated or not + * @param authenticated whether this is an authenticated {@link Authentication} + * instance + * @return the {@link Builder} for additional configuration + * @see Authentication#isAuthenticated + */ B authenticated(boolean authenticated); /** diff --git a/core/src/test/java/org/springframework/security/authentication/AbstractAuthenticationBuilderTests.java b/core/src/test/java/org/springframework/security/authentication/AbstractAuthenticationBuilderTests.java index de88eaf431..6bd66b7166 100644 --- a/core/src/test/java/org/springframework/security/authentication/AbstractAuthenticationBuilderTests.java +++ b/core/src/test/java/org/springframework/security/authentication/AbstractAuthenticationBuilderTests.java @@ -18,10 +18,8 @@ package org.springframework.security.authentication; 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.authority.AuthorityUtils; @@ -40,20 +38,15 @@ class AbstractAuthenticationBuilderTests { } private static final class TestAbstractAuthenticationBuilder - extends AbstractAuthenticationBuilder { + extends TestingAuthenticationToken.Builder { private TestAbstractAuthenticationBuilder(TestingAuthenticationToken token) { super(token); } - @Override - public TestAbstractAuthenticationBuilder principal(@Nullable Object principal) { - return this; - } - @Override public TestingAuthenticationToken build() { - return new TestingAuthenticationToken("user", "password", this.authorities); + return new TestingAuthenticationToken(this); } } diff --git a/core/src/test/java/org/springframework/security/authentication/rememberme/RememberMeAuthenticationTokenTests.java b/core/src/test/java/org/springframework/security/authentication/rememberme/RememberMeAuthenticationTokenTests.java index 9e8f123445..d40954b950 100644 --- a/core/src/test/java/org/springframework/security/authentication/rememberme/RememberMeAuthenticationTokenTests.java +++ b/core/src/test/java/org/springframework/security/authentication/rememberme/RememberMeAuthenticationTokenTests.java @@ -18,6 +18,7 @@ package org.springframework.security.authentication.rememberme; import java.util.Arrays; import java.util.List; +import java.util.Set; import org.junit.jupiter.api.Test; @@ -25,6 +26,7 @@ import org.springframework.security.authentication.RememberMeAuthenticationToken import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.core.userdetails.PasswordEncodedUser; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; @@ -96,4 +98,21 @@ public class RememberMeAuthenticationTokenTests { assertThat(!token.isAuthenticated()).isTrue(); } + @Test + public void toBuilderWhenApplyThenCopies() { + RememberMeAuthenticationToken factorOne = new RememberMeAuthenticationToken("key", PasswordEncodedUser.user(), + AuthorityUtils.createAuthorityList("FACTOR_ONE")); + RememberMeAuthenticationToken factorTwo = new RememberMeAuthenticationToken("yek", PasswordEncodedUser.admin(), + AuthorityUtils.createAuthorityList("FACTOR_TWO")); + RememberMeAuthenticationToken authentication = factorOne.toBuilder() + .authorities((a) -> a.addAll(factorTwo.getAuthorities())) + .key("yek") + .principal(factorTwo.getPrincipal()) + .build(); + Set authorities = AuthorityUtils.authorityListToSet(authentication.getAuthorities()); + assertThat(authentication.getKeyHash()).isEqualTo(factorTwo.getKeyHash()); + assertThat(authentication.getPrincipal()).isEqualTo(factorTwo.getPrincipal()); + assertThat(authorities).containsExactlyInAnyOrder("FACTOR_ONE", "FACTOR_TWO"); + } + } From 69ee8d9aecf841e861c0c8419dcf7fa815196865 Mon Sep 17 00:00:00 2001 From: Josh Cummings <3627351+jzheaux@users.noreply.github.com> Date: Tue, 9 Sep 2025 14:06:15 -0600 Subject: [PATCH 10/14] Polish OAuth 2.0 Authentication Builders Issue gh-17861 --- .../OAuth2AuthenticationToken.java | 10 ++++- ...bstractOAuth2TokenAuthenticationToken.java | 10 ++++- .../BearerTokenAuthentication.java | 29 +++++++++++- .../JwtAuthenticationToken.java | 44 ++++++++++++++++++- 4 files changed, 87 insertions(+), 6 deletions(-) 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 8e2796355c..44e6875dc1 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 @@ -101,7 +101,7 @@ public class OAuth2AuthenticationToken extends AbstractAuthenticationToken { } /** - * A builder preserving the concrete {@link Authentication} type + * A builder of {@link OAuth2AuthenticationToken} instances * * @since 7.0 */ @@ -124,6 +124,14 @@ public class OAuth2AuthenticationToken extends AbstractAuthenticationToken { return (B) this; } + /** + * Use this + * {@link org.springframework.security.oauth2.client.registration.ClientRegistration} + * {@code registrationId}. + * @param authorizedClientRegistrationId the registration id to use + * @return the {@link Builder} for further configurations + * @see OAuth2AuthenticationToken#getAuthorizedClientRegistrationId + */ public B authorizedClientRegistrationId(String authorizedClientRegistrationId) { this.authorizedClientRegistrationId = authorizedClientRegistrationId; return (B) this; diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/AbstractOAuth2TokenAuthenticationToken.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/AbstractOAuth2TokenAuthenticationToken.java index 079f738460..86ae387e00 100644 --- a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/AbstractOAuth2TokenAuthenticationToken.java +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/AbstractOAuth2TokenAuthenticationToken.java @@ -118,8 +118,9 @@ public abstract class AbstractOAuth2TokenAuthenticationToken getTokenAttributes(); /** - * A builder preserving the concrete {@link Authentication} type + * A builder for {@link AbstractOAuth2TokenAuthenticationToken} implementations * + * @param * @since 7.0 */ public abstract static class AbstractOAuth2TokenAuthenticationBuilder> @@ -152,8 +153,13 @@ public abstract class AbstractOAuth2TokenAuthenticationToken> extends AbstractOAuth2TokenAuthenticationBuilder { @@ -109,6 +113,44 @@ public class JwtAuthenticationToken extends AbstractOAuth2TokenAuthenticationTok this.name = token.getName(); } + /** + * A synonym for {@link #token(Jwt)} + * @return the {@link Builder} for further configurations + */ + @Override + public B principal(@Nullable Object principal) { + Assert.isInstanceOf(Jwt.class, principal, "principal must be of type Jwt"); + return token((Jwt) principal); + } + + /** + * A synonym for {@link #token(Jwt)} + * @return the {@link Builder} for further configurations + */ + @Override + public B credentials(@Nullable Object credentials) { + Assert.isInstanceOf(Jwt.class, credentials, "credentials must be of type Jwt"); + return token((Jwt) credentials); + } + + /** + * Use this {@code token} as the token, principal, and credentials. Also sets the + * {@code name} to {@link Jwt#getSubject}. + * @param token the token to use + * @return the {@link Builder} for further configurations + */ + @Override + public B token(Jwt token) { + super.principal(token); + super.credentials(token); + return super.token(token).name(token.getSubject()); + } + + /** + * The name to use. + * @param name the name to use + * @return the {@link Builder} for further configurations + */ public B name(String name) { this.name = name; return (B) this; From e7281a71c6bfa5795d11cb0e008f963e2f480444 Mon Sep 17 00:00:00 2001 From: Josh Cummings <3627351+jzheaux@users.noreply.github.com> Date: Tue, 9 Sep 2025 14:06:27 -0600 Subject: [PATCH 11/14] Polish SAML 2.0 Authentication Builder Issue gh-17861 --- .../Saml2AssertionAuthentication.java | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) 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 22f8c64356..faf3d13db4 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 @@ -21,8 +21,8 @@ import java.util.Collection; import org.jspecify.annotations.Nullable; -import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; import org.springframework.util.Assert; /** @@ -78,7 +78,7 @@ public class Saml2AssertionAuthentication extends Saml2Authentication { } /** - * A builder preserving the concrete {@link Authentication} type + * A builder of {@link Saml2AssertionAuthentication} instances * * @since 7.0 */ @@ -94,6 +94,12 @@ public class Saml2AssertionAuthentication extends Saml2Authentication { this.relyingPartyRegistrationId = token.relyingPartyRegistrationId; } + /** + * Use these credentials. They must be of type + * {@link Saml2ResponseAssertionAccessor}. + * @param credentials the credentials to use + * @return the {@link Builder} for further configurations + */ @Override public B credentials(@Nullable Object credentials) { Assert.isInstanceOf(Saml2ResponseAssertionAccessor.class, credentials, @@ -103,6 +109,12 @@ public class Saml2AssertionAuthentication extends Saml2Authentication { return (B) this; } + /** + * Use this registration id + * @param relyingPartyRegistrationId the + * {@link RelyingPartyRegistration#getRegistrationId} to use + * @return the {@link Builder} for further configurations + */ public B relyingPartyRegistrationId(String relyingPartyRegistrationId) { this.relyingPartyRegistrationId = relyingPartyRegistrationId; return (B) this; From e97a335edced3c0fdebae0b0e9f27d3050a19f3a Mon Sep 17 00:00:00 2001 From: Josh Cummings <3627351+jzheaux@users.noreply.github.com> Date: Tue, 9 Sep 2025 14:06:41 -0600 Subject: [PATCH 12/14] Polish Web Authentication Builders Issue gh-17861 --- .../preauth/PreAuthenticatedAuthenticationToken.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 fef86d08ff..e7d75c3206 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,7 +21,6 @@ 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; @@ -96,7 +95,7 @@ public class PreAuthenticatedAuthenticationToken extends AbstractAuthenticationT } /** - * A builder preserving the concrete {@link Authentication} type + * A builder of {@link PreAuthenticatedAuthenticationToken} instances * * @since 7.0 */ From 2476875990258daa6d3c88f789f3ba889be6674c Mon Sep 17 00:00:00 2001 From: Josh Cummings <3627351+jzheaux@users.noreply.github.com> Date: Tue, 9 Sep 2025 14:46:47 -0600 Subject: [PATCH 13/14] Polish WebAuthn Authentication Builder Issue gh-17861 --- .../webauthn/authentication/WebAuthnAuthentication.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) 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 6a2119dd1e..bb5b462e92 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,7 +22,6 @@ 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; @@ -81,7 +80,7 @@ public class WebAuthnAuthentication extends AbstractAuthenticationToken { } /** - * A builder preserving the concrete {@link Authentication} type + * A builder of {@link WebAuthnAuthentication} instances * * @since 7.0 */ @@ -94,6 +93,11 @@ public class WebAuthnAuthentication extends AbstractAuthenticationToken { this.principal = token.principal; } + /** + * Use this principal. It must be of type {@link PublicKeyCredentialUserEntity} + * @param principal the principal to use + * @return the {@link Builder} for further configurations + */ @Override public B principal(@Nullable Object principal) { Assert.isInstanceOf(PublicKeyCredentialUserEntity.class, principal, From b09afb34cc721d3c74f690ce5f0faf3357e9c42d Mon Sep 17 00:00:00 2001 From: Josh Cummings <3627351+jzheaux@users.noreply.github.com> Date: Tue, 9 Sep 2025 14:47:34 -0600 Subject: [PATCH 14/14] Document Authentication.Builder The commit documents the new Authentication Builder interface and its usage in the security filter chain. Closes gh-17861 Closes gh-17862 --- .../servlet/authentication/architecture.adoc | 9 +++- .../authentication/passwords/basic.adoc | 2 + .../servlet/oauth2/resource-server/index.adoc | 2 + docs/modules/ROOT/pages/whats-new.adoc | 1 + .../CopyAuthoritiesTests.java | 41 +++++++++++++++++++ .../CopyAuthoritiesTests.kt | 39 ++++++++++++++++++ 6 files changed, 92 insertions(+), 2 deletions(-) create mode 100644 docs/src/test/java/org/springframework/security/docs/servlet/authentication/servletauthenticationauthentication/CopyAuthoritiesTests.java create mode 100644 docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/servletauthenticationauthentication/CopyAuthoritiesTests.kt diff --git a/docs/modules/ROOT/pages/servlet/authentication/architecture.adoc b/docs/modules/ROOT/pages/servlet/authentication/architecture.adoc index 7d900f9476..89abe49407 100644 --- a/docs/modules/ROOT/pages/servlet/authentication/architecture.adoc +++ b/docs/modules/ROOT/pages/servlet/authentication/architecture.adoc @@ -140,6 +140,11 @@ In many cases, this is cleared after the user is authenticated, to ensure that i * `authorities`: The <> instances are high-level permissions the user is granted. Two examples are roles and scopes. +It is also equipped with a `Builder` that allows you to mutate an existing `Authentication` instance and potentially merge it with another. +This is useful in scenarios like taking the authorities from one authentication step, like form login, and applying them to another, like one-time-token login, like so: + +include-code::./CopyAuthoritiesTests[tag=springSecurity,indent=0] + [[servlet-authentication-granted-authority]] == GrantedAuthority javadoc:org.springframework.security.core.GrantedAuthority[] instances are high-level permissions that the user is granted. @@ -231,8 +236,6 @@ In other cases, a client makes an unauthenticated request to a resource that the In this case, an implementation of `AuthenticationEntryPoint` is used to request credentials from the client. The `AuthenticationEntryPoint` implementation might perform a xref:servlet/authentication/passwords/form.adoc#servlet-authentication-form[redirect to a log in page], respond with an xref:servlet/authentication/passwords/basic.adoc#servlet-authentication-basic[WWW-Authenticate] header, or take other action. - - // FIXME: authenticationsuccesshandler // FIXME: authenticationfailurehandler @@ -266,6 +269,8 @@ image:{icondir}/number_4.png[] If authentication is successful, then __Success__ * `SessionAuthenticationStrategy` is notified of a new login. See the javadoc:org.springframework.security.web.authentication.session.SessionAuthenticationStrategy[] interface. +* Any already-authenticated `Authentication` in the <> is loaded and its +authorities are added to the returned <>. * The <> is set on the <>. Later, if you need to save the `SecurityContext` so that it can be automatically set on future requests, `SecurityContextRepository#saveContext` must be explicitly invoked. See the javadoc:org.springframework.security.web.context.SecurityContextHolderFilter[] class. diff --git a/docs/modules/ROOT/pages/servlet/authentication/passwords/basic.adoc b/docs/modules/ROOT/pages/servlet/authentication/passwords/basic.adoc index 77de88f55e..4e24d44e57 100644 --- a/docs/modules/ROOT/pages/servlet/authentication/passwords/basic.adoc +++ b/docs/modules/ROOT/pages/servlet/authentication/passwords/basic.adoc @@ -56,6 +56,8 @@ See the javadoc:org.springframework.security.web.AuthenticationEntryPoint[] inte image:{icondir}/number_4.png[] If authentication is successful, then __Success__. +* Any already-authenticated `Authentication` in the xref:servlet/authentication/architecture.adoc#servlet-authentication-securitycontextholder[`SecurityContextHolder`] is loaded and its +authorities are added to the returned xref:servlet/authentication/architecture.adoc#servlet-authentication-authentication[`Authentication`]. . The xref:servlet/authentication/architecture.adoc#servlet-authentication-authentication[Authentication] is set on the xref:servlet/authentication/architecture.adoc#servlet-authentication-securitycontextholder[SecurityContextHolder]. . `RememberMeServices.loginSuccess` is invoked. If remember me is not configured, this is a no-op. diff --git a/docs/modules/ROOT/pages/servlet/oauth2/resource-server/index.adoc b/docs/modules/ROOT/pages/servlet/oauth2/resource-server/index.adoc index e30e55dca1..092a520073 100644 --- a/docs/modules/ROOT/pages/servlet/oauth2/resource-server/index.adoc +++ b/docs/modules/ROOT/pages/servlet/oauth2/resource-server/index.adoc @@ -56,5 +56,7 @@ image:{icondir}/number_3.png[] If authentication fails, then __Failure__ image:{icondir}/number_4.png[] If authentication is successful, then __Success__. +* Any already-authenticated `Authentication` in the xref:servlet/authentication/architecture.adoc#servlet-authentication-securitycontextholder[`SecurityContextHolder`] is loaded and its +authorities are added to the returned xref:servlet/authentication/architecture.adoc#servlet-authentication-authentication[`Authentication`]. * The xref:servlet/authentication/architecture.adoc#servlet-authentication-authentication[Authentication] is set on the xref:servlet/authentication/architecture.adoc#servlet-authentication-securitycontextholder[SecurityContextHolder]. * The `BearerTokenAuthenticationFilter` invokes `FilterChain.doFilter(request,response)` to continue with the rest of the application logic. diff --git a/docs/modules/ROOT/pages/whats-new.adoc b/docs/modules/ROOT/pages/whats-new.adoc index 1aa5803d1e..c09c26b019 100644 --- a/docs/modules/ROOT/pages/whats-new.adoc +++ b/docs/modules/ROOT/pages/whats-new.adoc @@ -13,6 +13,7 @@ Each section that follows will indicate the more notable removals as well as the * Removed `AuthorizationManager#check` in favor of `AuthorizationManager#authorize` * Added xref:servlet/authorization/architecture.adoc#authz-authorization-manager-factory[`AuthorizationManagerFactory`] for creating `AuthorizationManager` instances in xref:servlet/authorization/authorize-http-requests.adoc#customizing-authorization-managers[request-based] and xref:servlet/authorization/method-security.adoc#customizing-authorization-managers[method-based] authorization components +* Added `Authentication.Builder` for mutating and merging `Authentication` instances == Config diff --git a/docs/src/test/java/org/springframework/security/docs/servlet/authentication/servletauthenticationauthentication/CopyAuthoritiesTests.java b/docs/src/test/java/org/springframework/security/docs/servlet/authentication/servletauthenticationauthentication/CopyAuthoritiesTests.java new file mode 100644 index 0000000000..ca5de102fa --- /dev/null +++ b/docs/src/test/java/org/springframework/security/docs/servlet/authentication/servletauthenticationauthentication/CopyAuthoritiesTests.java @@ -0,0 +1,41 @@ +package org.springframework.security.docs.servlet.authentication.servletauthenticationauthentication; + +import org.junit.jupiter.api.Test; + +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.SecurityAssertions; +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.authentication.ott.OneTimeTokenAuthentication; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.core.context.SecurityContextHolder; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +public class CopyAuthoritiesTests { + @Test + void toBuilderWhenApplyThenCopies() { + UsernamePasswordAuthenticationToken previous = new UsernamePasswordAuthenticationToken("alice", "pass", + AuthorityUtils.createAuthorityList("FACTOR_PASSWORD")); + SecurityContextHolder.getContext().setAuthentication(previous); + Authentication latest = new OneTimeTokenAuthentication("bob", + AuthorityUtils.createAuthorityList("FACTOR_OTT")); + AuthenticationManager authenticationManager = mock(AuthenticationManager.class); + given(authenticationManager.authenticate(any())).willReturn(latest); + Authentication authenticationRequest = new TestingAuthenticationToken("user", "pass"); + // tag::springSecurity[] + Authentication lastestResult = authenticationManager.authenticate(authenticationRequest); + Authentication previousResult = SecurityContextHolder.getContext().getAuthentication(); + if (previousResult != null && previousResult.isAuthenticated()) { + lastestResult = lastestResult.toBuilder() + .authorities((a) -> a.addAll(previous.getAuthorities())) + .build(); + } + // end::springSecurity[] + SecurityAssertions.assertThat(lastestResult).hasAuthorities("FACTOR_PASSWORD", "FACTOR_OTT"); + SecurityContextHolder.clearContext(); + } +} diff --git a/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/servletauthenticationauthentication/CopyAuthoritiesTests.kt b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/servletauthenticationauthentication/CopyAuthoritiesTests.kt new file mode 100644 index 0000000000..af25a3a346 --- /dev/null +++ b/docs/src/test/kotlin/org/springframework/security/kt/docs/servlet/authentication/servletauthenticationauthentication/CopyAuthoritiesTests.kt @@ -0,0 +1,39 @@ +package org.springframework.security.kt.docs.servlet.authentication.servletauthenticationauthentication + +import org.junit.jupiter.api.Test +import org.mockito.ArgumentMatchers +import org.mockito.BDDMockito +import org.mockito.Mockito +import org.springframework.security.authentication.AuthenticationManager +import org.springframework.security.authentication.SecurityAssertions +import org.springframework.security.authentication.TestingAuthenticationToken +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken +import org.springframework.security.authentication.ott.OneTimeTokenAuthentication +import org.springframework.security.core.Authentication +import org.springframework.security.core.authority.AuthorityUtils +import org.springframework.security.core.context.SecurityContextHolder + +class CopyAuthoritiesTests { + @Test + fun toBuilderWhenApplyThenCopies() { + val previous: Authentication = UsernamePasswordAuthenticationToken("alice", "pass", + AuthorityUtils.createAuthorityList("FACTOR_PASSWORD")) + SecurityContextHolder.getContext().authentication = previous + var latest: Authentication = OneTimeTokenAuthentication("bob", + AuthorityUtils.createAuthorityList("FACTOR_OTT")) + val authenticationManager: AuthenticationManager = Mockito.mock(AuthenticationManager::class.java) + BDDMockito.given(authenticationManager.authenticate(ArgumentMatchers.any())).willReturn(latest) + val authenticationRequest: Authentication = TestingAuthenticationToken("user", "pass") + // tag::springSecurity[] + var latestResult: Authentication = authenticationManager.authenticate(authenticationRequest) + val previousResult = SecurityContextHolder.getContext().authentication; + if (previousResult?.isAuthenticated == true) { + latestResult = latestResult.toBuilder().authorities { a -> + a.addAll(previousResult.authorities) + }.build() + } + // end::springSecurity[] + SecurityAssertions.assertThat(latestResult).hasAuthorities("FACTOR_PASSWORD", "FACTOR_OTT") + SecurityContextHolder.clearContext() + } +}