diff --git a/scm-core/src/main/java/sonia/scm/security/AccessToken.java b/scm-core/src/main/java/sonia/scm/security/AccessToken.java index ec448ad235..c2a5f4b747 100644 --- a/scm-core/src/main/java/sonia/scm/security/AccessToken.java +++ b/scm-core/src/main/java/sonia/scm/security/AccessToken.java @@ -80,7 +80,7 @@ public interface AccessToken { */ Date getExpiration(); - Date getRefreshExpiration(); + Optional getRefreshExpiration(); /** * Returns the scope of the token. The scope is able to reduce the permissions of the subject in the context of this diff --git a/scm-webapp/src/main/java/sonia/scm/security/JwtAccessToken.java b/scm-webapp/src/main/java/sonia/scm/security/JwtAccessToken.java index 35013e4fec..e6cfb8c957 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/JwtAccessToken.java +++ b/scm-webapp/src/main/java/sonia/scm/security/JwtAccessToken.java @@ -37,6 +37,8 @@ import java.util.Date; import java.util.Map; import java.util.Optional; +import static java.util.Optional.ofNullable; + /** * Jwt implementation of {@link AccessToken}. * @@ -44,7 +46,9 @@ import java.util.Optional; * @since 2.0.0 */ public final class JwtAccessToken implements AccessToken { - + + public static final String REFRESHABLE_UNTIL_CLAIM_KEY = "scm-manager.refreshableUntil"; + public static final String PARENT_TOKEN_ID_CLAIM_KEY = "scm-manager.parentTokenId"; private final Claims claims; private final String compact; @@ -79,8 +83,8 @@ public final class JwtAccessToken implements AccessToken { } @Override - public Date getRefreshExpiration() { - return claims.get("scm-manager.refreshableUntil", Date.class); + public Optional getRefreshExpiration() { + return ofNullable(claims.get(REFRESHABLE_UNTIL_CLAIM_KEY, Date.class)); } @Override diff --git a/scm-webapp/src/main/java/sonia/scm/security/JwtAccessTokenBuilder.java b/scm-webapp/src/main/java/sonia/scm/security/JwtAccessTokenBuilder.java index 8e5de9c283..b6b293f525 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/JwtAccessTokenBuilder.java +++ b/scm-webapp/src/main/java/sonia/scm/security/JwtAccessTokenBuilder.java @@ -67,6 +67,7 @@ public final class JwtAccessTokenBuilder implements AccessTokenBuilder { private TimeUnit expiresInUnit = TimeUnit.HOURS; private long refreshableFor = 12; private TimeUnit refreshableForUnit = TimeUnit.HOURS; + private String parentKeyId; private Scope scope = Scope.empty(); private final Map custom = Maps.newHashMap(); @@ -127,6 +128,11 @@ public final class JwtAccessTokenBuilder implements AccessTokenBuilder { return this; } + public JwtAccessTokenBuilder parentKey(String parentKeyId) { + this.parentKeyId = parentKeyId; + return this; + } + private String getSubject(){ if (subject == null) { Subject currentSubject = SecurityUtils.getSubject(); @@ -162,7 +168,12 @@ public final class JwtAccessTokenBuilder implements AccessTokenBuilder { if (refreshableFor > 0) { long refreshExpiration = refreshableForUnit.toMillis(refreshableFor); - claims.put("scm-manager.refreshableUntil", new Date(now.getTime() + refreshExpiration).getTime()); + claims.put(JwtAccessToken.REFRESHABLE_UNTIL_CLAIM_KEY, new Date(now.getTime() + refreshExpiration).getTime()); + } + if (parentKeyId == null) { + claims.put(JwtAccessToken.PARENT_TOKEN_ID_CLAIM_KEY, id); + } else { + claims.put(JwtAccessToken.PARENT_TOKEN_ID_CLAIM_KEY, parentKeyId); } if ( issuer != null ) { diff --git a/scm-webapp/src/main/java/sonia/scm/security/JwtAccessTokenRefresher.java b/scm-webapp/src/main/java/sonia/scm/security/JwtAccessTokenRefresher.java index 16370209b6..f3f3d032fc 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/JwtAccessTokenRefresher.java +++ b/scm-webapp/src/main/java/sonia/scm/security/JwtAccessTokenRefresher.java @@ -1,5 +1,8 @@ package sonia.scm.security; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import javax.inject.Inject; import java.time.Clock; import java.util.Date; @@ -9,6 +12,8 @@ import java.util.concurrent.TimeUnit; public class JwtAccessTokenRefresher { + private static final Logger log = LoggerFactory.getLogger(JwtAccessTokenRefresher.class); + private final JwtAccessTokenBuilderFactory builderFactory; private final JwtAccessTokenRefreshStrategy refreshStrategy; private final Clock clock; @@ -30,8 +35,13 @@ public class JwtAccessTokenRefresher { claims.forEach(builder::custom); if (canBeRefreshed(oldToken) && shouldBeRefreshed(oldToken)) { + Optional parentTokenId = oldToken.getCustom("scm-manager.parentTokenId"); + if (!parentTokenId.isPresent()) { + log.warn("no parent token id found in token; could not refresh"); + return Optional.empty(); + } builder.expiresIn(1, TimeUnit.HOURS); -// builder.custom("scm-manager.parentTokenId") + builder.parentKey(parentTokenId.get().toString()); return Optional.of(builder.build()); } else { return Optional.empty(); @@ -47,8 +57,7 @@ public class JwtAccessTokenRefresher { } private boolean tokenCanBeRefreshed(JwtAccessToken oldToken) { - Date refreshExpiration = oldToken.getRefreshExpiration(); - return refreshExpiration != null && isAfterNow(refreshExpiration); + return oldToken.getRefreshExpiration().map(this::isAfterNow).orElse(false); } private boolean tokenIsValid(JwtAccessToken oldToken) { diff --git a/scm-webapp/src/test/java/sonia/scm/security/JwtAccessTokenRefresherTest.java b/scm-webapp/src/test/java/sonia/scm/security/JwtAccessTokenRefresherTest.java index 7c7a8c68cf..06daf87fcb 100644 --- a/scm-webapp/src/test/java/sonia/scm/security/JwtAccessTokenRefresherTest.java +++ b/scm-webapp/src/test/java/sonia/scm/security/JwtAccessTokenRefresherTest.java @@ -39,6 +39,8 @@ public class JwtAccessTokenRefresherTest { @Mock private Clock clock; + private KeyGenerator keyGenerator = () -> "key"; + private JwtAccessTokenRefresher refresher; private JwtAccessTokenBuilder tokenBuilder; @@ -49,11 +51,16 @@ public class JwtAccessTokenRefresherTest { SecureKey secureKey = new SecureKey(bytes, System.currentTimeMillis()); when(keyResolver.getSecureKey(any())).thenReturn(secureKey); - JwtAccessTokenBuilderFactory builderFactory = new JwtAccessTokenBuilderFactory(new DefaultKeyGenerator(), keyResolver, Collections.emptySet()); + JwtAccessTokenBuilderFactory builderFactory = new JwtAccessTokenBuilderFactory(keyGenerator, keyResolver, Collections.emptySet()); refresher = new JwtAccessTokenRefresher(builderFactory, refreshStrategy, clock); tokenBuilder = builderFactory.create(); when(clock.instant()).thenAnswer(invocationOnMock -> Instant.now()); when(refreshStrategy.shouldBeRefreshed(any())).thenReturn(true); + + // set default expiration values + tokenBuilder + .expiresIn(5, MINUTES) + .refreshableFor(10, MINUTES); } @Test @@ -69,12 +76,9 @@ public class JwtAccessTokenRefresherTest { @Test public void shouldNotRefreshTokenWhenTokenExpired() { - Instant oneMinuteAgo = Instant.now().plus(ofMinutes(2)); - when(clock.instant()).thenReturn(oneMinuteAgo); - JwtAccessToken oldToken = tokenBuilder - .expiresIn(1, MINUTES) - .refreshableFor(5, MINUTES) - .build(); + Instant afterNormalExpiration = Instant.now().plus(ofMinutes(6)); + when(clock.instant()).thenReturn(afterNormalExpiration); + JwtAccessToken oldToken = tokenBuilder.build(); Optional refreshedToken = refresher.refresh(oldToken); @@ -83,10 +87,9 @@ public class JwtAccessTokenRefresherTest { @Test public void shouldNotRefreshTokenWhenRefreshExpired() { - Instant oneMinuteAgo = Instant.now().plus(ofMinutes(2)); - when(clock.instant()).thenReturn(oneMinuteAgo); + Instant afterRefreshExpiration = Instant.now().plus(ofMinutes(2)); + when(clock.instant()).thenReturn(afterRefreshExpiration); JwtAccessToken oldToken = tokenBuilder - .expiresIn(5, MINUTES) .refreshableFor(1, MINUTES) .build(); @@ -97,9 +100,7 @@ public class JwtAccessTokenRefresherTest { @Test public void shouldNotRefreshTokenWhenStrategyDoesNotSaySo() { - JwtAccessToken oldToken = tokenBuilder - .refreshableFor(10, MINUTES) - .build(); + JwtAccessToken oldToken = tokenBuilder.build(); when(refreshStrategy.shouldBeRefreshed(oldToken)).thenReturn(false); Optional refreshedToken = refresher.refresh(oldToken); @@ -109,14 +110,13 @@ public class JwtAccessTokenRefresherTest { @Test public void shouldRefreshTokenWithEnabledRefresh() { - JwtAccessToken oldToken = tokenBuilder - .expiresIn(1, MINUTES) - .refreshableFor(1, MINUTES) - .build(); + JwtAccessToken oldToken = tokenBuilder.build(); when(refreshStrategy.shouldBeRefreshed(oldToken)).thenReturn(true); Optional refreshedToken = refresher.refresh(oldToken); assertThat(refreshedToken).isNotEmpty(); + assertThat(refreshedToken.get().getClaims()) + .containsEntry(JwtAccessToken.PARENT_TOKEN_ID_CLAIM_KEY, "key"); } }