Polish Core Authentication Builders

Issue gh-17861
This commit is contained in:
Josh Cummings 2025-09-09 14:06:03 -06:00
parent 18fbf88993
commit c66a028332
9 changed files with 120 additions and 32 deletions

View File

@ -197,14 +197,22 @@ public abstract class AbstractAuthenticationToken implements Authentication, Cre
return sb.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 <B>
* @since 7.0
*/
protected abstract static class AbstractAuthenticationBuilder<B extends AbstractAuthenticationBuilder<B>> protected abstract static class AbstractAuthenticationBuilder<B extends AbstractAuthenticationBuilder<B>>
implements Authentication.Builder<B> { implements Authentication.Builder<B> {
protected boolean authenticated; private boolean authenticated;
protected @Nullable Object details; private @Nullable Object details;
protected final Collection<GrantedAuthority> authorities; private final Collection<GrantedAuthority> authorities;
protected AbstractAuthenticationBuilder(AbstractAuthenticationToken token) { protected AbstractAuthenticationBuilder(AbstractAuthenticationToken token) {
this.authorities = new LinkedHashSet<>(token.getAuthorities()); this.authorities = new LinkedHashSet<>(token.getAuthorities());

View File

@ -20,7 +20,6 @@ import java.util.Collection;
import org.jspecify.annotations.Nullable; import org.jspecify.annotations.Nullable;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.GrantedAuthority;
import org.springframework.util.Assert; import org.springframework.util.Assert;
@ -99,8 +98,8 @@ public class RememberMeAuthenticationToken extends AbstractAuthenticationToken {
} }
@Override @Override
public Builder toBuilder() { public Builder<?> toBuilder() {
return new Builder(this); return new Builder<>(this);
} }
@Override @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 * @since 7.0
*/ */
@ -145,8 +144,13 @@ public class RememberMeAuthenticationToken extends AbstractAuthenticationToken {
return (B) this; 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; return (B) this;
} }

View File

@ -21,7 +21,6 @@ import java.util.List;
import org.jspecify.annotations.Nullable; import org.jspecify.annotations.Nullable;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.util.Assert; 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 * @since 7.0
*/ */

View File

@ -20,7 +20,6 @@ import java.util.Collection;
import org.jspecify.annotations.Nullable; import org.jspecify.annotations.Nullable;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.GrantedAuthority;
import org.springframework.util.Assert; 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 * @since 7.0
*/ */
public static class Builder<B extends Builder<B>> extends AbstractAuthenticationBuilder<B> { public static class Builder<B extends Builder<B>> extends AbstractAuthenticationBuilder<B> {
protected @Nullable Object principal; private @Nullable Object principal;
protected @Nullable Object credentials; private @Nullable Object credentials;
protected Builder(UsernamePasswordAuthenticationToken token) { protected Builder(UsernamePasswordAuthenticationToken token) {
super(token); super(token);

View File

@ -23,9 +23,7 @@ import javax.security.auth.login.LoginContext;
import org.jspecify.annotations.Nullable; import org.jspecify.annotations.Nullable;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.GrantedAuthority;
import org.springframework.util.Assert;
/** /**
* UsernamePasswordAuthenticationToken extension to carry the Jaas LoginContext that the * 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 * @since 7.0
*/ */
@ -81,7 +79,7 @@ public class JaasAuthenticationToken extends UsernamePasswordAuthenticationToken
/** /**
* Use this {@link LoginContext} * Use this {@link LoginContext}
* @param loginContext the {@link LoginContext} to use * @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) { public B loginContext(LoginContext loginContext) {
this.loginContext = loginContext; this.loginContext = loginContext;
@ -90,7 +88,6 @@ public class JaasAuthenticationToken extends UsernamePasswordAuthenticationToken
@Override @Override
public JaasAuthenticationToken build() { public JaasAuthenticationToken build() {
Assert.notNull(this.principal, "principal cannot be null");
return new JaasAuthenticationToken(this); return new JaasAuthenticationToken(this);
} }

View File

@ -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<B extends Builder<B>> extends AbstractAuthenticationBuilder<B> { public static class Builder<B extends Builder<B>> extends AbstractAuthenticationBuilder<B> {
@ -77,7 +79,7 @@ public class OneTimeTokenAuthentication extends AbstractAuthenticationToken {
} }
/** /**
* Use this principal * Use this principal.
* @return the {@link Builder} for further configuration * @return the {@link Builder} for further configuration
*/ */
@Override @Override

View File

@ -138,7 +138,19 @@ public interface Authentication extends Principal, Serializable {
void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException; 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}.
* <p>
* 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.
* </p>
* <p>
* 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.
* </p>
* @return an {@link Builder} for building a new {@link Authentication} based on this * @return an {@link Builder} for building a new {@link Authentication} based on this
* instance * instance
* @since 7.0 * @since 7.0
@ -155,17 +167,72 @@ public interface Authentication extends Principal, Serializable {
*/ */
interface Builder<B extends Builder<B>> { interface Builder<B extends Builder<B>> {
/**
* Mutate the authorities with this {@link Consumer}.
* <p>
* 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.
* </p>
* @param authorities a consumer that receives the full set of authorities
* @return the {@link Builder} for additional configuration
* @see Authentication#getAuthorities
*/
B authorities(Consumer<Collection<GrantedAuthority>> authorities); B authorities(Consumer<Collection<GrantedAuthority>> authorities);
/**
* Use this credential.
* <p>
* 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.
* </p>
* @param credentials the credentials to use
* @return the {@link Builder} for additional configuration
* @see Authentication#getCredentials
*/
default B credentials(@Nullable Object credentials) { default B credentials(@Nullable Object credentials) {
throw new UnsupportedOperationException( throw new UnsupportedOperationException(
String.format("%s does not store credentials", this.getClass().getSimpleName())); String.format("%s does not store credentials", this.getClass().getSimpleName()));
} }
/**
* Use this details object.
* <p>
* Implementations may choose to use these {@code details} in combination with any
* principal from the pre-existing {@link Authentication} instance.
* </p>
* @param details the details to use
* @return the {@link Builder} for additional configuration
* @see Authentication#getDetails
*/
B details(@Nullable Object details); B details(@Nullable Object details);
/**
* Use this principal.
* <p>
* 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.
* </p>
* <p>
* Implementations may choose to use this {@code principal} in combination with
* any principal from the pre-existing {@link Authentication} instance.
* </p>
* @param principal the principal to use
* @return the {@link Builder} for additional configuration
* @see Authentication#getPrincipal
*/
B principal(@Nullable Object principal); 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); B authenticated(boolean authenticated);
/** /**

View File

@ -18,10 +18,8 @@ package org.springframework.security.authentication;
import java.util.Set; import java.util.Set;
import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.security.authentication.AbstractAuthenticationToken.AbstractAuthenticationBuilder;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.authority.AuthorityUtils;
@ -40,20 +38,15 @@ class AbstractAuthenticationBuilderTests {
} }
private static final class TestAbstractAuthenticationBuilder private static final class TestAbstractAuthenticationBuilder
extends AbstractAuthenticationBuilder<TestAbstractAuthenticationBuilder> { extends TestingAuthenticationToken.Builder<TestAbstractAuthenticationBuilder> {
private TestAbstractAuthenticationBuilder(TestingAuthenticationToken token) { private TestAbstractAuthenticationBuilder(TestingAuthenticationToken token) {
super(token); super(token);
} }
@Override
public TestAbstractAuthenticationBuilder principal(@Nullable Object principal) {
return this;
}
@Override @Override
public TestingAuthenticationToken build() { public TestingAuthenticationToken build() {
return new TestingAuthenticationToken("user", "password", this.authorities); return new TestingAuthenticationToken(this);
} }
} }

View File

@ -18,6 +18,7 @@ package org.springframework.security.authentication.rememberme;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Set;
import org.junit.jupiter.api.Test; 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.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils; 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.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
@ -96,4 +98,21 @@ public class RememberMeAuthenticationTokenTests {
assertThat(!token.isAuthenticated()).isTrue(); 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<String> authorities = AuthorityUtils.authorityListToSet(authentication.getAuthorities());
assertThat(authentication.getKeyHash()).isEqualTo(factorTwo.getKeyHash());
assertThat(authentication.getPrincipal()).isEqualTo(factorTwo.getPrincipal());
assertThat(authorities).containsExactlyInAnyOrder("FACTOR_ONE", "FACTOR_TWO");
}
} }