diff --git a/scm-webapp/src/main/java/sonia/scm/security/PercentageJwtAccessTokenRefreshStrategy.java b/scm-webapp/src/main/java/sonia/scm/security/PercentageJwtAccessTokenRefreshStrategy.java new file mode 100644 index 0000000000..2d5c67c7b6 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/security/PercentageJwtAccessTokenRefreshStrategy.java @@ -0,0 +1,25 @@ +package sonia.scm.security; + +import java.time.Clock; + +public class PercentageJwtAccessTokenRefreshStrategy implements JwtAccessTokenRefreshStrategy { + + private final Clock clock; + private final float refreshPercentage; + + public PercentageJwtAccessTokenRefreshStrategy(float refreshPercentage) { + this(Clock.systemDefaultZone(), refreshPercentage); + } + + PercentageJwtAccessTokenRefreshStrategy(Clock clock, float refreshPercentage) { + this.clock = clock; + this.refreshPercentage = refreshPercentage; + } + + @Override + public boolean shouldBeRefreshed(JwtAccessToken oldToken) { + long liveSpan = oldToken.getExpiration().getTime() - oldToken.getIssuedAt().getTime(); + long age = clock.instant().toEpochMilli() - oldToken.getIssuedAt().getTime(); + return age/liveSpan > refreshPercentage; + } +} 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 83f528092c..cd902fb0a8 100644 --- a/scm-webapp/src/test/java/sonia/scm/security/JwtAccessTokenRefresherTest.java +++ b/scm-webapp/src/test/java/sonia/scm/security/JwtAccessTokenRefresherTest.java @@ -12,12 +12,10 @@ import org.mockito.junit.MockitoJUnitRunner; import java.sql.Date; import java.time.Clock; import java.time.Instant; -import java.time.temporal.ChronoUnit; import java.util.Collections; import java.util.Optional; import java.util.Random; -import static java.time.Duration.ofHours; import static java.time.Duration.ofMinutes; import static java.time.temporal.ChronoUnit.SECONDS; import static java.util.concurrent.TimeUnit.MINUTES; diff --git a/scm-webapp/src/test/java/sonia/scm/security/PercentageJwtAccessTokenRefreshStrategyTest.java b/scm-webapp/src/test/java/sonia/scm/security/PercentageJwtAccessTokenRefreshStrategyTest.java new file mode 100644 index 0000000000..d2c684d4a0 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/security/PercentageJwtAccessTokenRefreshStrategyTest.java @@ -0,0 +1,70 @@ +package sonia.scm.security; + +import com.github.sdorra.shiro.ShiroRule; +import com.github.sdorra.shiro.SubjectAware; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; + +import java.time.Clock; +import java.time.Instant; +import java.util.Collections; +import java.util.Random; + +import static java.time.temporal.ChronoUnit.MINUTES; +import static java.time.temporal.ChronoUnit.SECONDS; +import static java.util.concurrent.TimeUnit.HOURS; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@SubjectAware( + username = "user", + password = "secret", + configuration = "classpath:sonia/scm/repository/shiro.ini" +) +public class PercentageJwtAccessTokenRefreshStrategyTest { + + private static final Instant TOKEN_CREATION = Instant.now().truncatedTo(SECONDS); + + @Rule + public ShiroRule shiro = new ShiroRule(); + + private KeyGenerator keyGenerator = () -> "key"; + + private Clock refreshClock = mock(Clock.class); + + private JwtAccessTokenBuilder tokenBuilder; + private PercentageJwtAccessTokenRefreshStrategy refreshStrategy; + + @Before + public void initToken() { + SecureKeyResolver keyResolver = mock(SecureKeyResolver.class); + byte[] bytes = new byte[256]; + new Random().nextBytes(bytes); + SecureKey secureKey = new SecureKey(bytes, System.currentTimeMillis()); + when(keyResolver.getSecureKey(any())).thenReturn(secureKey); + + Clock creationClock = mock(Clock.class); + when(creationClock.instant()).thenReturn(TOKEN_CREATION); + + tokenBuilder = new JwtAccessTokenBuilderFactory(keyGenerator, keyResolver, Collections.emptySet(), creationClock).create(); + tokenBuilder + .refreshableFor(1, HOURS); + + refreshStrategy = new PercentageJwtAccessTokenRefreshStrategy(refreshClock, 0.5F); + } + + @Test + public void shouldNotRefreshWhenTokenIsYoung() { + when(refreshClock.instant()).thenReturn(TOKEN_CREATION.plus(1, MINUTES)); + assertThat(refreshStrategy.shouldBeRefreshed(tokenBuilder.build())).isFalse(); + } + + @Test + public void shouldRefreshWhenTokenIsOld() { + when(refreshClock.instant()).thenReturn(TOKEN_CREATION.plus(31, MINUTES)); + assertThat(refreshStrategy.shouldBeRefreshed(tokenBuilder.build())).isFalse(); + } +}