From d782959c40be2f2ed7eda1b6a7b296f8b3d1eafd Mon Sep 17 00:00:00 2001 From: Crain-32 <25422785+Crain-32@users.noreply.github.com> Date: Fri, 22 Dec 2023 15:52:28 -0700 Subject: [PATCH] Closes gh-14352 --- .../AbstractAuthenticationToken.java | 5 +- .../AbstractUserDetailsAuthentication.java | 170 ++++++++++++++++++ ...rnamePasswordTypedAuthenticationToken.java | 85 +++++++++ .../security/core/TypedAuthentication.java | 20 +++ ...PasswordTypedAuthenticationTokenTests.java | 75 ++++++++ 5 files changed, 353 insertions(+), 2 deletions(-) create mode 100644 core/src/main/java/org/springframework/security/authentication/AbstractUserDetailsAuthentication.java create mode 100644 core/src/main/java/org/springframework/security/authentication/UsernamePasswordTypedAuthenticationToken.java create mode 100644 core/src/main/java/org/springframework/security/core/TypedAuthentication.java create mode 100644 core/src/test/java/org/springframework/security/authentication/UsernamePasswordTypedAuthenticationTokenTests.java 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 cfd066a912a..3ecf7e98f8a 100644 --- a/core/src/main/java/org/springframework/security/authentication/AbstractAuthenticationToken.java +++ b/core/src/main/java/org/springframework/security/authentication/AbstractAuthenticationToken.java @@ -36,6 +36,7 @@ * * @author Ben Alex * @author Luke Taylor + * @author Peter Eastham */ public abstract class AbstractAuthenticationToken implements Authentication, CredentialsContainer { @@ -112,8 +113,8 @@ public void eraseCredentials() { } private void eraseSecret(Object secret) { - if (secret instanceof CredentialsContainer) { - ((CredentialsContainer) secret).eraseCredentials(); + if (secret instanceof CredentialsContainer container) { + container.eraseCredentials(); } } diff --git a/core/src/main/java/org/springframework/security/authentication/AbstractUserDetailsAuthentication.java b/core/src/main/java/org/springframework/security/authentication/AbstractUserDetailsAuthentication.java new file mode 100644 index 00000000000..663bc5d87ef --- /dev/null +++ b/core/src/main/java/org/springframework/security/authentication/AbstractUserDetailsAuthentication.java @@ -0,0 +1,170 @@ +package org.springframework.security.authentication; + +import org.springframework.security.core.CredentialsContainer; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.TypedAuthentication; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.util.Assert; + +import java.util.Collection; +import java.util.List; + +/** + * Base class for {@link TypedAuthentication} objects, where a {@link UserDetails} is the + * Principal. + *

+ * Implementations which use this class should be immutable. + *

+ * Based on {@link AbstractAuthenticationToken}. + * + * @param The type the Credentials are bound to. + * @param The type the Details are bound to. + * @author Peter Eastham + */ +public abstract class AbstractUserDetailsAuthentication + implements TypedAuthentication, CredentialsContainer { + + private final List authorities; + + private final UserDetails principal; + + private D details; + + private boolean authenticated = false; + + /** + * Creates a token based on a supplied UserDetails Object. + * + * @param principal a nonnull UserDetails for the principal. + * @throws IllegalArgumentException if {@code principal.getAuthorities()} is null, or + * any Authorities in it are null. + */ + public AbstractUserDetailsAuthentication(UserDetails principal) { + Assert.notNull(principal, "Principal Provided cannot be null"); + Assert.notNull(principal.getAuthorities(), "Principal Authorities cannot be null"); + for (GrantedAuthority a : principal.getAuthorities()) { + Assert.notNull(a, "Authorities collection cannot contain any null elements"); + } + this.authorities = List.copyOf(principal.getAuthorities()); + this.principal = principal; + } + + @Override + public Collection getAuthorities() { + return this.authorities; + } + + @Override + public String getName() { + return getPrincipal().getUsername(); + } + + @Override + public boolean isAuthenticated() { + return this.authenticated; + } + + @Override + public void setAuthenticated(boolean authenticated) { + this.authenticated = authenticated; + } + + @Override + public D getDetails() { + return this.details; + } + + public void setDetails(D details) { + this.details = details; + } + + @Override + public UserDetails getPrincipal() { + return this.principal; + } + + /** + * Checks the {@code credentials}, {@code principal} and {@code details} objects, + * invoking the {@code eraseCredentials} method on any which implement + * {@link CredentialsContainer}. + */ + @Override + public void eraseCredentials() { + eraseSecret(getCredentials()); + eraseSecret(this.principal); + eraseSecret(this.details); + } + + private void eraseSecret(Object secret) { + if (secret instanceof CredentialsContainer cc) { + cc.eraseCredentials(); + } + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof AbstractUserDetailsAuthentication test)) { + return false; + } + if (!this.authorities.equals(test.getAuthorities())) { + return false; + } + if ((this.details == null) && (test.getDetails() != null)) { + return false; + } + if ((this.details != null) && (test.getDetails() == null)) { + return false; + } + if ((this.details != null) && (!this.details.equals(test.getDetails()))) { + return false; + } + if ((this.getCredentials() == null) && (test.getCredentials() != null)) { + return false; + } + if ((this.getCredentials() != null) && !this.getCredentials().equals(test.getCredentials())) { + return false; + } + if (this.getPrincipal() == null && test.getPrincipal() != null) { + return false; + } + if (this.getPrincipal() != null && !this.getPrincipal().equals(test.getPrincipal())) { + return false; + } + return this.isAuthenticated() == test.isAuthenticated(); + } + + @Override + public int hashCode() { + int code = 31; + for (GrantedAuthority authority : this.authorities) { + code ^= authority.hashCode(); + } + if (this.getPrincipal() != null) { + code ^= this.getPrincipal().hashCode(); + } + if (this.getCredentials() != null) { + code ^= this.getCredentials().hashCode(); + } + if (this.getDetails() != null) { + code ^= this.getDetails().hashCode(); + } + if (this.isAuthenticated()) { + code ^= -37; + } + return code; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(getClass().getSimpleName()).append(" ["); + sb.append("Principal=").append(getPrincipal()).append(", "); + sb.append("Credentials=[PROTECTED], "); + sb.append("Authenticated=").append(isAuthenticated()).append(", "); + sb.append("Details=").append(getDetails()).append(", "); + sb.append("Granted Authorities=").append(this.authenticated); + sb.append("]"); + return sb.toString(); + } + +} diff --git a/core/src/main/java/org/springframework/security/authentication/UsernamePasswordTypedAuthenticationToken.java b/core/src/main/java/org/springframework/security/authentication/UsernamePasswordTypedAuthenticationToken.java new file mode 100644 index 00000000000..291ff1fcef0 --- /dev/null +++ b/core/src/main/java/org/springframework/security/authentication/UsernamePasswordTypedAuthenticationToken.java @@ -0,0 +1,85 @@ +package org.springframework.security.authentication; + +import org.springframework.security.core.SpringSecurityCoreVersion; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.util.Assert; + +/** + * A {@link AbstractUserDetailsAuthentication} implementation that is designed for simple + * presentation of a username and password. + *

+ * + * @param The Details Object the Token can map to. + * @author Peter Eastham + */ +public class UsernamePasswordTypedAuthenticationToken extends AbstractUserDetailsAuthentication { + + private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; + + private String credentials; + + /** + * Creates a token based on a supplied UserDetails instance and + * String Object. + * + * @param principal a nonnull UserDetails for the principal. + * @throws IllegalArgumentException if {@code principal.getAuthorities()} is null, or + * any Authorities in it are null. + */ + private UsernamePasswordTypedAuthenticationToken(UserDetails principal, String credentials, boolean authenticated) { + super(principal); + this.credentials = credentials; + super.setAuthenticated(authenticated); + } + + /** + * Factory method that support creation of an unauthenticated + * UsernamePasswordTypedAuthenticationToken + * + * @param principal Nonnull UserDetails object to set to the Principal + * @param credentials Nullable String which represents the User's Password + * @param The Type assigned to the TypedAuthentication::getDetails + * method. + * @return UsernamePasswordTypedAuthenticationToken with false + * isAuthenticated() result. + */ + public static UsernamePasswordTypedAuthenticationToken unauthenticated(UserDetails principal, + String credentials) { + return new UsernamePasswordTypedAuthenticationToken<>(principal, credentials, false); + } + + /** + * Factory method that support creation of an unauthenticated + * UsernamePasswordTypedAuthenticationToken + * + * @param principal Nonnull UserDetails object to set to the Principal + * @param credentials Nullable String which represents the User's Password + * @param The Type assigned to the TypedAuthentication::getDetails + * method. + * @return UsernamePasswordTypedAuthenticationToken with true + * isAuthenticated() result. + */ + public static UsernamePasswordTypedAuthenticationToken authenticated(UserDetails principal, + String credentials) { + return new UsernamePasswordTypedAuthenticationToken<>(principal, credentials, true); + } + + @Override + public String getCredentials() { + return this.credentials; + } + + @Override + public void eraseCredentials() { + super.eraseCredentials(); + this.credentials = null; + } + + @Override + public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException { + Assert.isTrue(!isAuthenticated, + "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead"); + super.setAuthenticated(false); + } + +} diff --git a/core/src/main/java/org/springframework/security/core/TypedAuthentication.java b/core/src/main/java/org/springframework/security/core/TypedAuthentication.java new file mode 100644 index 00000000000..ca1d0315bec --- /dev/null +++ b/core/src/main/java/org/springframework/security/core/TypedAuthentication.java @@ -0,0 +1,20 @@ +package org.springframework.security.core; + +/** + * Provided as a wrapper for {@link Authentication} + * + * @author Peter Eastham + * @see Authentication + */ +public interface TypedAuthentication extends Authentication { + + @Override + C getCredentials(); + + @Override + D getDetails(); + + @Override + P getPrincipal(); + +} diff --git a/core/src/test/java/org/springframework/security/authentication/UsernamePasswordTypedAuthenticationTokenTests.java b/core/src/test/java/org/springframework/security/authentication/UsernamePasswordTypedAuthenticationTokenTests.java new file mode 100644 index 00000000000..8af421d46d6 --- /dev/null +++ b/core/src/test/java/org/springframework/security/authentication/UsernamePasswordTypedAuthenticationTokenTests.java @@ -0,0 +1,75 @@ +package org.springframework.security.authentication; + +import org.junit.jupiter.api.Test; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.core.userdetails.User; + +import static org.assertj.core.api.Assertions.*; + +/** + * Tests {@link UsernamePasswordTypedAuthenticationToken}. + * Replicates {@link UsernamePasswordAuthenticationTokenTests} + * @author Peter Eastham + */ +public class UsernamePasswordTypedAuthenticationTokenTests { + + @Test + public void authenticatedPropertyContractIsSatisfied() { + User simpleUser = new User("Test", "Password", AuthorityUtils.NO_AUTHORITIES); + UsernamePasswordTypedAuthenticationToken grantedToken = UsernamePasswordTypedAuthenticationToken + .authenticated(simpleUser, simpleUser.getPassword()); + // check default given we passed some GrantedAuthority[]s (well, we passed empty + // list) + assertThat(grantedToken.isAuthenticated()).isTrue(); + // check explicit set to untrusted (we can safely go from trusted to untrusted, + // but not the reverse) + grantedToken.setAuthenticated(false); + assertThat(!grantedToken.isAuthenticated()).isTrue(); + // Now let's create a UsernamePasswordAuthenticationToken without any + // GrantedAuthority[]s (different constructor) + UsernamePasswordTypedAuthenticationToken noneGrantedToken = UsernamePasswordTypedAuthenticationToken + .unauthenticated(simpleUser, simpleUser.getPassword()); + assertThat(!noneGrantedToken.isAuthenticated()).isTrue(); + // check we're allowed to still set it to untrusted + noneGrantedToken.setAuthenticated(false); + assertThat(!noneGrantedToken.isAuthenticated()).isTrue(); + // check denied changing it to trusted + assertThatIllegalArgumentException().isThrownBy(() -> noneGrantedToken.setAuthenticated(true)); + } + + @Test + public void gettersReturnCorrectData() { + User simpleUser = new User("Test", "Password", AuthorityUtils.createAuthorityList("ROLE_ONE", "ROLE_TWO")); + UsernamePasswordTypedAuthenticationToken token = UsernamePasswordTypedAuthenticationToken + .authenticated(simpleUser, simpleUser.getPassword()); + assertThat(token.getName()).isEqualTo("Test"); + assertThat(token.getPrincipal().getUsername()).isEqualTo("Test"); + assertThat(token.getCredentials()).isEqualTo("Password"); + assertThat(AuthorityUtils.authorityListToSet(token.getAuthorities())).contains("ROLE_ONE"); + assertThat(AuthorityUtils.authorityListToSet(token.getAuthorities())).contains("ROLE_TWO"); + } + + @Test + public void testNoArgConstructorDoesntExist() throws Exception { + Class clazz = UsernamePasswordTypedAuthenticationToken.class; + assertThatExceptionOfType(NoSuchMethodException.class) + .isThrownBy(() -> clazz.getDeclaredConstructor((Class[]) null)); + } + + @Test + public void unauthenticatedFactoryMethodResultsUnauthenticatedToken() { + User simpleUser = new User("Test", "Password", AuthorityUtils.NO_AUTHORITIES); + UsernamePasswordTypedAuthenticationToken grantedToken = UsernamePasswordTypedAuthenticationToken + .unauthenticated(simpleUser, simpleUser.getPassword()); + assertThat(grantedToken.isAuthenticated()).isFalse(); + } + + @Test + public void authenticatedFactoryMethodResultsAuthenticatedToken() { + User simpleUser = new User("Test", "Password", AuthorityUtils.NO_AUTHORITIES); + UsernamePasswordTypedAuthenticationToken grantedToken = UsernamePasswordTypedAuthenticationToken + .authenticated(simpleUser, simpleUser.getPassword()); + assertThat(grantedToken.isAuthenticated()).isTrue(); + } + +}