User jwt sessions can now be endless

Committed-by: Eduard Heimbuch <eduard.heimbuch@cloudogu.com>
Co-authored-by: tzerr <thomas.zerr@cloudogu.com>

Reviewed-by: Konstantin Schaper <konstantin.schaper@cloudogu.com>
This commit is contained in:
Thomas Zerr
2023-07-27 13:03:35 +02:00
parent 891c56b21d
commit a2c9ed67a3
13 changed files with 503 additions and 15 deletions

View File

@@ -0,0 +1,26 @@
---
title: JWT Configuration
---
SCM-Manager uses [JWT](https://datatracker.ietf.org/doc/html/rfc7519) to authenticate its users.
The creation of JWTs can be controlled via Java system properties.
## Endless JWT
Usually a JWT contains the exp claim. This claim determines how long a JWT is valid by defining an expiration time.
If the JWT does not contain this claim, then the JWT is valid forever until the secret for the signature changes.
Per default the JWT created by the SCM-Manager contain the exp claim with a duration of one hour.
If needed, it is possible to configure the SCM-Manager, so that the JWT get created without the exp claim.
Therefore, the user session would be endless.
We advise **against** this behavior, because limited lifespans for JWT improve security.
But if you really need it, you can enable endless JWT by starting the SCM-Manager with this flag:
```
-Dscm.endlessJwt="true"
```
If you want to disable the feature, then restart the SCM-Manager without this flag.
If you want to invalidate already created endless JWT, then restarting the SCM-Manager, with the endless JWT feature disabled, is enough.
The SCM-Manager will automatically create new secrets for the JWT and therefore invalidate every already existing JWT.

View File

@@ -0,0 +1,2 @@
- type: added
description: User sessions can now be configured to be endless

View File

@@ -0,0 +1,44 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.lifecycle.jwt;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement;
@Data
@XmlRootElement(name = "jwtSettings")
@XmlAccessorType(XmlAccessType.FIELD)
@NoArgsConstructor
@AllArgsConstructor
public class JwtSettings {
private boolean endlessJwtEnabledLastStartUp = false;
private long keysValidAfterTimestampInMs = 0;
}

View File

@@ -0,0 +1,87 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.lifecycle.jwt;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.lifecycle.PrivilegedStartupAction;
import sonia.scm.plugin.Extension;
import sonia.scm.security.JwtSystemProperties;
import javax.inject.Inject;
import java.time.Clock;
import java.time.Instant;
@Extension
public class JwtSettingsStartupAction implements PrivilegedStartupAction {
private static final Logger LOG = LoggerFactory.getLogger(JwtSettingsStartupAction.class);
private final JwtSettingsStore store;
private final Clock clock;
@Inject
public JwtSettingsStartupAction(JwtSettingsStore store) {
this(store, Clock.systemDefaultZone());
}
public JwtSettingsStartupAction(JwtSettingsStore store, Clock clock) {
this.store = store;
this.clock = clock;
}
@Override
public void run() {
LOG.debug("Checking JWT Settings");
JwtSettings settings = store.get();
boolean isEndlessJwtEnabledNow = JwtSystemProperties.isEndlessJwtEnabled();
if(!areSettingsChanged(settings, isEndlessJwtEnabledNow)) {
LOG.debug("JWT Settings unchanged");
return;
}
JwtSettings updatedSettings = new JwtSettings();
updatedSettings.setEndlessJwtEnabledLastStartUp(isEndlessJwtEnabledNow);
if(areEndlessJwtNeedingInvalidation(settings, isEndlessJwtEnabledNow)) {
updatedSettings.setKeysValidAfterTimestampInMs(Instant.now(clock).toEpochMilli());
} else {
updatedSettings.setKeysValidAfterTimestampInMs(settings.getKeysValidAfterTimestampInMs());
}
store.set(updatedSettings);
LOG.debug("JWT Settings updated");
}
private boolean areSettingsChanged(JwtSettings settings, boolean isEndlessJwtEnabledNow) {
return settings.isEndlessJwtEnabledLastStartUp() != isEndlessJwtEnabledNow;
}
private boolean areEndlessJwtNeedingInvalidation(JwtSettings settings, boolean isEndlessJwtEnabledNow) {
return settings.isEndlessJwtEnabledLastStartUp() && !isEndlessJwtEnabledNow;
}
}

View File

@@ -0,0 +1,51 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.lifecycle.jwt;
import sonia.scm.store.ConfigurationStore;
import sonia.scm.store.ConfigurationStoreFactory;
import javax.inject.Inject;
import javax.inject.Singleton;
@Singleton
public class JwtSettingsStore {
static final String STORE_NAME = "jwt-settings";
private final ConfigurationStore<JwtSettings> store;
@Inject
public JwtSettingsStore(ConfigurationStoreFactory storeFactory) {
store = storeFactory.withType(JwtSettings.class).withName(STORE_NAME).build();
}
public JwtSettings get() {
return store.getOptional().orElse(new JwtSettings());
}
public void set(JwtSettings settings) {
store.set(settings);
}
}

View File

@@ -45,6 +45,8 @@ import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import static sonia.scm.security.JwtSystemProperties.ENDLESS_JWT;
/**
* Jwt implementation of {@link AccessTokenBuilder}.
*
@@ -184,9 +186,11 @@ public final class JwtAccessTokenBuilder implements AccessTokenBuilder {
Claims claims = Jwts.claims(customClaims)
.setSubject(sub)
.setId(id)
.setIssuedAt(Date.from(now))
.setExpiration(new Date(now.toEpochMilli() + expiration));
.setIssuedAt(Date.from(now));
if(!JwtSystemProperties.isEndlessJwtEnabled()) {
claims.setExpiration(new Date(now.toEpochMilli() + expiration));
}
if (refreshableFor > 0) {
long re = refreshableForUnit.toMillis(refreshableFor);

View File

@@ -0,0 +1,34 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.security;
public class JwtSystemProperties {
public static final String ENDLESS_JWT = "scm.endlessJwt";
public static boolean isEndlessJwtEnabled() {
return Boolean.parseBoolean(System.getProperty(ENDLESS_JWT));
}
}

View File

@@ -36,6 +36,8 @@ import io.jsonwebtoken.SigningKeyResolverAdapter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.lifecycle.jwt.JwtSettings;
import sonia.scm.lifecycle.jwt.JwtSettingsStore;
import sonia.scm.store.ConfigurationEntryStore;
import sonia.scm.store.ConfigurationEntryStoreFactory;
@@ -82,16 +84,17 @@ public class SecureKeyResolver extends SigningKeyResolverAdapter
*/
@Inject
@SuppressWarnings("unchecked")
public SecureKeyResolver(ConfigurationEntryStoreFactory storeFactory) {
this(storeFactory, new SecureRandom());
public SecureKeyResolver(ConfigurationEntryStoreFactory storeFactory, JwtSettingsStore jwtSettingsStore) {
this(storeFactory, jwtSettingsStore, new SecureRandom());
}
SecureKeyResolver(ConfigurationEntryStoreFactory storeFactory, Random random)
SecureKeyResolver(ConfigurationEntryStoreFactory storeFactory, JwtSettingsStore jwtSettingsStore, Random random)
{
store = storeFactory
.withType(SecureKey.class)
.withName(STORE_NAME)
.build();
this.jwtSettingsStore = jwtSettingsStore;
this.random = random;
}
@@ -109,13 +112,7 @@ public class SecureKeyResolver extends SigningKeyResolverAdapter
checkArgument(!Strings.isNullOrEmpty(subject), "subject is required");
SecureKey key = store.get(subject);
if (key == null) {
return getSecureKey(subject).getBytes();
}
return key.getBytes();
return getSecureKey(subject).getBytes();
}
//~--- get methods ----------------------------------------------------------
@@ -132,7 +129,7 @@ public class SecureKeyResolver extends SigningKeyResolverAdapter
{
SecureKey key = store.get(subject);
if (key == null)
if (key == null || isKeyExpired(key))
{
logger.trace("create new key for subject");
key = createNewKey();
@@ -142,6 +139,12 @@ public class SecureKeyResolver extends SigningKeyResolverAdapter
return key;
}
private boolean isKeyExpired(SecureKey key) {
JwtSettings settings = jwtSettingsStore.get();
return key.getCreationDate() < settings.getKeysValidAfterTimestampInMs();
}
//~--- methods --------------------------------------------------------------
/**
@@ -166,4 +169,6 @@ public class SecureKeyResolver extends SigningKeyResolverAdapter
/** configuration entry store */
private final ConfigurationEntryStore<SecureKey> store;
private final JwtSettingsStore jwtSettingsStore;
}

View File

@@ -105,7 +105,7 @@ public class TokenRefreshFilter extends HttpFilter {
LOG.trace("could not resolve token", e);
return;
}
if (accessToken instanceof JwtAccessToken) {
if (accessToken instanceof JwtAccessToken && !isEndlessToken((JwtAccessToken) accessToken)) {
refresher.refresh((JwtAccessToken) accessToken)
.ifPresent(jwtAccessToken -> refreshJwtToken(request, response, jwtAccessToken));
}
@@ -116,4 +116,8 @@ public class TokenRefreshFilter extends HttpFilter {
LOG.debug("refreshing JWT authentication token");
issuer.authenticate(request, response, jwtAccessToken);
}
private boolean isEndlessToken(JwtAccessToken token) {
return token.getExpiration() == null;
}
}

View File

@@ -0,0 +1,114 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.lifecycle.jwt;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.security.JwtSystemProperties;
import java.time.Clock;
import java.time.Instant;
import java.time.ZoneId;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class JwtSettingsStartupActionTest {
private JwtSettingsStartupAction jwtSettingsAction;
@Mock
private JwtSettingsStore jwtSettingsStore;
private final Clock clock = Clock.fixed(Instant.now(), ZoneId.systemDefault());
@BeforeEach
void setupAction() {
jwtSettingsAction = new JwtSettingsStartupAction(jwtSettingsStore, clock);
}
@BeforeEach
void clearSystemProperties() {
System.clearProperty(JwtSystemProperties.ENDLESS_JWT);
}
@ParameterizedTest
@CsvSource({"true,true", "false,false"})
void shouldNotChangeSettings(String isEndlessJwtNowEnabled, String isEndlessJwtEnabledLastStartUp) {
System.setProperty(JwtSystemProperties.ENDLESS_JWT, isEndlessJwtNowEnabled);
JwtSettings settings = new JwtSettings(Boolean.parseBoolean(isEndlessJwtEnabledLastStartUp), 0);
when(jwtSettingsStore.get()).thenReturn(settings);
jwtSettingsAction.run();
assertThat(settings.isEndlessJwtEnabledLastStartUp()).isEqualTo(Boolean.parseBoolean(isEndlessJwtNowEnabled));
assertThat(settings.getKeysValidAfterTimestampInMs()).isEqualTo(0);
verify(jwtSettingsStore).get();
verifyNoMoreInteractions(jwtSettingsStore);
}
@Test
void shouldOnlyUpdateEndlessJwtEnabledLastStartup() {
System.setProperty(JwtSystemProperties.ENDLESS_JWT, "true");
JwtSettings settings = new JwtSettings(false, 0);
when(jwtSettingsStore.get()).thenReturn(settings);
jwtSettingsAction.run();
verify(jwtSettingsStore).get();
verify(jwtSettingsStore).set(argThat(actualSettings -> {
assertThat(actualSettings.isEndlessJwtEnabledLastStartUp()).isEqualTo(true);
assertThat(actualSettings.getKeysValidAfterTimestampInMs()).isEqualTo(0);
return true;
}));
}
@Test
void shouldInvalidateKeys() {
System.setProperty(JwtSystemProperties.ENDLESS_JWT, "false");
JwtSettings settings = new JwtSettings(true, 0);
when(jwtSettingsStore.get()).thenReturn(settings);
jwtSettingsAction.run();
verify(jwtSettingsStore).get();
verify(jwtSettingsStore).set(argThat(actualSettings -> {
assertThat(actualSettings.isEndlessJwtEnabledLastStartUp()).isEqualTo(false);
assertThat(actualSettings.getKeysValidAfterTimestampInMs()).isEqualTo(Instant.now(clock).toEpochMilli());
return true;
}));
}
}

View File

@@ -95,6 +95,11 @@ class JwtAccessTokenBuilderTest {
factory = new JwtAccessTokenBuilderFactory(keyGenerator, secureKeyResolver, enrichers);
}
@BeforeEach
void clearSystemProperties() {
System.clearProperty(JwtSystemProperties.ENDLESS_JWT);
}
@Nested
class SimpleTests {
@@ -261,5 +266,70 @@ class JwtAccessTokenBuilderTest {
JwtAccessToken token = factory.create().subject("dent").build();
assertThat(token.getCustom("c")).get().isEqualTo("d");
}
}
@Nested
class WithEndlessJwtFeature {
@Test
void testBuildWithEndlessJwtEnabled() {
System.setProperty(JwtSystemProperties.ENDLESS_JWT, "true");
JwtAccessToken token = factory.create().subject("Red").issuer("https://scm-manager.org").build();
assertThat(token.getId()).isNotEmpty();
assertThat(token.getIssuedAt()).isNotNull();
assertThat(token.getExpiration()).isNull();
assertThat(token.getSubject()).isEqualTo("Red");
assertThat(token.getIssuer()).isNotEmpty();
assertThat(token.getIssuer().get()).isEqualTo("https://scm-manager.org");
}
@Test
void testBuildWithEndlessJwtDisabled() {
System.setProperty(JwtSystemProperties.ENDLESS_JWT, "false");
JwtAccessToken token = factory.create().subject("Red").issuer("https://scm-manager.org").build();
assertThat(token.getId()).isNotEmpty();
assertThat(token.getIssuedAt()).isNotNull();
assertThat(token.getExpiration()).isNotNull();
assertThat(token.getExpiration().getTime() > token.getIssuedAt().getTime()).isTrue();
assertThat(token.getSubject()).isEqualTo("Red");
assertThat(token.getIssuer()).isNotEmpty();
assertThat(token.getIssuer().get()).isEqualTo("https://scm-manager.org");
}
@Test
void testBuildWithInvalidConfig() {
System.setProperty(JwtSystemProperties.ENDLESS_JWT, "invalidStuff");
JwtAccessToken token = factory.create().subject("Red").issuer("https://scm-manager.org").build();
assertThat(token.getId()).isNotEmpty();
assertThat(token.getIssuedAt()).isNotNull();
assertThat(token.getExpiration()).isNotNull();
assertThat(token.getExpiration().getTime() > token.getIssuedAt().getTime()).isTrue();
assertThat(token.getSubject()).isEqualTo("Red");
assertThat(token.getIssuer()).isNotEmpty();
assertThat(token.getIssuer().get()).isEqualTo("https://scm-manager.org");
}
@Test
void testBuildWithMissingConfig() {
System.clearProperty(JwtSystemProperties.ENDLESS_JWT);
JwtAccessToken token = factory.create().subject("Red").issuer("https://scm-manager.org").build();
assertThat(token.getId()).isNotEmpty();
assertThat(token.getIssuedAt()).isNotNull();
assertThat(token.getExpiration()).isNotNull();
assertThat(token.getExpiration().getTime() > token.getIssuedAt().getTime()).isTrue();
assertThat(token.getSubject()).isEqualTo("Red");
assertThat(token.getIssuer()).isNotEmpty();
assertThat(token.getIssuer().get()).isEqualTo("https://scm-manager.org");
}
}
}

