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,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);