mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-13 17:05:43 +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 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;
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 --------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user