View File

@@ -32,12 +32,16 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import sonia.scm.lifecycle.jwt.JwtSettings;
import sonia.scm.lifecycle.jwt.JwtSettingsStore;
import sonia.scm.store.ConfigurationEntryStore;
import sonia.scm.store.ConfigurationEntryStoreFactory;
import java.util.Arrays;
import java.util.Random;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.not;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertSame;
@@ -66,12 +70,30 @@ public class SecureKeyResolverTest
assertNotNull(key);
when(store.get("test")).thenReturn(key);
when(jwtSettingsStore.get()).thenReturn(settings);
SecureKey sameKey = resolver.getSecureKey("test");
assertSame(key, sameKey);
}
@Test
public void shouldReturnRegeneratedKey() {
when(jwtSettingsStore.get()).thenReturn(settings);
SecureKey expiredKey = new SecureKey("oldKey".getBytes(), 0);
when(store.get("test")).thenReturn(expiredKey);
SecureKey regeneratedKey = resolver.getSecureKey("test");
assertThat(Arrays.equals(regeneratedKey.getBytes(), expiredKey.getBytes())).isFalse();
assertThat(regeneratedKey.getCreationDate() > settings.getKeysValidAfterTimestampInMs()).isTrue();
when(store.get("test")).thenReturn(regeneratedKey);
SecureKey sameRegeneratedKey = resolver.getSecureKey("test");
assertThat(Arrays.equals(sameRegeneratedKey.getBytes(), regeneratedKey.getBytes())).isTrue();
assertThat(sameRegeneratedKey.getCreationDate()).isEqualTo(regeneratedKey.getCreationDate());
}
/**
* Method description
*
@@ -82,6 +104,7 @@ public class SecureKeyResolverTest
SecureKey key = resolver.getSecureKey("test");
when(store.get("test")).thenReturn(key);
when(jwtSettingsStore.get()).thenReturn(settings);
byte[] bytes = resolver.resolveSigningKeyBytes(null,
Jwts.claims().setSubject("test"));
@@ -129,7 +152,7 @@ public class SecureKeyResolverTest
}))).thenReturn(store);
Random random = mock(Random.class);
doAnswer(invocation -> ((byte[]) invocation.getArguments()[0])[0] = 42).when(random).nextBytes(any());
resolver = new SecureKeyResolver(factory, random);
resolver = new SecureKeyResolver(factory, jwtSettingsStore, random);
}
//~--- fields ---------------------------------------------------------------
@@ -140,4 +163,9 @@ public class SecureKeyResolverTest
/** Field description */
@Mock
private ConfigurationEntryStore<SecureKey> store;
@Mock
private JwtSettingsStore jwtSettingsStore;
private JwtSettings settings = new JwtSettings(false, 100);
}

