token enricher should use new access token api

This commit is contained in:
Sebastian Sdorra
2017-01-17 15:33:19 +01:00
parent 2388cfd35d
commit 70d5942250
10 changed files with 68 additions and 70 deletions

View File

@@ -30,24 +30,23 @@
*/ */
package sonia.scm.security; package sonia.scm.security;
import java.util.Map;
import sonia.scm.plugin.ExtensionPoint; import sonia.scm.plugin.ExtensionPoint;
/** /**
* TokenClaimsEnricher is able to modify the claims of a JWT token, before it is delivered to the client. * AccessTokenEnricher is able to enhance the {@link AccessToken}, before it is delivered to the client.
* TokenClaimsEnricher can be used to add custom values to the token claim. * AccessTokenEnricher can be used to add custom fields to the {@link AccessToken}. The enricher is always called before
* an {@link AccessToken} is build by the {@link AccessTokenBuilder}.
* *
* @author Sebastian Sdorra * @author Sebastian Sdorra
* @since 2.0.0 * @since 2.0.0
*/ */
@ExtensionPoint @ExtensionPoint
public interface TokenClaimsEnricher { public interface AccessTokenEnricher {
/** /**
* Modify the token claims. * Enriches the access token by adding fields to the {@link AccessTokenBuilder}.
* *
* @param claims token claims * @param builder access token builder
*/ */
void enrich(Map<String, Object> claims); void enrich(AccessTokenBuilder builder);
} }

View File

@@ -81,6 +81,7 @@ public final class JwtAccessToken implements AccessToken {
} }
@Override @Override
@SuppressWarnings("unchecked")
public Optional<Object> getCustom(String key) { public Optional<Object> getCustom(String key) {
return Optional.ofNullable(claims.get(key)); return Optional.ofNullable(claims.get(key));
} }

View File

