replace TokenClaimsValidator with not so generic AccessTokenValidator interface and fixed duplicated code of BearerRealm and JwtAccessTokenResolve

This commit is contained in:
Sebastian Sdorra
2018-12-21 08:35:18 +01:00
parent 46d6e88530
commit ac4a57f2f3
9 changed files with 187 additions and 421 deletions

View File

@@ -29,271 +29,98 @@
*
*/
package sonia.scm.security;
//~--- non-JDK imports --------------------------------------------------------
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Sets;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwsHeader;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.PrincipalCollection;
import org.hamcrest.Matchers;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
import sonia.scm.group.GroupDAO;
import sonia.scm.user.User;
import sonia.scm.user.UserDAO;
import sonia.scm.user.UserTestData;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.stubbing.Answer;
import javax.crypto.spec.SecretKeySpec;
import java.util.Date;
import java.util.Set;
import java.util.HashMap;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.any;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static sonia.scm.security.SecureKeyTestUtil.createSecureKey;
/**
* Unit tests for {@link BearerRealm}.
*
* @author Sebastian Sdorra
*/
@SuppressWarnings("unchecked")
@RunWith(MockitoJUnitRunner.class)
public class BearerRealmTest
{
@Rule
public ExpectedException expectedException = ExpectedException.none();
@ExtendWith(MockitoExtension.class)
class BearerRealmTest {
/**
* Method description
*
*/
@Test
public void testDoGetAuthenticationInfo()
{
SecureKey key = createSecureKey();
@Mock
private DAORealmHelperFactory realmHelperFactory;
User marvin = UserTestData.createMarvin();
@Mock
private DAORealmHelper realmHelper;
when(userDAO.get(marvin.getName())).thenReturn(marvin);
@Mock
private AccessTokenResolver accessTokenResolver;
resolveKey(key);
String compact = createCompactToken(marvin.getName(), key);
BearerToken token = BearerToken.valueOf(compact);
AuthenticationInfo info = realm.doGetAuthenticationInfo(token);
assertNotNull(info);
PrincipalCollection principals = info.getPrincipals();
assertEquals(marvin.getName(), principals.getPrimaryPrincipal());
assertEquals(marvin, principals.oneByType(User.class));
assertNotNull(principals.oneByType(Scope.class));
assertTrue(principals.oneByType(Scope.class).isEmpty());
}
/**
* Test {@link BearerRealm#doGetAuthenticationInfo(AuthenticationToken)} with scope.
*
*/
@Test
public void testDoGetAuthenticationInfoWithScope()
{
SecureKey key = createSecureKey();
User marvin = UserTestData.createMarvin();
when(userDAO.get(marvin.getName())).thenReturn(marvin);
resolveKey(key);
String compact = createCompactToken(
marvin.getName(),
key,
new Date(System.currentTimeMillis() + 60000),
Scope.valueOf("repo:*", "user:*")
);
AuthenticationInfo info = realm.doGetAuthenticationInfo(BearerToken.valueOf(compact));
Scope scope = info.getPrincipals().oneByType(Scope.class);
assertThat(scope, Matchers.containsInAnyOrder("repo:*", "user:*"));
}
/**
* Test {@link BearerRealm#doGetAuthenticationInfo(AuthenticationToken)} with a failed
* claims validation.
*/
@Test
public void testDoGetAuthenticationInfoWithInvalidClaims()
{
SecureKey key = createSecureKey();
User marvin = UserTestData.createMarvin();
resolveKey(key);
String compact = createCompactToken(marvin.getName(), key);
// treat claims as invalid
when(validator.validate(Mockito.anyMap())).thenReturn(false);
// expect exception
expectedException.expect(AuthenticationException.class);
expectedException.expectMessage(Matchers.containsString("claims"));
// kick authentication
realm.doGetAuthenticationInfo(BearerToken.valueOf(compact));
}
/**
* Method description
*
*/
@Test(expected = AuthenticationException.class)
public void testDoGetAuthenticationInfoWithExpiredToken()
{
User trillian = UserTestData.createTrillian();
SecureKey key = createSecureKey();
resolveKey(key);
Date exp = new Date(System.currentTimeMillis() - 600l);
String compact = createCompactToken(trillian.getName(), key, exp, Scope.empty());
realm.doGetAuthenticationInfo(BearerToken.valueOf(compact));
}
/**
* Method description
*
*/
@Test(expected = AuthenticationException.class)
public void testDoGetAuthenticationInfoWithInvalidSignature()
{
resolveKey(createSecureKey());
User trillian = UserTestData.createTrillian();
String compact = createCompactToken(trillian.getName(), createSecureKey());
realm.doGetAuthenticationInfo(BearerToken.valueOf(compact));
}
/**
* Method description
*
*/
@Test(expected = AuthenticationException.class)
public void testDoGetAuthenticationInfoWithoutSignature()
{
String compact = Jwts.builder().setSubject("test").compact();
realm.doGetAuthenticationInfo(BearerToken.valueOf(compact));
}
/**
* Method description
*
*/
@Test(expected = IllegalArgumentException.class)
public void testDoGetAuthenticationInfoWrongToken()
{
realm.doGetAuthenticationInfo(new UsernamePasswordToken("test", "test"));
}
//~--- set methods ----------------------------------------------------------
/**
* Method description
*
*/
@Before
public void setUp()
{
when(validator.validate(Mockito.anyMap())).thenReturn(true);
Set<TokenClaimsValidator> validators = Sets.newHashSet(validator);
realm = new BearerRealm(helperFactory, keyResolver, validators);
}
//~--- methods --------------------------------------------------------------
private String createCompactToken(String subject, SecureKey key) {
return createCompactToken(subject, key, Scope.empty());
}
private String createCompactToken(String subject, SecureKey key, Scope scope) {
return createCompactToken(subject, key, new Date(System.currentTimeMillis() + 60000), scope);
}
private String createCompactToken(String subject, SecureKey key, Date exp, Scope scope) {
return Jwts.builder()
.claim(Scopes.CLAIMS_KEY, ImmutableList.copyOf(scope))
.setSubject(subject)
.setExpiration(exp)
.signWith(SignatureAlgorithm.HS256, key.getBytes())
.compact();
}
private void resolveKey(SecureKey key) {
when(
keyResolver.resolveSigningKey(
any(JwsHeader.class),
any(Claims.class)
)
)
.thenReturn(
new SecretKeySpec(
key.getBytes(),
SignatureAlgorithm.HS256.getJcaName()
)
);
}
//~--- fields ---------------------------------------------------------------
@InjectMocks
private DAORealmHelperFactory helperFactory;
@Mock
private LoginAttemptHandler loginAttemptHandler;
@Mock
private TokenClaimsValidator validator;
/** Field description */
@Mock
private GroupDAO groupDAO;
/** Field description */
@Mock
private SecureKeyResolver keyResolver;
/** Field description */
private BearerRealm realm;
/** Field description */
@Mock
private UserDAO userDAO;
private AuthenticationInfo authenticationInfo;
@BeforeEach
void prepareObjectUnderTest() {
when(realmHelperFactory.create(BearerRealm.REALM)).thenReturn(realmHelper);
realm = new BearerRealm(realmHelperFactory, accessTokenResolver);
}
@Test
void shouldDoGetAuthentication() {
BearerToken bearerToken = BearerToken.valueOf("__bearer__");
AccessToken accessToken = mock(AccessToken.class);
when(accessToken.getSubject()).thenReturn("trillian");
when(accessToken.getClaims()).thenReturn(new HashMap<>());
when(accessTokenResolver.resolve(bearerToken)).thenReturn(accessToken);
// we have to use answer, because we could not mock the result of Scopes
when(realmHelper.getAuthenticationInfo(
anyString(), anyString(), any(Scope.class)
)).thenAnswer(createAnswer("trillian", "__bearer__", true));
AuthenticationInfo result = realm.doGetAuthenticationInfo(bearerToken);
assertThat(result).isSameAs(authenticationInfo);
}
@Test
void shouldThrowIllegalArgumentExceptionForWrongTypeOfToken() {
assertThrows(IllegalArgumentException.class, () -> realm.doGetAuthenticationInfo(new UsernamePasswordToken()));
}
private Answer<AuthenticationInfo> createAnswer(String expectedSubject, String expectedCredentials, boolean scopeEmpty) {
return (iom) -> {
String subject = iom.getArgument(0);
assertThat(subject).isEqualTo(expectedSubject);
String credentials = iom.getArgument(1);
assertThat(credentials).isEqualTo(expectedCredentials);
Scope scope = iom.getArgument(2);
assertThat(scope.isEmpty()).isEqualTo(scopeEmpty);
return authenticationInfo;
};
}
private class MyAnswer implements Answer<AuthenticationInfo> {
@Override
public AuthenticationInfo answer(InvocationOnMock invocationOnMock) throws Throwable {
return null;
}
}
}