mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-14 01:15:44 +01:00
introduce TokenClaimsEnricher and TokenClaimsValidator api
This commit is contained in:
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 --------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user