@@ -61,7 +61,6 @@ public final class JwtAccessTokenBuilder implements AccessTokenBuilder {
private final KeyGenerator keyGenerator; private final KeyGenerator keyGenerator;
private final SecureKeyResolver keyResolver; private final SecureKeyResolver keyResolver;
private final Set<TokenClaimsEnricher> enrichers;
private String subject; private String subject;
private String issuer; private String issuer;
@@ -71,12 +70,9 @@ public final class JwtAccessTokenBuilder implements AccessTokenBuilder {
private final Map<String,Object> custom = Maps.newHashMap(); private final Map<String,Object> custom = Maps.newHashMap();
JwtAccessTokenBuilder( JwtAccessTokenBuilder(KeyGenerator keyGenerator, SecureKeyResolver keyResolver) {
KeyGenerator keyGenerator, SecureKeyResolver keyResolver, Set<TokenClaimsEnricher> enrichers
) {
this.keyGenerator = keyGenerator; this.keyGenerator = keyGenerator;
this.keyResolver = keyResolver; this.keyResolver = keyResolver;
this.enrichers = enrichers;
} }
@Override @Override
@@ -143,11 +139,6 @@ public final class JwtAccessTokenBuilder implements AccessTokenBuilder {
// add scope to custom claims // add scope to custom claims
Scopes.toClaims(customClaims, scope); Scopes.toClaims(customClaims, scope);
// enrich claims with registered enrichers
enrichers.forEach((enricher) -> {
enricher.enrich(customClaims);
});
Date now = new Date(); Date now = new Date();
long expiration = expiresInUnit.toMillis(expiresIn); long expiration = expiresInUnit.toMillis(expiresIn);

View File

@@ -45,11 +45,11 @@ public final class JwtAccessTokenBuilderFactory implements AccessTokenBuilderFac
private final KeyGenerator keyGenerator; private final KeyGenerator keyGenerator;
private final SecureKeyResolver keyResolver; private final SecureKeyResolver keyResolver;
private final Set<TokenClaimsEnricher> enrichers; private final Set<AccessTokenEnricher> enrichers;
@Inject @Inject
public JwtAccessTokenBuilderFactory( public JwtAccessTokenBuilderFactory(
KeyGenerator keyGenerator, SecureKeyResolver keyResolver, Set<TokenClaimsEnricher> enrichers KeyGenerator keyGenerator, SecureKeyResolver keyResolver, Set<AccessTokenEnricher> enrichers
) { ) {
this.keyGenerator = keyGenerator; this.keyGenerator = keyGenerator;
this.keyResolver = keyResolver; this.keyResolver = keyResolver;
@@ -58,7 +58,14 @@ public final class JwtAccessTokenBuilderFactory implements AccessTokenBuilderFac
@Override @Override
public JwtAccessTokenBuilder create() { public JwtAccessTokenBuilder create() {
return new JwtAccessTokenBuilder(keyGenerator, keyResolver, enrichers); JwtAccessTokenBuilder builder = new JwtAccessTokenBuilder(keyGenerator, keyResolver);
// enrich access token builder
enrichers.forEach((enricher) -> {
enricher.enrich(builder);
});
return builder;
} }
} }

View File

@@ -40,7 +40,7 @@ public final class Xsrf {
static final String HEADER_KEY = "X-XSRF-Token"; static final String HEADER_KEY = "X-XSRF-Token";
static final String CLAIMS_KEY = "xsrf"; static final String TOKEN_KEY = "xsrf";
private Xsrf() { private Xsrf() {
} }

View File

@@ -30,7 +30,7 @@
*/ */
package sonia.scm.security; package sonia.scm.security;
import java.util.Map; import com.google.common.annotations.VisibleForTesting;
import java.util.UUID; import java.util.UUID;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Provider; import javax.inject.Provider;
@@ -42,22 +42,22 @@ import sonia.scm.plugin.Extension;
import sonia.scm.util.HttpUtil; import sonia.scm.util.HttpUtil;
/** /**
* Xsrf token claims enricher will add an xsrf protection key to the claims of the jwt token. The enricher will only * Xsrf access token enricher will add an xsrf custom field to the access token. The enricher will only
* add the xsrf protection key, if the authentication request is issued from the web interface and xsrf protection is * add the xsrf field, if the authentication request is issued from the web interface and xsrf protection is
* enabled. The xsrf key will be validated on every request by the {@link XsrfTokenClaimsValidator}. Xsrf protection is * enabled. The xsrf field will be validated on every request by the {@link XsrfTokenClaimsValidator}. Xsrf protection
* disabled by default and can be enabled with {@link ScmConfiguration#setEnabledXsrfProtection(boolean)}. * can be disabled with {@link ScmConfiguration#setEnabledXsrfProtection(boolean)}.
* *
* @see https://bitbucket.org/sdorra/scm-manager/issues/793/json-hijacking-vulnerability-cwe-116-cwe * @see <a href="https://goo.gl/s67xO3">Issue 793</a>
* @author Sebastian Sdorra * @author Sebastian Sdorra
* @since 2.0.0 * @since 2.0.0
*/ */
@Extension @Extension
public class XsrfTokenClaimsEnricher implements TokenClaimsEnricher { public class XsrfAccessTokenEnricher implements AccessTokenEnricher {
/** /**
* the logger for XsrfTokenClaimsEnricher * the logger for XsrfAccessTokenEnricher
*/ */
private static final Logger LOG = LoggerFactory.getLogger(XsrfTokenClaimsEnricher.class); private static final Logger LOG = LoggerFactory.getLogger(XsrfAccessTokenEnricher.class);
private final ScmConfiguration configuration; private final ScmConfiguration configuration;
private final Provider<HttpServletRequest> requestProvider; private final Provider<HttpServletRequest> requestProvider;
@@ -69,17 +69,17 @@ public class XsrfTokenClaimsEnricher implements TokenClaimsEnricher {
* @param requestProvider http request provider * @param requestProvider http request provider
*/ */
@Inject @Inject
public XsrfTokenClaimsEnricher(ScmConfiguration configuration, Provider<HttpServletRequest> requestProvider) { public XsrfAccessTokenEnricher(ScmConfiguration configuration, Provider<HttpServletRequest> requestProvider) {
this.configuration = configuration; this.configuration = configuration;
this.requestProvider = requestProvider; this.requestProvider = requestProvider;
} }
@Override @Override
public void enrich(Map<String, Object> claims) { public void enrich(AccessTokenBuilder builder) {
if (configuration.isEnabledXsrfProtection()) { if (configuration.isEnabledXsrfProtection()) {
if (HttpUtil.isWUIRequest(requestProvider.get())) { if (HttpUtil.isWUIRequest(requestProvider.get())) {
LOG.debug("received wui token claim, enrich jwt with xsrf key"); LOG.debug("received wui token claim, enrich jwt with xsrf key");
claims.put(Xsrf.CLAIMS_KEY, createToken()); builder.custom(Xsrf.TOKEN_KEY, createToken());
} else { } else {
LOG.trace("skip xsrf enrichment, because jwt session is started from a non wui client"); LOG.trace("skip xsrf enrichment, because jwt session is started from a non wui client");
} }
@@ -88,7 +88,8 @@ public class XsrfTokenClaimsEnricher implements TokenClaimsEnricher {
} }
} }
private String createToken() { @VisibleForTesting
String createToken() {
// TODO create interface and use a better method // TODO create interface and use a better method
return UUID.randomUUID().toString(); return UUID.randomUUID().toString();
} }

View File

@@ -70,7 +70,7 @@ public class XsrfTokenClaimsValidator implements TokenClaimsValidator {
@Override @Override
public boolean validate(Map<String, Object> claims) { public boolean validate(Map<String, Object> claims) {
String xsrfClaimValue = (String) claims.get(Xsrf.CLAIMS_KEY); String xsrfClaimValue = (String) claims.get(Xsrf.TOKEN_KEY);
if (!Strings.isNullOrEmpty(xsrfClaimValue)) { if (!Strings.isNullOrEmpty(xsrfClaimValue)) {
String xsrfHeaderValue = requestProvider.get().getHeader(Xsrf.HEADER_KEY); String xsrfHeaderValue = requestProvider.get().getHeader(Xsrf.HEADER_KEY);
return xsrfClaimValue.equals(xsrfHeaderValue); return xsrfClaimValue.equals(xsrfHeaderValue);

View File

@@ -63,9 +63,9 @@ public class JwtAccessTokenBuilderTest {
@Mock @Mock
private SecureKeyResolver secureKeyResolver; private SecureKeyResolver secureKeyResolver;
private Set<TokenClaimsEnricher> enrichers; private Set<AccessTokenEnricher> enrichers;
private JwtAccessTokenBuilder builder; private JwtAccessTokenBuilderFactory factory;
@Rule @Rule
public ShiroRule shiro = new ShiroRule(); public ShiroRule shiro = new ShiroRule();
@@ -78,8 +78,7 @@ public class JwtAccessTokenBuilderTest {
when(keyGenerator.createKey()).thenReturn("42"); when(keyGenerator.createKey()).thenReturn("42");
when(secureKeyResolver.getSecureKey(anyString())).thenReturn(createSecureKey()); when(secureKeyResolver.getSecureKey(anyString())).thenReturn(createSecureKey());
enrichers = Sets.newHashSet(); enrichers = Sets.newHashSet();
JwtAccessTokenBuilderFactory factory = new JwtAccessTokenBuilderFactory(keyGenerator, secureKeyResolver, enrichers); factory = new JwtAccessTokenBuilderFactory(keyGenerator, secureKeyResolver, enrichers);
builder = factory.create();
} }
/** /**
@@ -92,7 +91,7 @@ public class JwtAccessTokenBuilderTest {
password = "secret" password = "secret"
) )
public void testBuildWithoutSubject() { public void testBuildWithoutSubject() {
JwtAccessToken token = builder.build(); JwtAccessToken token = factory.create().build();
assertEquals("trillian", token.getSubject()); assertEquals("trillian", token.getSubject());
} }
@@ -101,7 +100,7 @@ public class JwtAccessTokenBuilderTest {
*/ */
@Test @Test
public void testBuildWithSubject() { public void testBuildWithSubject() {
JwtAccessToken token = builder.subject("dent").build(); JwtAccessToken token = factory.create().subject("dent").build();
assertEquals("dent", token.getSubject()); assertEquals("dent", token.getSubject());
} }
@@ -110,8 +109,8 @@ public class JwtAccessTokenBuilderTest {
*/ */
@Test @Test
public void testBuildWithEnricher() { public void testBuildWithEnricher() {
enrichers.add((claims) -> claims.put("c", "d")); enrichers.add((b) -> b.custom("c", "d"));
JwtAccessToken token = builder.subject("dent").build(); JwtAccessToken token = factory.create().subject("dent").build();
assertEquals("d", token.getCustom("c").get()); assertEquals("d", token.getCustom("c").get());
} }
@@ -120,7 +119,7 @@ public class JwtAccessTokenBuilderTest {
*/ */
@Test @Test
public void testBuild(){ public void testBuild(){
JwtAccessToken token = builder.subject("dent") JwtAccessToken token = factory.create().subject("dent")
.issuer("https://www.scm-manager.org") .issuer("https://www.scm-manager.org")
.expiresIn(5, TimeUnit.SECONDS) .expiresIn(5, TimeUnit.SECONDS)
.custom("a", "b") .custom("a", "b")

View File

@@ -31,13 +31,8 @@
package sonia.scm.security; package sonia.scm.security;
import com.google.common.collect.Maps;
import java.util.Map;
import javax.inject.Provider;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import org.junit.Test; import org.junit.Test;
import static org.junit.Assert.*;
import static org.hamcrest.Matchers.*;
import org.junit.Before; import org.junit.Before;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.mockito.Mock; import org.mockito.Mock;
@@ -47,19 +42,22 @@ import sonia.scm.config.ScmConfiguration;
import sonia.scm.util.HttpUtil; import sonia.scm.util.HttpUtil;
/** /**
* Unit tests for {@link XsrfTokenClaimsEnricher}. * Unit tests for {@link XsrfAccessTokenEnricher}.
* *
* @author Sebastian Sdorra * @author Sebastian Sdorra
*/ */
@RunWith(MockitoJUnitRunner.class) @RunWith(MockitoJUnitRunner.class)
public class XsrfTokenClaimsEnricherTest { public class XsrfAccessTokenEnricherTest {
@Mock @Mock
private HttpServletRequest request; private HttpServletRequest request;
@Mock
private AccessTokenBuilder builder;
private ScmConfiguration configuration; private ScmConfiguration configuration;
private XsrfTokenClaimsEnricher enricher; private XsrfAccessTokenEnricher enricher;
/** /**
* Prepare object under test. * Prepare object under test.
@@ -67,11 +65,16 @@ public class XsrfTokenClaimsEnricherTest {
@Before @Before
public void prepareObjectUnderTest() { public void prepareObjectUnderTest() {
configuration = new ScmConfiguration(); configuration = new ScmConfiguration();
enricher = new XsrfTokenClaimsEnricher(configuration, () -> request); enricher = new XsrfAccessTokenEnricher(configuration, () -> request) {
@Override
String createToken() {
return "42";
}
};
} }
/** /**
* Tests {@link XsrfTokenClaimsEnricher#enrich(java.util.Map)}. * Tests {@link XsrfAccessTokenEnricher#enrich(java.util.Map)}.
*/ */
@Test @Test
public void testEnrich() { public void testEnrich() {
@@ -80,15 +83,14 @@ public class XsrfTokenClaimsEnricherTest {
when(request.getHeader(HttpUtil.HEADER_SCM_CLIENT)).thenReturn(HttpUtil.SCM_CLIENT_WUI); when(request.getHeader(HttpUtil.HEADER_SCM_CLIENT)).thenReturn(HttpUtil.SCM_CLIENT_WUI);
// execute // execute
Map<String,Object> claims = Maps.newHashMap(); enricher.enrich(builder);
enricher.enrich(claims);
// assert // assert
assertNotNull(claims.get(Xsrf.CLAIMS_KEY)); verify(builder).custom(Xsrf.TOKEN_KEY, "42");
} }
/** /**
* Tests {@link XsrfTokenClaimsEnricher#enrich(java.util.Map)} with disabled xsrf protection. * Tests {@link XsrfAccessTokenEnricher#enrich(java.util.Map)} with disabled xsrf protection.
*/ */
@Test @Test
public void testEnrichWithDisabledXsrf() { public void testEnrichWithDisabledXsrf() {
@@ -97,15 +99,14 @@ public class XsrfTokenClaimsEnricherTest {
when(request.getHeader(HttpUtil.HEADER_SCM_CLIENT)).thenReturn(HttpUtil.SCM_CLIENT_WUI); when(request.getHeader(HttpUtil.HEADER_SCM_CLIENT)).thenReturn(HttpUtil.SCM_CLIENT_WUI);
// execute // execute
Map<String,Object> claims = Maps.newHashMap(); enricher.enrich(builder);
enricher.enrich(claims);
// assert // assert
assertNull(claims.get(Xsrf.CLAIMS_KEY)); verify(builder, never()).custom(Xsrf.TOKEN_KEY, "42");
} }
/** /**
* Tests {@link XsrfTokenClaimsEnricher#enrich(java.util.Map)} with disabled xsrf protection. * Tests {@link XsrfAccessTokenEnricher#enrich(java.util.Map)} with disabled xsrf protection.
*/ */
@Test @Test
public void testEnrichWithNonWuiClient() { public void testEnrichWithNonWuiClient() {
@@ -113,11 +114,10 @@ public class XsrfTokenClaimsEnricherTest {
configuration.setEnabledXsrfProtection(true); configuration.setEnabledXsrfProtection(true);
// execute // execute
Map<String,Object> claims = Maps.newHashMap(); enricher.enrich(builder);
enricher.enrich(claims);
// assert // assert
assertNull(claims.get(Xsrf.CLAIMS_KEY)); verify(builder, never()).custom(Xsrf.TOKEN_KEY, "42");
} }
} }

View File

@@ -70,7 +70,7 @@ public class XsrfTokenClaimsValidatorTest {
public void testValidate() { public void testValidate() {
// prepare // prepare
Map<String, Object> claims = Maps.newHashMap(); Map<String, Object> claims = Maps.newHashMap();
claims.put(Xsrf.CLAIMS_KEY, "abc"); claims.put(Xsrf.TOKEN_KEY, "abc");
when(request.getHeader(Xsrf.HEADER_KEY)).thenReturn("abc"); when(request.getHeader(Xsrf.HEADER_KEY)).thenReturn("abc");
// execute and assert // execute and assert
@@ -84,7 +84,7 @@ public class XsrfTokenClaimsValidatorTest {
public void testValidateWithWrongHeader() { public void testValidateWithWrongHeader() {
// prepare // prepare
Map<String, Object> claims = Maps.newHashMap(); Map<String, Object> claims = Maps.newHashMap();
claims.put(Xsrf.CLAIMS_KEY, "abc"); claims.put(Xsrf.TOKEN_KEY, "abc");
when(request.getHeader(Xsrf.HEADER_KEY)).thenReturn("123"); when(request.getHeader(Xsrf.HEADER_KEY)).thenReturn("123");
// execute and assert // execute and assert
@@ -98,7 +98,7 @@ public class XsrfTokenClaimsValidatorTest {
public void testValidateWithoutHeader() { public void testValidateWithoutHeader() {
// prepare // prepare
Map<String, Object> claims = Maps.newHashMap(); Map<String, Object> claims = Maps.newHashMap();
claims.put(Xsrf.CLAIMS_KEY, "abc"); claims.put(Xsrf.TOKEN_KEY, "abc");
// execute and assert // execute and assert
assertFalse(validator.validate(claims)); assertFalse(validator.validate(claims));