introduce TokenClaimsEnricher and TokenClaimsValidator api

This commit is contained in:
Sebastian Sdorra
2017-01-12 22:04:19 +01:00
parent 0a22bc9919
commit 46d8b58810
6 changed files with 201 additions and 10 deletions

View File

@@ -0,0 +1,53 @@
/**
* Copyright (c) 2014, Sebastian Sdorra
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. Neither the name of SCM-Manager; nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* http://bitbucket.org/sdorra/scm-manager
*
*/
package sonia.scm.security;
import java.util.Map;
import sonia.scm.plugin.ExtensionPoint;
/**
* TokenClaimsEnricher is able to modify the claims of a JWT token, before it is delivered to the client.
* TokenClaimsEnricher can be used to add custom values to the token claim.
*
* @author Sebastian Sdorra
* @since 2.0.0
*/
@ExtensionPoint
public interface TokenClaimsEnricher {
/**
* Modify the token claims.
*
* @param claims token claims
*/
void enrich(Map<String, Object> claims);
}

View File

@@ -0,0 +1,55 @@
/**
* Copyright (c) 2014, Sebastian Sdorra
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. Neither the name of SCM-Manager; nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* http://bitbucket.org/sdorra/scm-manager
*
*/
package sonia.scm.security;
import java.util.Map;
import sonia.scm.plugin.ExtensionPoint;
/**
* Validates the claims of a jwt token. The validator is called durring authentication
* with a jwt token.
*
* @author Sebastian Sdorra
* @since 2.0.0
*/
@ExtensionPoint
public interface TokenClaimsValidator {
/**
* Returns {@code true} if the claims is valid. If the token is not valid and the
* method returns {@code false}, the authentication is treated as failed.
*
* @param claims token claims
*
* @return {@code true} if the claims is valid
*/
boolean validate(Map<String, Object> claims);
}

View File

