Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closes gh-14352 #14369

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
*
* @author Ben Alex
* @author Luke Taylor
* @author Peter Eastham
*/
public abstract class AbstractAuthenticationToken implements Authentication, CredentialsContainer {

Expand Down Expand Up @@ -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();
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* Implementations which use this class should be immutable.
* <p>
* Based on {@link AbstractAuthenticationToken}.
*
* @param <C> The type the Credentials are bound to.
* @param <D> The type the Details are bound to.
* @author Peter Eastham
*/
public abstract class AbstractUserDetailsAuthentication<C, D>
implements TypedAuthentication<C, D, UserDetails>, CredentialsContainer {

private final List<? extends GrantedAuthority> authorities;

private final UserDetails principal;

private D details;

private boolean authenticated = false;

/**
* Creates a token based on a supplied <tt>UserDetails</tt> Object.
*
* @param principal a nonnull <tt>UserDetails</tt> 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<? extends GrantedAuthority> 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();
}

}
Original file line number Diff line number Diff line change
@@ -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.
* <p>
*
* @param <D> The Details Object the Token can map to.
* @author Peter Eastham
*/
public class UsernamePasswordTypedAuthenticationToken<D> extends AbstractUserDetailsAuthentication<String, D> {

private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

private String credentials;

/**
* Creates a token based on a supplied <tt>UserDetails</tt> instance and
* <tt>String</tt> Object.
*
* @param principal a nonnull <tt>UserDetails</tt> 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) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another thing to note. I was very torn on if this should be a public or private constructor. In a review of how UsernamePasswordAuthenticationToken is handled, I arrived to the conclusion the maximum visibility should be protected, however with no sub-classes or other reasons to expose it, I left it as private.

super(principal);
this.credentials = credentials;
super.setAuthenticated(authenticated);
}

/**
* Factory method that support creation of an unauthenticated
* <code>UsernamePasswordTypedAuthenticationToken</code>
*
* @param principal Nonnull UserDetails object to set to the Principal
* @param credentials Nullable String which represents the User's Password
* @param <D> The Type assigned to the <code>TypedAuthentication::getDetails</code>
* method.
* @return <code>UsernamePasswordTypedAuthenticationToken</code> with false
* isAuthenticated() result.
*/
public static <D> UsernamePasswordTypedAuthenticationToken<D> unauthenticated(UserDetails principal,
String credentials) {
return new UsernamePasswordTypedAuthenticationToken<>(principal, credentials, false);
}

/**
* Factory method that support creation of an unauthenticated
* <code>UsernamePasswordTypedAuthenticationToken</code>
*
* @param principal Nonnull UserDetails object to set to the Principal
* @param credentials Nullable String which represents the User's Password
* @param <D> The Type assigned to the <code>TypedAuthentication::getDetails</code>
* method.
* @return <code>UsernamePasswordTypedAuthenticationToken</code> with true
* isAuthenticated() result.
*/
public static <D> UsernamePasswordTypedAuthenticationToken<D> 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);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package org.springframework.security.core;

/**
* Provided as a wrapper for {@link Authentication}
*
* @author Peter Eastham
* @see Authentication
*/
public interface TypedAuthentication<C, D, P> extends Authentication {

@Override
C getCredentials();

@Override
D getDetails();

@Override
P getPrincipal();

}
Original file line number Diff line number Diff line change
@@ -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<Object> 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<Object> 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<Object> 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<Object> grantedToken = UsernamePasswordTypedAuthenticationToken
.unauthenticated(simpleUser, simpleUser.getPassword());
assertThat(grantedToken.isAuthenticated()).isFalse();
}

@Test
public void authenticatedFactoryMethodResultsAuthenticatedToken() {
User simpleUser = new User("Test", "Password", AuthorityUtils.NO_AUTHORITIES);
UsernamePasswordTypedAuthenticationToken<Object> grantedToken = UsernamePasswordTypedAuthenticationToken
.authenticated(simpleUser, simpleUser.getPassword());
assertThat(grantedToken.isAuthenticated()).isTrue();
}

}