View File

@@ -45,6 +45,7 @@ import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Date;
import java.util.Set;
import static java.util.Collections.singleton;
@@ -54,6 +55,7 @@ import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
import static sonia.scm.security.BearerToken.valueOf;
@@ -129,6 +131,7 @@ class TokenRefreshFilterTest {
when(tokenGenerator.createToken(request)).thenReturn(token);
when(resolver.resolve(token)).thenReturn(jwtToken);
when(refresher.refresh(jwtToken)).thenReturn(of(newJwtToken));
when(jwtToken.getExpiration()).thenReturn(new Date());
filter.doFilter(request, response, filterChain);
@@ -136,6 +139,21 @@ class TokenRefreshFilterTest {
verify(filterChain).doFilter(request, response);
}
@Test
void shouldNotRefreshEndlessToken() throws IOException, ServletException {
BearerToken token = createValidToken();
when(tokenGenerator.createToken(request)).thenReturn(token);
JwtAccessToken jwtToken = mock(JwtAccessToken.class);
when(resolver.resolve(token)).thenReturn(jwtToken);
filter.doFilter(request, response, filterChain);
verify(filterChain).doFilter(request, response);
verifyNoInteractions(refresher);
verifyNoInteractions(issuer);
}
@Test
void shouldTrackMetricIfTokenWasRefreshed() throws IOException, ServletException {
BearerToken token = createValidToken();
@@ -144,6 +162,7 @@ class TokenRefreshFilterTest {
when(tokenGenerator.createToken(request)).thenReturn(token);
when(resolver.resolve(token)).thenReturn(jwtToken);
when(refresher.refresh(jwtToken)).thenReturn(of(newJwtToken));
when(jwtToken.getExpiration()).thenReturn(new Date());
filter.doFilter(request, response, filterChain);