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 static com.google.common.base.Preconditions.checkArgument;
import java.util.Set;
//~--- JDK imports ------------------------------------------------------------
import javax.inject.Inject;
import javax.inject.Singleton;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Realm for authentication with {@link BearerAuthenticationToken}.
@@ -67,6 +70,11 @@ import javax.inject.Singleton;
public class BearerRealm extends AuthenticatingRealm
{
/**
* the logger for BearerRealm
*/
private static final Logger LOG = LoggerFactory.getLogger(BearerRealm.class);
/** realm name */
@VisibleForTesting
static final String REALM = "BearerRealm";
@@ -80,13 +88,16 @@ public class BearerRealm extends AuthenticatingRealm
* @param resolver key resolver
* @param userDAO user dao
* @param groupDAO group dao
* @param validators token claims validators
*/
@Inject
public BearerRealm(SecureKeyResolver resolver, UserDAO userDAO,
GroupDAO groupDAO)
GroupDAO groupDAO, Set<TokenClaimsValidator> validators)
{
this.resolver = resolver;
this.helper = new DAORealmHelper(REALM, userDAO, groupDAO);
this.validators = validators;
setCredentialsMatcher(new AllowAllCredentialsMatcher());
setAuthenticationTokenClass(BearerAuthenticationToken.class);
}
@@ -135,6 +146,14 @@ public class BearerRealm extends AuthenticatingRealm
.parseClaimsJws(token.getCredentials())
.getBody();
//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)
{
@@ -146,6 +165,9 @@ public class BearerRealm extends AuthenticatingRealm
//~--- fields ---------------------------------------------------------------
/** token claims validators **/
private final Set<TokenClaimsValidator> validators;
/** dao realm helper */
private final DAORealmHelper helper;

View File

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

View File

@@ -35,6 +35,7 @@ package sonia.scm.security;
//~--- non-JDK imports --------------------------------------------------------
import com.google.common.collect.Sets;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwsHeader;
import io.jsonwebtoken.Jwts;
@@ -66,8 +67,13 @@ import static org.mockito.Mockito.*;
import java.security.SecureRandom;
import java.util.Date;
import java.util.Set;
import javax.crypto.spec.SecretKeySpec;
import org.hamcrest.Matchers;
import org.junit.Rule;
import org.junit.rules.ExpectedException;
import org.mockito.Mockito;
/**
*
@@ -77,6 +83,9 @@ import javax.crypto.spec.SecretKeySpec;
public class BearerRealmTest
{
@Rule
public ExpectedException expectedException = ExpectedException.none();
/**
* Method description
*
@@ -105,6 +114,32 @@ public class BearerRealmTest
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
*
@@ -176,7 +211,9 @@ public class BearerRealmTest
@Before
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 --------------------------------------------------------------
@@ -261,6 +298,9 @@ public class BearerRealmTest
/** Field description */
private final SecureRandom random = new SecureRandom();
@Mock
private TokenClaimsValidator validator;
/** Field description */
@Mock
private GroupDAO groupDAO;

View File

@@ -35,6 +35,7 @@ package sonia.scm.security;
//~--- non-JDK imports --------------------------------------------------------
import com.google.common.collect.Sets;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
@@ -57,6 +58,7 @@ import static org.mockito.Mockito.*;
//~--- JDK imports ------------------------------------------------------------
import java.security.SecureRandom;
import java.util.Set;
/**
*
@@ -89,6 +91,7 @@ public class BearerTokenGeneratorTest
assertEquals(trillian.getName(), claims.getSubject());
assertEquals("sid", claims.getId());
assertEquals("123", claims.get("abc"));
}
//~--- set methods ----------------------------------------------------------
@@ -100,7 +103,9 @@ public class BearerTokenGeneratorTest
@Before
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 --------------------------------------------------------------