diff --git a/core/src/main/java/org/springframework/security/authentication/AbstractAuthenticationToken.java b/core/src/main/java/org/springframework/security/authentication/AbstractAuthenticationToken.java index 8d3eb1e759..18307f8b64 100644 --- a/core/src/main/java/org/springframework/security/authentication/AbstractAuthenticationToken.java +++ b/core/src/main/java/org/springframework/security/authentication/AbstractAuthenticationToken.java @@ -197,14 +197,22 @@ public abstract class AbstractAuthenticationToken implements Authentication, Cre return sb.toString(); } + /** + * A common abstract implementation of {@link Authentication.Builder}. It implements + * the builder methods that correspond to the {@link Authentication} methods that + * {@link AbstractAuthenticationToken} implements + * + * @param + * @since 7.0 + */ protected abstract static class AbstractAuthenticationBuilder> implements Authentication.Builder { - protected boolean authenticated; + private boolean authenticated; - protected @Nullable Object details; + private @Nullable Object details; - protected final Collection authorities; + private final Collection authorities; protected AbstractAuthenticationBuilder(AbstractAuthenticationToken token) { this.authorities = new LinkedHashSet<>(token.getAuthorities()); diff --git a/core/src/main/java/org/springframework/security/authentication/RememberMeAuthenticationToken.java b/core/src/main/java/org/springframework/security/authentication/RememberMeAuthenticationToken.java index 1ecb5f851f..81d7edeafa 100644 --- a/core/src/main/java/org/springframework/security/authentication/RememberMeAuthenticationToken.java +++ b/core/src/main/java/org/springframework/security/authentication/RememberMeAuthenticationToken.java @@ -20,7 +20,6 @@ import java.util.Collection; import org.jspecify.annotations.Nullable; -import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.util.Assert; @@ -99,8 +98,8 @@ public class RememberMeAuthenticationToken extends AbstractAuthenticationToken { } @Override - public Builder toBuilder() { - return new Builder(this); + public Builder toBuilder() { + return new Builder<>(this); } @Override @@ -122,7 +121,7 @@ public class RememberMeAuthenticationToken extends AbstractAuthenticationToken { } /** - * A builder preserving the concrete {@link Authentication} type + * A builder of {@link RememberMeAuthenticationToken} instances * * @since 7.0 */ @@ -145,8 +144,13 @@ public class RememberMeAuthenticationToken extends AbstractAuthenticationToken { return (B) this; } - public B keyHash(int keyHash) { - this.keyHash = keyHash; + /** + * Use this key + * @param key the key to use + * @return the {@link Builder} for further configurations + */ + public B key(String key) { + this.keyHash = key.hashCode(); return (B) this; } diff --git a/core/src/main/java/org/springframework/security/authentication/TestingAuthenticationToken.java b/core/src/main/java/org/springframework/security/authentication/TestingAuthenticationToken.java index f60bb3b1b3..70778db2a4 100644 --- a/core/src/main/java/org/springframework/security/authentication/TestingAuthenticationToken.java +++ b/core/src/main/java/org/springframework/security/authentication/TestingAuthenticationToken.java @@ -21,7 +21,6 @@ import java.util.List; import org.jspecify.annotations.Nullable; -import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.util.Assert; @@ -87,7 +86,7 @@ public class TestingAuthenticationToken extends AbstractAuthenticationToken { } /** - * A builder preserving the concrete {@link Authentication} type + * A builder of {@link TestingAuthenticationToken} instances * * @since 7.0 */ diff --git a/core/src/main/java/org/springframework/security/authentication/UsernamePasswordAuthenticationToken.java b/core/src/main/java/org/springframework/security/authentication/UsernamePasswordAuthenticationToken.java index 024b6bb1ae..c63e5dfb34 100644 --- a/core/src/main/java/org/springframework/security/authentication/UsernamePasswordAuthenticationToken.java +++ b/core/src/main/java/org/springframework/security/authentication/UsernamePasswordAuthenticationToken.java @@ -20,7 +20,6 @@ import java.util.Collection; import org.jspecify.annotations.Nullable; -import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.util.Assert; @@ -137,15 +136,15 @@ public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationT } /** - * A builder preserving the concrete {@link Authentication} type + * A builder of {@link UsernamePasswordAuthenticationToken} instances * * @since 7.0 */ public static class Builder> extends AbstractAuthenticationBuilder { - protected @Nullable Object principal; + private @Nullable Object principal; - protected @Nullable Object credentials; + private @Nullable Object credentials; protected Builder(UsernamePasswordAuthenticationToken token) { super(token); diff --git a/core/src/main/java/org/springframework/security/authentication/jaas/JaasAuthenticationToken.java b/core/src/main/java/org/springframework/security/authentication/jaas/JaasAuthenticationToken.java index a1dd08a37b..9e1200a58f 100644 --- a/core/src/main/java/org/springframework/security/authentication/jaas/JaasAuthenticationToken.java +++ b/core/src/main/java/org/springframework/security/authentication/jaas/JaasAuthenticationToken.java @@ -23,9 +23,7 @@ import javax.security.auth.login.LoginContext; import org.jspecify.annotations.Nullable; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; -import org.springframework.util.Assert; /** * UsernamePasswordAuthenticationToken extension to carry the Jaas LoginContext that the @@ -65,7 +63,7 @@ public class JaasAuthenticationToken extends UsernamePasswordAuthenticationToken } /** - * A builder preserving the concrete {@link Authentication} type + * A builder of {@link JaasAuthenticationToken} instances * * @since 7.0 */ @@ -81,7 +79,7 @@ public class JaasAuthenticationToken extends UsernamePasswordAuthenticationToken /** * Use this {@link LoginContext} * @param loginContext the {@link LoginContext} to use - * @return the {@link Builder} for further configuration + * @return the {@link Builder} for further configurations */ public B loginContext(LoginContext loginContext) { this.loginContext = loginContext; @@ -90,7 +88,6 @@ public class JaasAuthenticationToken extends UsernamePasswordAuthenticationToken @Override public JaasAuthenticationToken build() { - Assert.notNull(this.principal, "principal cannot be null"); return new JaasAuthenticationToken(this); } diff --git a/core/src/main/java/org/springframework/security/authentication/ott/OneTimeTokenAuthentication.java b/core/src/main/java/org/springframework/security/authentication/ott/OneTimeTokenAuthentication.java index c2a8812724..2b961f3c32 100644 --- a/core/src/main/java/org/springframework/security/authentication/ott/OneTimeTokenAuthentication.java +++ b/core/src/main/java/org/springframework/security/authentication/ott/OneTimeTokenAuthentication.java @@ -65,7 +65,9 @@ public class OneTimeTokenAuthentication extends AbstractAuthenticationToken { } /** - * A builder for constructing a {@link OneTimeTokenAuthentication} instance + * A builder of {@link OneTimeTokenAuthentication} instances + * + * @since 7.0 */ public static class Builder> extends AbstractAuthenticationBuilder { @@ -77,7 +79,7 @@ public class OneTimeTokenAuthentication extends AbstractAuthenticationToken { } /** - * Use this principal + * Use this principal. * @return the {@link Builder} for further configuration */ @Override diff --git a/core/src/main/java/org/springframework/security/core/Authentication.java b/core/src/main/java/org/springframework/security/core/Authentication.java index eea57506e9..af581bc9ba 100644 --- a/core/src/main/java/org/springframework/security/core/Authentication.java +++ b/core/src/main/java/org/springframework/security/core/Authentication.java @@ -138,7 +138,19 @@ public interface Authentication extends Principal, Serializable { void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException; /** - * Return an {@link Builder} based on this instance + * Return an {@link Builder} based on this instance. By default, returns a builder + * that builds a {@link SimpleAuthentication}. + *

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

+ *

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

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

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

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

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

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

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

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

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

+ *

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

+ * @param principal the principal to use + * @return the {@link Builder} for additional configuration + * @see Authentication#getPrincipal + */ B principal(@Nullable Object principal); + /** + * Mark this authentication as authenticated or not + * @param authenticated whether this is an authenticated {@link Authentication} + * instance + * @return the {@link Builder} for additional configuration + * @see Authentication#isAuthenticated + */ B authenticated(boolean authenticated); /** diff --git a/core/src/test/java/org/springframework/security/authentication/AbstractAuthenticationBuilderTests.java b/core/src/test/java/org/springframework/security/authentication/AbstractAuthenticationBuilderTests.java index de88eaf431..6bd66b7166 100644 --- a/core/src/test/java/org/springframework/security/authentication/AbstractAuthenticationBuilderTests.java +++ b/core/src/test/java/org/springframework/security/authentication/AbstractAuthenticationBuilderTests.java @@ -18,10 +18,8 @@ package org.springframework.security.authentication; import java.util.Set; -import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; -import org.springframework.security.authentication.AbstractAuthenticationToken.AbstractAuthenticationBuilder; import org.springframework.security.core.Authentication; import org.springframework.security.core.authority.AuthorityUtils; @@ -40,20 +38,15 @@ class AbstractAuthenticationBuilderTests { } private static final class TestAbstractAuthenticationBuilder - extends AbstractAuthenticationBuilder { + extends TestingAuthenticationToken.Builder { private TestAbstractAuthenticationBuilder(TestingAuthenticationToken token) { super(token); } - @Override - public TestAbstractAuthenticationBuilder principal(@Nullable Object principal) { - return this; - } - @Override public TestingAuthenticationToken build() { - return new TestingAuthenticationToken("user", "password", this.authorities); + return new TestingAuthenticationToken(this); } } diff --git a/core/src/test/java/org/springframework/security/authentication/rememberme/RememberMeAuthenticationTokenTests.java b/core/src/test/java/org/springframework/security/authentication/rememberme/RememberMeAuthenticationTokenTests.java index 9e8f123445..d40954b950 100644 --- a/core/src/test/java/org/springframework/security/authentication/rememberme/RememberMeAuthenticationTokenTests.java +++ b/core/src/test/java/org/springframework/security/authentication/rememberme/RememberMeAuthenticationTokenTests.java @@ -18,6 +18,7 @@ package org.springframework.security.authentication.rememberme; import java.util.Arrays; import java.util.List; +import java.util.Set; import org.junit.jupiter.api.Test; @@ -25,6 +26,7 @@ import org.springframework.security.authentication.RememberMeAuthenticationToken import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.core.userdetails.PasswordEncodedUser; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; @@ -96,4 +98,21 @@ public class RememberMeAuthenticationTokenTests { assertThat(!token.isAuthenticated()).isTrue(); } + @Test + public void toBuilderWhenApplyThenCopies() { + RememberMeAuthenticationToken factorOne = new RememberMeAuthenticationToken("key", PasswordEncodedUser.user(), + AuthorityUtils.createAuthorityList("FACTOR_ONE")); + RememberMeAuthenticationToken factorTwo = new RememberMeAuthenticationToken("yek", PasswordEncodedUser.admin(), + AuthorityUtils.createAuthorityList("FACTOR_TWO")); + RememberMeAuthenticationToken authentication = factorOne.toBuilder() + .authorities((a) -> a.addAll(factorTwo.getAuthorities())) + .key("yek") + .principal(factorTwo.getPrincipal()) + .build(); + Set authorities = AuthorityUtils.authorityListToSet(authentication.getAuthorities()); + assertThat(authentication.getKeyHash()).isEqualTo(factorTwo.getKeyHash()); + assertThat(authentication.getPrincipal()).isEqualTo(factorTwo.getPrincipal()); + assertThat(authorities).containsExactlyInAnyOrder("FACTOR_ONE", "FACTOR_TWO"); + } + }