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 e19f8bd33cf..c05d5c5964c 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,6 +20,7 @@ import java.util.Collection; import org.apereo.cas.client.validation.Assertion; +import org.jspecify.annotations.Nullable; import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.core.GrantedAuthority; @@ -104,6 +105,19 @@ private CasAuthenticationToken(final Integer keyHash, final Object principal, fi setAuthenticated(true); } + protected CasAuthenticationToken(Builder builder) { + super(builder); + Assert.isTrue(!"".equals(builder.principal), "principal cannot be null or empty"); + Assert.notNull(!"".equals(builder.credentials), "credentials cannot be null or empty"); + Assert.notNull(builder.userDetails, "userDetails cannot be null"); + Assert.notNull(builder.assertion, "assertion cannot be null"); + this.keyHash = builder.keyHash; + this.principal = builder.principal; + this.credentials = builder.credentials; + this.userDetails = builder.userDetails; + this.assertion = builder.assertion; + } + private static Integer extractKeyHash(String key) { Assert.hasLength(key, "key cannot be null or empty"); return key.hashCode(); @@ -153,6 +167,11 @@ public UserDetails getUserDetails() { return this.userDetails; } + @Override + public Builder toBuilder() { + return new Builder<>(this); + } + @Override public String toString() { StringBuilder sb = new StringBuilder(); @@ -162,4 +181,81 @@ public String toString() { return (sb.toString()); } + /** + * A builder of {@link CasAuthenticationToken} instances + * + * @since 7.0 + */ + public static class Builder> extends AbstractAuthenticationBuilder { + + private Integer keyHash; + + private Object principal; + + private Object credentials; + + private UserDetails userDetails; + + private Assertion assertion; + + protected Builder(CasAuthenticationToken token) { + super(token); + this.keyHash = token.keyHash; + this.principal = token.principal; + this.credentials = token.credentials; + this.userDetails = token.userDetails; + this.assertion = token.assertion; + } + + /** + * Use this key + * @param key the key to use + * @return the {@link Builder} for further configurations + */ + public B key(String key) { + this.keyHash = key.hashCode(); + return (B) this; + } + + @Override + public B principal(@Nullable Object principal) { + Assert.notNull(principal, "principal cannot be null"); + this.principal = principal; + return (B) this; + } + + @Override + public B credentials(@Nullable Object credentials) { + Assert.notNull(credentials, "credentials cannot be null"); + this.credentials = credentials; + return (B) this; + } + + /** + * Use this {@link UserDetails} + * @param userDetails the {@link UserDetails} to use + * @return the {@link Builder} for further configurations + */ + public B userDetails(UserDetails userDetails) { + this.userDetails = userDetails; + return (B) this; + } + + /** + * Use this {@link Assertion} + * @param assertion the {@link Assertion} to use + * @return the {@link Builder} for further configurations + */ + public B assertion(Assertion assertion) { + this.assertion = assertion; + return (B) this; + } + + @Override + public CasAuthenticationToken build() { + return new CasAuthenticationToken(this); + } + + } + } 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 ac77f48c5b7..0337bafaf90 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 @@ -52,7 +52,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 +75,12 @@ public CasServiceTicketAuthenticationToken(String identifier, Object credentials super.setAuthenticated(true); } + protected CasServiceTicketAuthenticationToken(Builder builder) { + super(builder); + this.identifier = builder.principal; + this.credentials = builder.credentials; + } + public static CasServiceTicketAuthenticationToken stateful(Object credentials) { return new CasServiceTicketAuthenticationToken(CAS_STATEFUL_IDENTIFIER, credentials); } @@ -110,4 +116,46 @@ public void eraseCredentials() { this.credentials = null; } + public Builder toBuilder() { + return new Builder<>(this); + } + + /** + * A builder of {@link CasServiceTicketAuthenticationToken} instances + * + * @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 Object principal) { + Assert.isInstanceOf(String.class, principal, "principal must be of type String"); + this.principal = (String) principal; + return (B) this; + } + + @Override + public B credentials(@Nullable Object credentials) { + Assert.notNull(credentials, "credentials cannot be null"); + this.credentials = credentials; + return (B) this; + } + + @Override + public CasServiceTicketAuthenticationToken build() { + return new CasServiceTicketAuthenticationToken(this); + } + + } + } 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 aa0048d349d..506497124d4 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 @@ 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.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.PasswordEncodedUser; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; @@ -155,4 +157,29 @@ public void testToString() { assertThat(result.lastIndexOf("Credentials (Service/Proxy Ticket):") != -1).isTrue(); } + @Test + public void toBuilderWhenApplyThenCopies() { + Assertion assertionOne = new AssertionImpl("test"); + CasAuthenticationToken factorOne = new CasAuthenticationToken("key", "alice", "pass", + AuthorityUtils.createAuthorityList("FACTOR_ONE"), PasswordEncodedUser.user(), assertionOne); + Assertion assertionTwo = new AssertionImpl("test"); + CasAuthenticationToken factorTwo = new CasAuthenticationToken("yek", "bob", "ssap", + AuthorityUtils.createAuthorityList("FACTOR_TWO"), PasswordEncodedUser.admin(), assertionTwo); + CasAuthenticationToken authentication = factorOne.toBuilder() + .authorities((a) -> a.addAll(factorTwo.getAuthorities())) + .key("yek") + .principal(factorTwo.getPrincipal()) + .credentials(factorTwo.getCredentials()) + .userDetails(factorTwo.getUserDetails()) + .assertion(factorTwo.getAssertion()) + .build(); + Set 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/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 2616edb1fc7..4dc7c3ee3e6 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.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 boolean supports(Class authentication) { 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 c2aeef4c0a7..97c18412b74 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.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 boolean supports(Class authentication) { 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 7ec41671600..18307f8b641 100644 --- a/core/src/main/java/org/springframework/security/authentication/AbstractAuthenticationToken.java +++ b/core/src/main/java/org/springframework/security/authentication/AbstractAuthenticationToken.java @@ -16,10 +16,13 @@ package org.springframework.security.authentication; +import java.io.Serial; import java.security.Principal; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.function.Consumer; import org.jspecify.annotations.Nullable; @@ -41,6 +44,9 @@ */ public abstract class AbstractAuthenticationToken implements Authentication, CredentialsContainer { + @Serial + private static final long serialVersionUID = -3194696462184782834L; + private final Collection authorities; private @Nullable Object details; @@ -63,6 +69,12 @@ public AbstractAuthenticationToken(@Nullable Collection(authorities)); } + protected AbstractAuthenticationToken(AbstractAuthenticationBuilder builder) { + this(builder.authorities); + this.authenticated = builder.authenticated; + this.details = builder.details; + } + @Override public Collection getAuthorities() { return this.authorities; @@ -185,4 +197,48 @@ public String toString() { 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 { + + private boolean authenticated; + + private @Nullable Object details; + + private 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; + } + + } + } 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 2b489dbbdb4..c30fcbae3e7 100644 --- a/core/src/main/java/org/springframework/security/authentication/DelegatingReactiveAuthenticationManager.java +++ b/core/src/main/java/org/springframework/security/authentication/DelegatingReactiveAuthenticationManager.java @@ -61,7 +61,6 @@ public Mono authenticate(Authentication authentication) { 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 d90bfe5bad4..7167943a339 100644 --- a/core/src/main/java/org/springframework/security/authentication/ProviderManager.java +++ b/core/src/main/java/org/springframework/security/authentication/ProviderManager.java @@ -182,7 +182,7 @@ public Authentication authenticate(Authentication authentication) throws Authent try { result = provider.authenticate(authentication); if (result != null) { - copyDetails(authentication, result); + result = copyDetails(authentication, result); break; } } @@ -277,10 +277,14 @@ private void prepareException(AuthenticationException ex, Authentication auth) { * @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() { 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 5c17618cef8..81d7edeafad 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,10 @@ import java.util.Collection; +import org.jspecify.annotations.Nullable; + import org.springframework.security.core.GrantedAuthority; +import org.springframework.util.Assert; /** * Represents a remembered Authentication. @@ -70,6 +73,12 @@ private RememberMeAuthenticationToken(Integer keyHash, Object principal, 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 @@ -88,6 +97,11 @@ public Object getPrincipal() { return this.principal; } + @Override + public Builder toBuilder() { + return new Builder<>(this); + } + @Override public boolean equals(Object obj) { if (!super.equals(obj)) { @@ -106,4 +120,45 @@ public int hashCode() { return result; } + /** + * A builder of {@link RememberMeAuthenticationToken} instances + * + * @since 7.0 + */ + public static class Builder> extends AbstractAuthenticationBuilder { + + private Integer keyHash; + + private Object principal; + + protected Builder(RememberMeAuthenticationToken token) { + super(token); + this.keyHash = token.getKeyHash(); + this.principal = token.getPrincipal(); + } + + @Override + public B principal(@Nullable Object principal) { + Assert.notNull(principal, "principal cannot be null"); + this.principal = principal; + return (B) this; + } + + /** + * Use this key + * @param key the key to use + * @return the {@link Builder} for further configurations + */ + public B key(String key) { + this.keyHash = key.hashCode(); + return (B) this; + } + + @Override + public RememberMeAuthenticationToken build() { + return new RememberMeAuthenticationToken(this); + } + + } + } 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 abfc6560f45..70778db2a41 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,11 @@ import java.util.Collection; import java.util.List; +import org.jspecify.annotations.Nullable; + import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.util.Assert; /** * An {@link org.springframework.security.core.Authentication} implementation that is @@ -39,7 +42,7 @@ public class TestingAuthenticationToken extends AbstractAuthenticationToken { private final Object principal; public TestingAuthenticationToken(Object principal, Object credentials) { - super(null); + super((Collection) null); this.principal = principal; this.credentials = credentials; } @@ -61,6 +64,12 @@ public TestingAuthenticationToken(Object principal, Object credentials, setAuthenticated(true); } + protected TestingAuthenticationToken(Builder builder) { + super(builder); + this.principal = builder.principal; + this.credentials = builder.credentials; + } + @Override public Object getCredentials() { return this.credentials; @@ -71,4 +80,47 @@ public Object getPrincipal() { return this.principal; } + @Override + public Builder toBuilder() { + return new Builder<>(this); + } + + /** + * A builder of {@link TestingAuthenticationToken} instances + * + * @since 7.0 + */ + public static class Builder> extends AbstractAuthenticationBuilder { + + private Object principal; + + private Object credentials; + + protected Builder(TestingAuthenticationToken token) { + super(token); + this.principal = token.principal; + this.credentials = token.credentials; + } + + @Override + public B principal(@Nullable Object principal) { + Assert.notNull(principal, "principal cannot be null"); + this.principal = principal; + return (B) this; + } + + @Override + public B credentials(@Nullable Object credentials) { + Assert.notNull(credentials, "credentials cannot be null"); + this.credentials = credentials; + return (B) this; + } + + @Override + public TestingAuthenticationToken build() { + return new TestingAuthenticationToken(this); + } + + } + } 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 c25b4a9ce00..c63e5dfb349 100644 --- a/core/src/main/java/org/springframework/security/authentication/UsernamePasswordAuthenticationToken.java +++ b/core/src/main/java/org/springframework/security/authentication/UsernamePasswordAuthenticationToken.java @@ -50,7 +50,7 @@ public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationT * */ public UsernamePasswordAuthenticationToken(@Nullable Object principal, @Nullable Object credentials) { - super(null); + super((Collection) null); this.principal = principal; this.credentials = credentials; setAuthenticated(false); @@ -73,6 +73,12 @@ public UsernamePasswordAuthenticationToken(Object principal, @Nullable Object cr 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. @@ -124,4 +130,46 @@ public void eraseCredentials() { this.credentials = null; } + @Override + public Builder toBuilder() { + return new Builder<>(this); + } + + /** + * A builder of {@link UsernamePasswordAuthenticationToken} instances + * + * @since 7.0 + */ + public static class Builder> extends AbstractAuthenticationBuilder { + + private @Nullable Object principal; + + private @Nullable Object credentials; + + protected Builder(UsernamePasswordAuthenticationToken token) { + super(token); + this.principal = token.principal; + this.credentials = token.credentials; + } + + @Override + public B principal(@Nullable Object principal) { + Assert.notNull(principal, "principal cannot be null"); + this.principal = principal; + return (B) this; + } + + @Override + public B credentials(@Nullable Object credentials) { + this.credentials = credentials; + return (B) this; + } + + @Override + public UsernamePasswordAuthenticationToken build() { + return new UsernamePasswordAuthenticationToken(this); + } + + } + } 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 314f79e5636..9e1200a58fd 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 @@ -48,8 +48,49 @@ public JaasAuthenticationToken(Object principal, @Nullable Object credentials, L this.loginContext = loginContext; } + protected JaasAuthenticationToken(Builder builder) { + super(builder); + this.loginContext = builder.loginContext; + } + public LoginContext getLoginContext() { return this.loginContext; } + @Override + public Builder toBuilder() { + return new Builder<>(this); + } + + /** + * A builder of {@link JaasAuthenticationToken} instances + * + * @since 7.0 + */ + public static class Builder> extends UsernamePasswordAuthenticationToken.Builder { + + private LoginContext loginContext; + + protected Builder(JaasAuthenticationToken token) { + super(token); + this.loginContext = token.getLoginContext(); + } + + /** + * Use this {@link LoginContext} + * @param loginContext the {@link LoginContext} to use + * @return the {@link Builder} for further configurations + */ + public B loginContext(LoginContext loginContext) { + this.loginContext = loginContext; + return (B) this; + } + + @Override + public JaasAuthenticationToken build() { + return new JaasAuthenticationToken(this); + } + + } + } 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 fc0c8064634..2b961f3c327 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.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.core.GrantedAuthority; +import org.springframework.util.Assert; /** * The result of a successful one-time-token authentication @@ -43,6 +44,11 @@ public OneTimeTokenAuthentication(Object principal, Collection builder) { + super(builder); + this.principal = builder.principal; + } + @Override public Object getPrincipal() { return this.principal; @@ -53,4 +59,41 @@ public Object getPrincipal() { return null; } + @Override + public Builder toBuilder() { + return new Builder<>(this); + } + + /** + * A builder of {@link OneTimeTokenAuthentication} instances + * + * @since 7.0 + */ + public static class Builder> extends AbstractAuthenticationBuilder { + + private Object principal; + + protected Builder(OneTimeTokenAuthentication token) { + super(token); + this.principal = token.principal; + } + + /** + * Use this principal. + * @return the {@link Builder} for further configuration + */ + @Override + public B principal(@Nullable Object principal) { + Assert.notNull(principal, "principal cannot be null"); + this.principal = principal; + return (B) this; + } + + @Override + public OneTimeTokenAuthentication build() { + return new OneTimeTokenAuthentication(this); + } + + } + } 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 c8515b25fcd..af581bc9ba8 100644 --- a/core/src/main/java/org/springframework/security/core/Authentication.java +++ b/core/src/main/java/org/springframework/security/core/Authentication.java @@ -19,6 +19,7 @@ import java.io.Serializable; import java.security.Principal; import java.util.Collection; +import java.util.function.Consumer; import org.jspecify.annotations.Nullable; @@ -136,4 +137,110 @@ public interface Authentication extends Principal, Serializable { */ void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException; + /** + * Return an {@link Builder} based on this instance. By default, returns a builder + * that builds a {@link SimpleAuthentication}. + *

+ * 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 + */ + default Builder toBuilder() { + return new SimpleAuthentication.Builder(this); + } + + /** + * A builder based on a given {@link Authentication} instance + * + * @author Josh Cummings + * @since 7.0 + */ + interface Builder> { + + /** + * 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); + + /** + * Build an {@link Authentication} instance + * @return the {@link Authentication} instance + */ + Authentication build(); + + } + } 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 00000000000..367c01d1628 --- /dev/null +++ b/core/src/main/java/org/springframework/security/core/SimpleAuthentication.java @@ -0,0 +1,151 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.core; + +import java.io.Serial; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.function.Consumer; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; + +@Transient +final class SimpleAuthentication implements Authentication { + + @Serial + private static final long serialVersionUID = 3194696462184782814L; + + private final @Nullable Object principal; + + private final @Nullable Object credentials; + + private final Collection 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 Log logger = LogFactory.getLog(getClass()); + + 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.logger.debug("Creating a builder which will result in exchanging an authentication of type " + + authentication.getClass() + " for " + SimpleAuthentication.class.getSimpleName() + ";" + + " consider implementing " + authentication.getClass().getSimpleName() + "#toBuilder"); + this.authorities.addAll(authentication.getAuthorities()); + this.principal = authentication.getPrincipal(); + this.credentials = authentication.getCredentials(); + this.details = authentication.getDetails(); + this.authenticated = authentication.isAuthenticated(); + + } + + @Override + public Builder authorities(Consumer> 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); + } + + } + +} 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 00000000000..6bd66b71667 --- /dev/null +++ b/core/src/test/java/org/springframework/security/authentication/AbstractAuthenticationBuilderTests.java @@ -0,0 +1,54 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.authentication; + +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.AuthorityUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +class AbstractAuthenticationBuilderTests { + + @Test + void applyWhenAuthoritiesThenAdds() { + TestingAuthenticationToken factorOne = new TestingAuthenticationToken("user", "pass", "FACTOR_ONE"); + TestingAuthenticationToken factorTwo = new TestingAuthenticationToken("user", "pass", "FACTOR_TWO"); + TestAbstractAuthenticationBuilder builder = new TestAbstractAuthenticationBuilder(factorOne); + Authentication result = builder.authorities((a) -> a.addAll(factorTwo.getAuthorities())).build(); + Set authorities = AuthorityUtils.authorityListToSet(result.getAuthorities()); + assertThat(authorities).containsExactlyInAnyOrder("FACTOR_ONE", "FACTOR_TWO"); + } + + private static final class TestAbstractAuthenticationBuilder + extends TestingAuthenticationToken.Builder { + + private TestAbstractAuthenticationBuilder(TestingAuthenticationToken token) { + super(token); + } + + @Override + public TestingAuthenticationToken build() { + return new TestingAuthenticationToken(this); + } + + } + +} 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 7bb0c136bca..69fff2567c5 100644 --- a/core/src/test/java/org/springframework/security/authentication/ProviderManagerTests.java +++ b/core/src/test/java/org/springframework/security/authentication/ProviderManagerTests.java @@ -18,6 +18,7 @@ import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.List; import org.junit.jupiter.api.Test; @@ -25,6 +26,7 @@ import org.springframework.context.MessageSource; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.GrantedAuthority; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; @@ -47,7 +49,7 @@ public class ProviderManagerTests { @Test void authenticationFailsWithUnsupportedToken() { - Authentication token = new AbstractAuthenticationToken(null) { + Authentication token = new AbstractAuthenticationToken((Collection) null) { @Override public Object getCredentials() { return ""; @@ -78,24 +80,24 @@ void credentialsAreClearedByDefault() { @Test void authenticationSucceedsWithSupportedTokenAndReturnsExpectedObject() { - Authentication a = mock(Authentication.class); + Authentication a = new TestingAuthenticationToken("user", "pass", "FACTOR"); ProviderManager mgr = new ProviderManager(createProviderWhichReturns(a)); AuthenticationEventPublisher publisher = mock(AuthenticationEventPublisher.class); mgr.setAuthenticationEventPublisher(publisher); Authentication result = mgr.authenticate(a); - assertThat(result).isEqualTo(a); + assertThat(result.getPrincipal()).isEqualTo(a.getPrincipal()); verify(publisher).publishAuthenticationSuccess(result); } @Test void authenticationSucceedsWhenFirstProviderReturnsNullButSecondAuthenticates() { - Authentication a = mock(Authentication.class); + Authentication a = new TestingAuthenticationToken("user", "pass", "FACTOR"); ProviderManager mgr = new ProviderManager( Arrays.asList(createProviderWhichReturns(null), createProviderWhichReturns(a))); AuthenticationEventPublisher publisher = mock(AuthenticationEventPublisher.class); mgr.setAuthenticationEventPublisher(publisher); Authentication result = mgr.authenticate(a); - assertThat(result).isSameAs(a); + assertThat(result.getPrincipal()).isEqualTo(a.getPrincipal()); verify(publisher).publishAuthenticationSuccess(result); } @@ -162,11 +164,12 @@ void detailsAreSetOnAuthenticationTokenIfNotAlreadySetByProvider() { @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 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 cfab36a2e17..6490f2bc3da 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,21 @@ public void constructorWhenCollectionAuthoritiesThenAuthenticated() { assertThat(authenticated.isAuthenticated()).isTrue(); } + @Test + public void toBuilderWhenApplyThenCopies() { + TestingAuthenticationToken factorOne = new TestingAuthenticationToken("alice", "pass", + AuthorityUtils.createAuthorityList("FACTOR_ONE")); + TestingAuthenticationToken factorTwo = new TestingAuthenticationToken("bob", "ssap", + AuthorityUtils.createAuthorityList("FACTOR_TWO")); + TestingAuthenticationToken result = factorOne.toBuilder() + .authorities((a) -> a.addAll(factorTwo.getAuthorities())) + .principal(factorTwo.getPrincipal()) + .credentials(factorTwo.getCredentials()) + .build(); + Set 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 0e25aef80db..d09bbdabf8b 100644 --- a/core/src/test/java/org/springframework/security/authentication/UsernamePasswordAuthenticationTokenTests.java +++ b/core/src/test/java/org/springframework/security/authentication/UsernamePasswordAuthenticationTokenTests.java @@ -16,6 +16,8 @@ package org.springframework.security.authentication; +import java.util.Set; + import org.junit.jupiter.api.Test; import org.springframework.security.core.authority.AuthorityUtils; @@ -85,4 +87,21 @@ public void authenticatedFactoryMethodResultsAuthenticatedToken() { assertThat(grantedToken.isAuthenticated()).isTrue(); } + @Test + public void toBuilderWhenApplyThenCopies() { + UsernamePasswordAuthenticationToken factorOne = new UsernamePasswordAuthenticationToken("alice", "pass", + AuthorityUtils.createAuthorityList("FACTOR_ONE")); + UsernamePasswordAuthenticationToken factorTwo = new UsernamePasswordAuthenticationToken("bob", "ssap", + AuthorityUtils.createAuthorityList("FACTOR_TWO")); + UsernamePasswordAuthenticationToken result = factorOne.toBuilder() + .authorities((a) -> a.addAll(factorTwo.getAuthorities())) + .principal(factorTwo.getPrincipal()) + .credentials(factorTwo.getCredentials()) + .build(); + Set 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 new file mode 100644 index 00000000000..a3409848f0b --- /dev/null +++ b/core/src/test/java/org/springframework/security/authentication/jaas/JaasAuthenticationTokenTests.java @@ -0,0 +1,51 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.authentication.jaas; + +import java.util.Set; + +import javax.security.auth.login.LoginContext; + +import org.junit.jupiter.api.Test; + +import org.springframework.security.core.authority.AuthorityUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +class JaasAuthenticationTokenTests { + + @Test + void toBuilderWhenApplyThenCopies() { + JaasAuthenticationToken factorOne = new JaasAuthenticationToken("alice", "pass", + AuthorityUtils.createAuthorityList("FACTOR_ONE"), mock(LoginContext.class)); + JaasAuthenticationToken factorTwo = new JaasAuthenticationToken("bob", "ssap", + AuthorityUtils.createAuthorityList("FACTOR_TWO"), mock(LoginContext.class)); + JaasAuthenticationToken result = factorOne.toBuilder() + .authorities((a) -> a.addAll(factorTwo.getAuthorities())) + .principal(factorTwo.getPrincipal()) + .credentials(factorTwo.getCredentials()) + .loginContext(factorTwo.getLoginContext()) + .build(); + Set 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 00000000000..aec07a280d3 --- /dev/null +++ b/core/src/test/java/org/springframework/security/authentication/ott/OneTimeTokenAuthenticationTests.java @@ -0,0 +1,44 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.authentication.ott; + +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import org.springframework.security.core.authority.AuthorityUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +class OneTimeTokenAuthenticationTests { + + @Test + void toBuilderWhenApplyThenCopies() { + OneTimeTokenAuthentication factorOne = new OneTimeTokenAuthentication("alice", + AuthorityUtils.createAuthorityList("FACTOR_ONE")); + OneTimeTokenAuthentication factorTwo = new OneTimeTokenAuthentication("bob", + AuthorityUtils.createAuthorityList("FACTOR_TWO")); + OneTimeTokenAuthentication result = factorOne.toBuilder() + .authorities((a) -> a.addAll(factorTwo.getAuthorities())) + .principal(factorTwo.getPrincipal()) + .build(); + Set authorities = AuthorityUtils.authorityListToSet(result.getAuthorities()); + assertThat(result.getPrincipal()).isSameAs(factorTwo.getPrincipal()); + assertThat(authorities).containsExactlyInAnyOrder("FACTOR_ONE", "FACTOR_TWO"); + } + +} 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 9e8f1234457..d40954b9500 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 @@ 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.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 void testSetAuthenticatedIgnored() { 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"); + } + } diff --git a/docs/modules/ROOT/pages/servlet/authentication/architecture.adoc b/docs/modules/ROOT/pages/servlet/authentication/architecture.adoc index 7d900f94767..89abe494071 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 77de88f55e0..4e24d44e572 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 e30e55dca15..092a5200731 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 1aa5803d1e9..c09c26b019b 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 00000000000..ca5de102fa9 --- /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 00000000000..af25a3a346a --- /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() + } +} 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 741189304c5..82184888b31 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 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 Builder principal(String principalName) { 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 c766522199b..44e6875dc1c 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 @@ 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 OAuth2AuthenticationToken(OAuth2User principal, Collection builder) { + super(builder); + Assert.notNull(builder.principal, "principal cannot be null"); + Assert.hasText(builder.authorizedClientRegistrationId, "authorizedClientRegistrationId cannot be empty"); + this.principal = builder.principal; + this.authorizedClientRegistrationId = builder.authorizedClientRegistrationId; + } + @Override public OAuth2User getPrincipal() { return this.principal; @@ -85,4 +95,53 @@ public String getAuthorizedClientRegistrationId() { return this.authorizedClientRegistrationId; } + @Override + public Builder toBuilder() { + return new Builder<>(this); + } + + /** + * A builder of {@link OAuth2AuthenticationToken} instances + * + * @since 7.0 + */ + public static class Builder> extends AbstractAuthenticationBuilder { + + private OAuth2User principal; + + private String authorizedClientRegistrationId; + + protected Builder(OAuth2AuthenticationToken token) { + super(token); + this.principal = token.principal; + this.authorizedClientRegistrationId = token.authorizedClientRegistrationId; + } + + @Override + public B principal(@Nullable Object principal) { + Assert.isInstanceOf(OAuth2User.class, principal, "principal must be of type OAuth2User"); + this.principal = (OAuth2User) principal; + return (B) this; + } + + /** + * Use this + * {@link org.springframework.security.oauth2.client.registration.ClientRegistration} + * {@code registrationId}. + * @param authorizedClientRegistrationId the registration id to use + * @return the {@link Builder} for further configurations + * @see OAuth2AuthenticationToken#getAuthorizedClientRegistrationId + */ + public B authorizedClientRegistrationId(String authorizedClientRegistrationId) { + this.authorizedClientRegistrationId = authorizedClientRegistrationId; + return (B) this; + } + + @Override + public OAuth2AuthenticationToken build() { + return new OAuth2AuthenticationToken(this); + } + + } + } 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 47c27acbaaf..6f7f8d3a12e 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.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 @@ static HttpServletResponse getResponse(Map attrs) { 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 4c590fcaf4e..2cd5a9d5e35 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 @@ import java.util.Collection; import java.util.Collections; +import java.util.Set; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.security.oauth2.core.user.TestOAuth2Users; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; @@ -82,4 +85,21 @@ public void constructorWhenAllParametersProvidedAndValidThenCreated() { assertThat(authentication.isAuthenticated()).isEqualTo(true); } + @Test + public void toBuilderWhenApplyThenCopies() { + OAuth2AuthenticationToken factorOne = new OAuth2AuthenticationToken(TestOAuth2Users.create(), + AuthorityUtils.createAuthorityList("FACTOR_ONE"), "alice"); + OAuth2AuthenticationToken factorTwo = new OAuth2AuthenticationToken(TestOAuth2Users.create(), + AuthorityUtils.createAuthorityList("FACTOR_TWO"), "bob"); + OAuth2AuthenticationToken result = factorOne.toBuilder() + .authorities((a) -> a.addAll(factorTwo.getAuthorities())) + .principal(factorTwo.getPrincipal()) + .authorizedClientRegistrationId(factorTwo.getAuthorizedClientRegistrationId()) + .build(); + Set 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/AbstractOAuth2TokenAuthenticationToken.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/authentication/AbstractOAuth2TokenAuthenticationToken.java index b8e2be61e0b..86ae387e009 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 @@ 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 @@ protected AbstractOAuth2TokenAuthenticationToken(T token, Object principal, Obje this.token = token; } + protected AbstractOAuth2TokenAuthenticationToken(AbstractOAuth2TokenAuthenticationBuilder builder) { + super(builder); + Assert.notNull(builder.credentials, "token cannot be null"); + Assert.notNull(builder.principal, "principal cannot be null"); + this.principal = builder.principal; + this.credentials = builder.credentials; + this.token = builder.token; + } + @Override public Object getPrincipal() { return this.principal; @@ -106,4 +117,53 @@ public final T getToken() { */ public abstract Map getTokenAttributes(); + /** + * A builder for {@link AbstractOAuth2TokenAuthenticationToken} implementations + * + * @param + * @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; + } + + /** + * The OAuth 2.0 Token to use + * @param token the token to use + * @return the {@link Builder} for further configurations + */ + public B token(T token) { + Assert.notNull(token, "token cannot be null"); + this.token = token; + return (B) this; + } + + } + } 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 f3dfb832709..0da2633c7a8 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,9 @@ import java.util.LinkedHashMap; import java.util.Map; +import org.jspecify.annotations.Nullable; + +import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.Transient; import org.springframework.security.oauth2.core.OAuth2AccessToken; @@ -56,9 +59,81 @@ public BearerTokenAuthentication(OAuth2AuthenticatedPrincipal principal, OAuth2A 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<>(this); + } + + /** + * A builder preserving the concrete {@link Authentication} type + * + * @since 7.0 + */ + public static class Builder> + extends AbstractOAuth2TokenAuthenticationBuilder { + + private Map attributes; + + protected Builder(BearerTokenAuthentication token) { + super(token); + this.attributes = token.getTokenAttributes(); + } + + /** + * Use this principal. Must be of type {@link OAuth2AuthenticatedPrincipal} + * @param principal the principal to use + * @return the {@link Builder} for further configurations + */ + @Override + public B principal(@Nullable Object principal) { + Assert.isInstanceOf(OAuth2AuthenticatedPrincipal.class, principal, + "principal must be of type OAuth2AuthenticatedPrincipal"); + this.attributes = ((OAuth2AuthenticatedPrincipal) principal).getAttributes(); + return super.principal(principal); + } + + /** + * A synonym for {@link #token(OAuth2AccessToken)} + * @param token the token to use + * @return the {@link Builder} for further configurations + */ + @Override + public B credentials(@Nullable Object token) { + Assert.isInstanceOf(OAuth2AccessToken.class, token, "token must be of type OAuth2AccessToken"); + return token((OAuth2AccessToken) token); + } + + /** + * Use this token. Must have a {@link OAuth2AccessToken#getTokenType()} as + * {@link OAuth2AccessToken.TokenType#BEARER}. + * @param token the token to use + * @return the {@link Builder} for further configurations + */ + @Override + public B token(OAuth2AccessToken token) { + Assert.isTrue(token.getTokenType() == OAuth2AccessToken.TokenType.BEARER, "token must be a bearer token"); + super.credentials(token); + return super.token(token); + } + + /** + * {@inheritDoc} + */ + @Override + public BearerTokenAuthentication build() { + return new BearerTokenAuthentication(this); + } + + } + } 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 43cc749d9d9..7e52b30e001 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,9 +19,13 @@ import java.util.Collection; import java.util.Map; +import org.jspecify.annotations.Nullable; + +import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.Transient; import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.util.Assert; /** * An implementation of an {@link AbstractOAuth2TokenAuthenticationToken} representing a @@ -71,6 +75,11 @@ public JwtAuthenticationToken(Jwt jwt, Collection au this.name = name; } + protected JwtAuthenticationToken(Builder builder) { + super(builder); + this.name = builder.name; + } + @Override public Map getTokenAttributes() { return this.getToken().getClaims(); @@ -84,4 +93,74 @@ public String getName() { return this.name; } + @Override + public Builder toBuilder() { + return new Builder<>(this); + } + + /** + * A builder for {@link JwtAuthenticationToken} instances + * + * @since 7.0 + * @see Authentication.Builder + */ + public static class Builder> extends AbstractOAuth2TokenAuthenticationBuilder { + + private String name; + + protected Builder(JwtAuthenticationToken token) { + super(token); + this.name = token.getName(); + } + + /** + * A synonym for {@link #token(Jwt)} + * @return the {@link Builder} for further configurations + */ + @Override + public B principal(@Nullable Object principal) { + Assert.isInstanceOf(Jwt.class, principal, "principal must be of type Jwt"); + return token((Jwt) principal); + } + + /** + * A synonym for {@link #token(Jwt)} + * @return the {@link Builder} for further configurations + */ + @Override + public B credentials(@Nullable Object credentials) { + Assert.isInstanceOf(Jwt.class, credentials, "credentials must be of type Jwt"); + return token((Jwt) credentials); + } + + /** + * Use this {@code token} as the token, principal, and credentials. Also sets the + * {@code name} to {@link Jwt#getSubject}. + * @param token the token to use + * @return the {@link Builder} for further configurations + */ + @Override + public B token(Jwt token) { + super.principal(token); + super.credentials(token); + return super.token(token).name(token.getSubject()); + } + + /** + * The name to use. + * @param name the name to use + * @return the {@link Builder} for further configurations + */ + public B name(String name) { + this.name = name; + return (B) this; + } + + @Override + public JwtAuthenticationToken build() { + return new JwtAuthenticationToken(this); + } + + } + } 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 1deb3a76892..89adbf9f5c9 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 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse 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 39360f862d5..d1fef9d1f8b 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.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.OAuth2AccessToken; import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal; import org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaimNames; +import org.springframework.security.oauth2.core.TestOAuth2AuthenticatedPrincipals; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; @@ -151,4 +153,24 @@ public void toStringWhenAttributesContainsURLThenDoesNotFail() throws Exception token.toString(); } + @Test + public void toBuilderWhenApplyThenCopies() { + BearerTokenAuthentication factorOne = new BearerTokenAuthentication(TestOAuth2AuthenticatedPrincipals.active(), + this.token, AuthorityUtils.createAuthorityList("FACTOR_ONE")); + BearerTokenAuthentication factorTwo = new BearerTokenAuthentication( + TestOAuth2AuthenticatedPrincipals.active((m) -> m.put("k", "v")), + new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, "nekot", Instant.now(), + Instant.now().plusSeconds(3600)), + AuthorityUtils.createAuthorityList("FACTOR_TWO")); + BearerTokenAuthentication authentication = factorOne.toBuilder() + .authorities((a) -> a.addAll(factorTwo.getAuthorities())) + .principal(factorTwo.getPrincipal()) + .token(factorTwo.getToken()) + .build(); + Set 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 d6af03cc3bd..2114c6f87b8 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; @@ -54,7 +55,7 @@ public void getNameWhenJwtHasNoSubjectThenReturnsNull() { @Test public void constructorWhenJwtIsNullThenThrowsException() { - assertThatIllegalArgumentException().isThrownBy(() -> new JwtAuthenticationToken(null)) + assertThatIllegalArgumentException().isThrownBy(() -> new JwtAuthenticationToken((Jwt) null)) .withMessageContaining("token cannot be null"); } @@ -115,6 +116,23 @@ public void getNameWhenConstructedWithNoSubjectThenReturnsNull() { assertThat(new JwtAuthenticationToken(jwt).getName()).isNull(); } + @Test + public void toBuilderWhenApplyThenCopies() { + JwtAuthenticationToken factorOne = new JwtAuthenticationToken(builder().claim("c", "v").build(), + AuthorityUtils.createAuthorityList("FACTOR_ONE"), "alice"); + JwtAuthenticationToken factorTwo = new JwtAuthenticationToken(builder().claim("d", "w").build(), + AuthorityUtils.createAuthorityList("FACTOR_TWO"), "bob"); + JwtAuthenticationToken result = factorOne.toBuilder() + .authorities((a) -> a.addAll(factorTwo.getAuthorities())) + .principal(factorTwo.getPrincipal()) + .name(factorTwo.getName()) + .build(); + Set 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/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 8f0f91093ca..0fc4974a6c8 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 @@ 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.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 void doFilterWhenCustomSecurityContextHolderStrategyThenUses() throws Ser 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 void constructorWhenNullAuthenticationManagerResolverThenThrowsException( // @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 3b528c04a3d..faf3d13db48 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,7 +19,11 @@ import java.io.Serial; import java.util.Collection; +import org.jspecify.annotations.Nullable; + import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; +import org.springframework.util.Assert; /** * An authentication based off of a SAML 2.0 Assertion @@ -53,6 +57,12 @@ public Saml2AssertionAuthentication(Object principal, Saml2ResponseAssertionAcce setAuthenticated(true); } + protected Saml2AssertionAuthentication(Builder builder) { + super(builder); + this.assertion = builder.assertion; + this.relyingPartyRegistrationId = builder.relyingPartyRegistrationId; + } + @Override public Saml2ResponseAssertionAccessor getCredentials() { return this.assertion; @@ -62,4 +72,59 @@ public String getRelyingPartyRegistrationId() { return this.relyingPartyRegistrationId; } + @Override + public Builder toBuilder() { + return new Builder<>(this); + } + + /** + * A builder of {@link Saml2AssertionAuthentication} instances + * + * @since 7.0 + */ + public static class Builder> extends Saml2Authentication.Builder { + + private Saml2ResponseAssertionAccessor assertion; + + private String relyingPartyRegistrationId; + + protected Builder(Saml2AssertionAuthentication token) { + super(token); + this.assertion = token.assertion; + this.relyingPartyRegistrationId = token.relyingPartyRegistrationId; + } + + /** + * Use these credentials. They must be of type + * {@link Saml2ResponseAssertionAccessor}. + * @param credentials the credentials to use + * @return the {@link Builder} for further configurations + */ + @Override + public B credentials(@Nullable Object credentials) { + Assert.isInstanceOf(Saml2ResponseAssertionAccessor.class, credentials, + "credentials must be of type Saml2ResponseAssertionAccessor"); + saml2Response(((Saml2ResponseAssertionAccessor) credentials).getResponseValue()); + this.assertion = (Saml2ResponseAssertionAccessor) credentials; + return (B) this; + } + + /** + * Use this registration id + * @param relyingPartyRegistrationId the + * {@link RelyingPartyRegistration#getRegistrationId} to use + * @return the {@link Builder} for further configurations + */ + public B relyingPartyRegistrationId(String relyingPartyRegistrationId) { + this.relyingPartyRegistrationId = relyingPartyRegistrationId; + return (B) this; + } + + @Override + public Saml2AssertionAuthentication build() { + return new Saml2AssertionAuthentication(this); + } + + } + } 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 82b4042c493..d3e57fe3bd8 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 @@ 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 Saml2Authentication(Object principal, String saml2Response, 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 Object getCredentials() { 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 new file mode 100644 index 00000000000..d67ee3bc7ca --- /dev/null +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/Saml2AssertionAuthenticationTests.java @@ -0,0 +1,49 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.saml2.provider.service.authentication; + +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import org.springframework.security.core.authority.AuthorityUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +class Saml2AssertionAuthenticationTests { + + @Test + void toBuilderWhenApplyThenCopies() { + Saml2ResponseAssertion.Builder prototype = Saml2ResponseAssertion.withResponseValue("response"); + Saml2AssertionAuthentication factorOne = new Saml2AssertionAuthentication("alice", + prototype.nameId("alice").build(), AuthorityUtils.createAuthorityList("FACTOR_ONE"), "alice"); + Saml2AssertionAuthentication factorTwo = new Saml2AssertionAuthentication("bob", + prototype.nameId("bob").build(), AuthorityUtils.createAuthorityList("FACTOR_TWO"), "bob"); + Saml2AssertionAuthentication result = factorOne.toBuilder() + .authorities((a) -> a.addAll(factorTwo.getAuthorities())) + .principal(factorTwo.getPrincipal()) + .credentials(factorTwo.getCredentials()) + .relyingPartyRegistrationId(factorTwo.getRelyingPartyRegistrationId()) + .build(); + Set 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/AbstractAuthenticationProcessingFilter.java b/web/src/main/java/org/springframework/security/web/authentication/AbstractAuthenticationProcessingFilter.java index a817d7013ef..6bbbb3f7669 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 @@ private void doFilter(HttpServletRequest request, HttpServletResponse response, // 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 5366ebd84b4..8295a962bcb 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 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse 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 8189cc2b1f8..b0910c85ed3 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 @@ private void doAuthenticate(HttpServletRequest request, HttpServletResponse resp 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/preauth/PreAuthenticatedAuthenticationToken.java b/web/src/main/java/org/springframework/security/web/authentication/preauth/PreAuthenticatedAuthenticationToken.java index fefda2ca490..e7d75c32064 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 @@ -22,6 +22,7 @@ import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.core.GrantedAuthority; +import org.springframework.util.Assert; /** * {@link org.springframework.security.core.Authentication} implementation for @@ -46,7 +47,7 @@ public class PreAuthenticatedAuthenticationToken extends AbstractAuthenticationT * @param aCredentials The pre-authenticated credentials */ public PreAuthenticatedAuthenticationToken(Object aPrincipal, @Nullable Object aCredentials) { - super(null); + super((Collection) null); this.principal = aPrincipal; this.credentials = aCredentials; } @@ -66,6 +67,12 @@ public PreAuthenticatedAuthenticationToken(Object aPrincipal, @Nullable Object a setAuthenticated(true); } + protected PreAuthenticatedAuthenticationToken(Builder builder) { + super(builder); + this.principal = builder.principal; + this.credentials = builder.credentials; + } + /** * Get the credentials */ @@ -82,4 +89,46 @@ public Object getPrincipal() { return this.principal; } + @Override + public Builder toBuilder() { + return new Builder<>(this); + } + + /** + * A builder of {@link PreAuthenticatedAuthenticationToken} instances + * + * @since 7.0 + */ + public static class Builder> extends AbstractAuthenticationBuilder { + + private Object principal; + + private @Nullable Object credentials; + + protected Builder(PreAuthenticatedAuthenticationToken token) { + super(token); + this.principal = token.principal; + this.credentials = token.credentials; + } + + @Override + public B principal(@Nullable Object principal) { + Assert.notNull(principal, "principal cannot be null"); + this.principal = principal; + return (B) this; + } + + @Override + public B credentials(@Nullable Object credentials) { + this.credentials = credentials; + return (B) this; + } + + @Override + public PreAuthenticatedAuthenticationToken build() { + return new PreAuthenticatedAuthenticationToken(this); + } + + } + } 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 98fdd58da1d..91f40b01b7b 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 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse 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 3bbbdb108cf..2f836291817 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 @@ private Mono authenticate(ServerWebExchange exchange, WebFilterChain chain .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 3851311bee2..aaf17d62c94 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 void filterWhenCustomSecurityContextHolderStrategyThenUses() throws Excep 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(); 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 99825bd7d04..fc111528ff1 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 @@ import java.util.Collection; import java.util.List; +import java.util.Set; import org.junit.jupiter.api.Test; @@ -73,4 +74,21 @@ public void testPreAuthenticatedAuthenticationTokenResponse() { .isTrue(); } + @Test + public void toBuilderWhenApplyThenCopies() { + PreAuthenticatedAuthenticationToken factorOne = new PreAuthenticatedAuthenticationToken("alice", "pass", + AuthorityUtils.createAuthorityList("FACTOR_ONE")); + PreAuthenticatedAuthenticationToken factorTwo = new PreAuthenticatedAuthenticationToken("bob", "ssap", + AuthorityUtils.createAuthorityList("FACTOR_TWO")); + PreAuthenticatedAuthenticationToken result = factorOne.toBuilder() + .authorities((a) -> a.addAll(factorTwo.getAuthorities())) + .principal(factorTwo.getPrincipal()) + .credentials(factorTwo.getCredentials()) + .build(); + Set 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/web/src/test/java/org/springframework/security/web/context/HttpSessionSecurityContextRepositoryTests.java b/web/src/test/java/org/springframework/security/web/context/HttpSessionSecurityContextRepositoryTests.java index 979c6c81388..a27873d2308 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.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.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 @@ private SecurityContext createSecurityContext(UserDetails userDetails) { private static class SomeTransientAuthentication extends AbstractAuthenticationToken { SomeTransientAuthentication() { - super(null); + super((Collection) null); } @Override @@ -840,7 +842,7 @@ private static class SomeTransientAuthenticationSubclass extends SomeTransientAu 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 42007f9ece9..bb5b462e927 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 @@ -48,6 +48,11 @@ public WebAuthnAuthentication(PublicKeyCredentialUserEntity principal, super.setAuthenticated(true); } + private WebAuthnAuthentication(Builder builder) { + super(builder); + this.principal = builder.principal; + } + @Override public void setAuthenticated(boolean authenticated) { Assert.isTrue(!authenticated, "Cannot set this token to trusted"); @@ -69,4 +74,43 @@ public String getName() { return this.principal.getName(); } + @Override + public Builder toBuilder() { + return new Builder<>(this); + } + + /** + * A builder of {@link WebAuthnAuthentication} instances + * + * @since 7.0 + */ + public static final class Builder> extends AbstractAuthenticationBuilder { + + private PublicKeyCredentialUserEntity principal; + + private Builder(WebAuthnAuthentication token) { + super(token); + this.principal = token.principal; + } + + /** + * Use this principal. It must be of type {@link PublicKeyCredentialUserEntity} + * @param principal the principal to use + * @return the {@link Builder} for further configurations + */ + @Override + public B principal(@Nullable Object principal) { + Assert.isInstanceOf(PublicKeyCredentialUserEntity.class, principal, + "principal must be of type PublicKeyCredentialUserEntity"); + this.principal = (PublicKeyCredentialUserEntity) principal; + return (B) this; + } + + @Override + public WebAuthnAuthentication build() { + return new WebAuthnAuthentication(this); + } + + } + } 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 8ad6e92ea45..409e05623e5 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,21 @@ void setAuthenticationWhenFalseThenNotAuthenticated() { assertThat(authentication.isAuthenticated()).isFalse(); } + @Test + void toBuilderWhenApplyThenCopies() { + PublicKeyCredentialUserEntity alice = TestPublicKeyCredentialUserEntities.userEntity().build(); + WebAuthnAuthentication factorOne = new WebAuthnAuthentication(alice, + AuthorityUtils.createAuthorityList("FACTOR_ONE")); + PublicKeyCredentialUserEntity bob = TestPublicKeyCredentialUserEntities.userEntity().build(); + WebAuthnAuthentication factorTwo = new WebAuthnAuthentication(bob, + AuthorityUtils.createAuthorityList("FACTOR_TWO")); + WebAuthnAuthentication result = factorOne.toBuilder() + .authorities((a) -> a.addAll(factorTwo.getAuthorities())) + .principal(factorTwo.getPrincipal()) + .build(); + Set authorities = AuthorityUtils.authorityListToSet(result.getAuthorities()); + assertThat(result.getPrincipal()).isSameAs(factorTwo.getPrincipal()); + assertThat(authorities).containsExactlyInAnyOrder("FACTOR_ONE", "FACTOR_TWO"); + } + }