diff --git a/scm-core/src/main/java/sonia/scm/security/TokenClaimsEnricher.java b/scm-core/src/main/java/sonia/scm/security/TokenClaimsEnricher.java new file mode 100644 index 0000000000..b8d69886b6 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/security/TokenClaimsEnricher.java @@ -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 claims); +} diff --git a/scm-core/src/main/java/sonia/scm/security/TokenClaimsValidator.java b/scm-core/src/main/java/sonia/scm/security/TokenClaimsValidator.java new file mode 100644 index 0000000000..4389e7bfb7 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/security/TokenClaimsValidator.java @@ -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 claims); +} diff --git a/scm-webapp/src/main/java/sonia/scm/security/BearerRealm.java b/scm-webapp/src/main/java/sonia/scm/security/BearerRealm.java index 7dc6c6b03b..8a3109e83a 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/BearerRealm.java +++ b/scm-webapp/src/main/java/sonia/scm/security/BearerRealm.java @@ -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 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 validators; + /** dao realm helper */ private final DAORealmHelper helper; diff --git a/scm-webapp/src/main/java/sonia/scm/security/BearerTokenGenerator.java b/scm-webapp/src/main/java/sonia/scm/security/BearerTokenGenerator.java index 16aacbf8ec..de50f0cc99 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/BearerTokenGenerator.java +++ b/scm-webapp/src/main/java/sonia/scm/security/BearerTokenGenerator.java @@ -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; @@ -58,7 +62,7 @@ import javax.inject.Inject; */ public final class BearerTokenGenerator { - + /** * the logger for BearerTokenGenerator */ @@ -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 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 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 enrichers; + /** key generator */ private final KeyGenerator keyGenerator; diff --git a/scm-webapp/src/test/java/sonia/scm/security/BearerRealmTest.java b/scm-webapp/src/test/java/sonia/scm/security/BearerRealmTest.java index 243e0da8f2..007a0803df 100644 --- a/scm-webapp/src/test/java/sonia/scm/security/BearerRealmTest.java +++ b/scm-webapp/src/test/java/sonia/scm/security/BearerRealmTest.java @@ -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; /** * @@ -76,6 +82,9 @@ import javax.crypto.spec.SecretKeySpec; @RunWith(MockitoJUnitRunner.class) public class BearerRealmTest { + + @Rule + public ExpectedException expectedException = ExpectedException.none(); /** * Method description @@ -104,6 +113,32 @@ public class BearerRealmTest assertEquals(marvin.getName(), principals.getPrimaryPrincipal()); 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 validators = Sets.newHashSet(validator); + realm = new BearerRealm(keyResolver, userDAO, groupDAO, validators); } //~--- methods -------------------------------------------------------------- @@ -257,10 +294,13 @@ public class BearerRealmTest } //~--- fields --------------------------------------------------------------- - + /** Field description */ private final SecureRandom random = new SecureRandom(); + @Mock + private TokenClaimsValidator validator; + /** Field description */ @Mock private GroupDAO groupDAO; diff --git a/scm-webapp/src/test/java/sonia/scm/security/BearerTokenGeneratorTest.java b/scm-webapp/src/test/java/sonia/scm/security/BearerTokenGeneratorTest.java index 7f1b0a6e37..4661d71eac 100644 --- a/scm-webapp/src/test/java/sonia/scm/security/BearerTokenGeneratorTest.java +++ b/scm-webapp/src/test/java/sonia/scm/security/BearerTokenGeneratorTest.java @@ -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 enrichers = Sets.newHashSet(); + enrichers.add((claims) -> {claims.put("abc", "123");}); + tokenGenerator = new BearerTokenGenerator(keyGenerator, keyResolver, enrichers); } //~--- methods --------------------------------------------------------------