@@ -50,11 +50,14 @@ import sonia.scm.plugin.Extension;
import sonia.scm.user.UserDAO; import sonia.scm.user.UserDAO;
import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkArgument;
import java.util.Set;
//~--- JDK imports ------------------------------------------------------------ //~--- JDK imports ------------------------------------------------------------
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Singleton; import javax.inject.Singleton;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** /**
* Realm for authentication with {@link BearerAuthenticationToken}. * Realm for authentication with {@link BearerAuthenticationToken}.
@@ -67,6 +70,11 @@ import javax.inject.Singleton;
public class BearerRealm extends AuthenticatingRealm public class BearerRealm extends AuthenticatingRealm
{ {
/**
* the logger for BearerRealm
*/
private static final Logger LOG = LoggerFactory.getLogger(BearerRealm.class);
/** realm name */ /** realm name */
@VisibleForTesting @VisibleForTesting
static final String REALM = "BearerRealm"; static final String REALM = "BearerRealm";
@@ -80,13 +88,16 @@ public class BearerRealm extends AuthenticatingRealm
* @param resolver key resolver * @param resolver key resolver
* @param userDAO user dao * @param userDAO user dao
* @param groupDAO group dao * @param groupDAO group dao
* @param validators token claims validators
*/ */
@Inject @Inject
public BearerRealm(SecureKeyResolver resolver, UserDAO userDAO, public BearerRealm(SecureKeyResolver resolver, UserDAO userDAO,
GroupDAO groupDAO) GroupDAO groupDAO, Set<TokenClaimsValidator> validators)
{ {
this.resolver = resolver; this.resolver = resolver;
this.helper = new DAORealmHelper(REALM, userDAO, groupDAO); this.helper = new DAORealmHelper(REALM, userDAO, groupDAO);
this.validators = validators;
setCredentialsMatcher(new AllowAllCredentialsMatcher()); setCredentialsMatcher(new AllowAllCredentialsMatcher());
setAuthenticationTokenClass(BearerAuthenticationToken.class); setAuthenticationTokenClass(BearerAuthenticationToken.class);
} }
@@ -135,6 +146,14 @@ public class BearerRealm extends AuthenticatingRealm
.parseClaimsJws(token.getCredentials()) .parseClaimsJws(token.getCredentials())
.getBody(); .getBody();
//J+ //J+
// check all registered claims validators
validators.forEach((validator) -> {
if (!validator.validate(claims)) {
LOG.warn("token claims is invalid, marked by validator {}", validator.getClass());
throw new AuthenticationException("token claims is invalid");
}
});
} }
catch (JwtException ex) catch (JwtException ex)
{ {
@@ -146,6 +165,9 @@ public class BearerRealm extends AuthenticatingRealm
//~--- fields --------------------------------------------------------------- //~--- fields ---------------------------------------------------------------
/** token claims validators **/
private final Set<TokenClaimsValidator> validators;
/** dao realm helper */ /** dao realm helper */
private final DAORealmHelper helper; private final DAORealmHelper helper;

View File

@@ -43,9 +43,13 @@ import sonia.scm.user.User;
import static com.google.common.base.Preconditions.*; import static com.google.common.base.Preconditions.*;
import com.google.common.collect.Maps;
//~--- JDK imports ------------------------------------------------------------ //~--- JDK imports ------------------------------------------------------------
import java.util.Date; import java.util.Date;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import javax.inject.Inject; import javax.inject.Inject;
@@ -58,7 +62,7 @@ import javax.inject.Inject;
*/ */
public final class BearerTokenGenerator public final class BearerTokenGenerator
{ {
/** /**
* the logger for BearerTokenGenerator * the logger for BearerTokenGenerator
*/ */
@@ -73,13 +77,15 @@ public final class BearerTokenGenerator
* *
* @param keyGenerator key generator * @param keyGenerator key generator
* @param keyResolver secure key resolver * @param keyResolver secure key resolver
* @param enrichers token claims modifier
*/ */
@Inject @Inject
public BearerTokenGenerator(KeyGenerator keyGenerator, public BearerTokenGenerator(
SecureKeyResolver keyResolver) KeyGenerator keyGenerator, SecureKeyResolver keyResolver, Set<TokenClaimsEnricher> enrichers
{ ) {
this.keyGenerator = keyGenerator; this.keyGenerator = keyGenerator;
this.keyResolver = keyResolver; this.keyResolver = keyResolver;
this.enrichers = enrichers;
} }
//~--- methods -------------------------------------------------------------- //~--- methods --------------------------------------------------------------
@@ -92,8 +98,7 @@ public final class BearerTokenGenerator
* *
* @return bearer token * @return bearer token
*/ */
public String createBearerToken(User user) public String createBearerToken(User user) {
{
checkNotNull(user, "user is required"); checkNotNull(user, "user is required");
String username = user.getName(); String username = user.getName();
@@ -109,8 +114,16 @@ public final class BearerTokenGenerator
// TODO: should be configurable // TODO: should be configurable
long expiration = TimeUnit.MILLISECONDS.convert(10, TimeUnit.HOURS); long expiration = TimeUnit.MILLISECONDS.convert(10, TimeUnit.HOURS);
Map<String,Object> claim = Maps.newHashMap();
// enrich claims with registered enrichers
enrichers.forEach((enricher) -> {
enricher.enrich(claim);
});
//J- //J-
return Jwts.builder() return Jwts.builder()
.setClaims(claim)
.setSubject(username) .setSubject(username)
.setId(id) .setId(id)
.signWith(SignatureAlgorithm.HS256, key.getBytes()) .signWith(SignatureAlgorithm.HS256, key.getBytes())
@@ -122,6 +135,9 @@ public final class BearerTokenGenerator
//~--- fields --------------------------------------------------------------- //~--- fields ---------------------------------------------------------------
/** token claims modifier **/
private final Set<TokenClaimsEnricher> enrichers;
/** key generator */ /** key generator */
private final KeyGenerator keyGenerator; private final KeyGenerator keyGenerator;

View File

@@ -35,6 +35,7 @@ package sonia.scm.security;
//~--- non-JDK imports -------------------------------------------------------- //~--- non-JDK imports --------------------------------------------------------
import com.google.common.collect.Sets;
import io.jsonwebtoken.Claims; import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwsHeader; import io.jsonwebtoken.JwsHeader;
import io.jsonwebtoken.Jwts; import io.jsonwebtoken.Jwts;
@@ -66,8 +67,13 @@ import static org.mockito.Mockito.*;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.util.Date; import java.util.Date;
import java.util.Set;
import javax.crypto.spec.SecretKeySpec; import javax.crypto.spec.SecretKeySpec;
import org.hamcrest.Matchers;
import org.junit.Rule;
import org.junit.rules.ExpectedException;
import org.mockito.Mockito;
/** /**
* *
@@ -76,6 +82,9 @@ import javax.crypto.spec.SecretKeySpec;
@RunWith(MockitoJUnitRunner.class) @RunWith(MockitoJUnitRunner.class)
public class BearerRealmTest public class BearerRealmTest
{ {
@Rule
public ExpectedException expectedException = ExpectedException.none();
/** /**
* Method description * Method description
@@ -104,6 +113,32 @@ public class BearerRealmTest
assertEquals(marvin.getName(), principals.getPrimaryPrincipal()); assertEquals(marvin.getName(), principals.getPrimaryPrincipal());
assertEquals(marvin, principals.oneByType(User.class)); assertEquals(marvin, principals.oneByType(User.class));
} }
/**
* Test {@link BearerRealm#doGetAuthenticationInfo(AuthenticationToken)} with a failed
* claims validation.
*/
@Test
public void testDoGetAuthenticationInfoWithInvalidClaims()
{
SecureKey key = createSecureKey();
User marvin = UserTestData.createMarvin();
when(userDAO.get(marvin.getName())).thenReturn(marvin);
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(new BearerAuthenticationToken(compact));
}
/** /**
* Method description * Method description
@@ -176,7 +211,9 @@ public class BearerRealmTest
@Before @Before
public void setUp() public void setUp()
{ {
realm = new BearerRealm(keyResolver, userDAO, groupDAO); when(validator.validate(Mockito.anyMap())).thenReturn(true);
Set<TokenClaimsValidator> validators = Sets.newHashSet(validator);
realm = new BearerRealm(keyResolver, userDAO, groupDAO, validators);
} }
//~--- methods -------------------------------------------------------------- //~--- methods --------------------------------------------------------------
@@ -257,10 +294,13 @@ public class BearerRealmTest
} }
//~--- fields --------------------------------------------------------------- //~--- fields ---------------------------------------------------------------
/** Field description */ /** Field description */
private final SecureRandom random = new SecureRandom(); private final SecureRandom random = new SecureRandom();
@Mock
private TokenClaimsValidator validator;
/** Field description */ /** Field description */
@Mock @Mock
private GroupDAO groupDAO; private GroupDAO groupDAO;

View File

@@ -35,6 +35,7 @@ package sonia.scm.security;
//~--- non-JDK imports -------------------------------------------------------- //~--- non-JDK imports --------------------------------------------------------
import com.google.common.collect.Sets;
import io.jsonwebtoken.Claims; import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts; import io.jsonwebtoken.Jwts;
@@ -57,6 +58,7 @@ import static org.mockito.Mockito.*;
//~--- JDK imports ------------------------------------------------------------ //~--- JDK imports ------------------------------------------------------------
import java.security.SecureRandom; import java.security.SecureRandom;
import java.util.Set;
/** /**
* *
@@ -89,6 +91,7 @@ public class BearerTokenGeneratorTest
assertEquals(trillian.getName(), claims.getSubject()); assertEquals(trillian.getName(), claims.getSubject());
assertEquals("sid", claims.getId()); assertEquals("sid", claims.getId());
assertEquals("123", claims.get("abc"));
} }
//~--- set methods ---------------------------------------------------------- //~--- set methods ----------------------------------------------------------
@@ -100,7 +103,9 @@ public class BearerTokenGeneratorTest
@Before @Before
public void setUp() public void setUp()
{ {
tokenGenerator = new BearerTokenGenerator(keyGenerator, keyResolver); Set<TokenClaimsEnricher> enrichers = Sets.newHashSet();
enrichers.add((claims) -> {claims.put("abc", "123");});
tokenGenerator = new BearerTokenGenerator(keyGenerator, keyResolver, enrichers);
} }
//~--- methods -------------------------------------------------------------- //~--- methods --------------------------------------------------------------