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 c2a5f4b747..ac7700b030 100644 --- a/scm-core/src/main/java/sonia/scm/security/AccessToken.java +++ b/scm-core/src/main/java/sonia/scm/security/AccessToken.java @@ -80,8 +80,20 @@ public interface AccessToken { */ Date getExpiration(); + /** + * Returns refresh expiration of token. + * + * @return refresh expiration + */ Optional getRefreshExpiration(); + /** + * Returns id of the parent key. + * + * @return parent key id + */ + Optional getParentKey(); + /** * Returns the scope of the token. The scope is able to reduce the permissions of the subject in the context of this * token. For example we could issue a token which can only be used to read a single repository. for more informations diff --git a/scm-core/src/main/java/sonia/scm/security/AccessTokenCookieIssuer.java b/scm-core/src/main/java/sonia/scm/security/AccessTokenCookieIssuer.java new file mode 100644 index 0000000000..999c693b8f --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/security/AccessTokenCookieIssuer.java @@ -0,0 +1,30 @@ +package sonia.scm.security; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * Generates cookies and invalidates access token cookies. + * + * @author Sebastian Sdorra + * @since 2.0.0 + */ +public interface AccessTokenCookieIssuer { + + /** + * Creates a cookie for token authentication and attaches it to the response. + * + * @param request http servlet request + * @param response http servlet response + * @param accessToken access token + */ + void authenticate(HttpServletRequest request, HttpServletResponse response, AccessToken accessToken); + /** + * Invalidates the authentication cookie. + * + * @param request http servlet request + * @param response http servlet response + */ + void invalidate(HttpServletRequest request, HttpServletResponse response); + +} diff --git a/scm-core/src/main/java/sonia/scm/security/DefaultCipherHandler.java b/scm-core/src/main/java/sonia/scm/security/DefaultCipherHandler.java index b4f0d81cd3..9c1fa590cc 100644 --- a/scm-core/src/main/java/sonia/scm/security/DefaultCipherHandler.java +++ b/scm-core/src/main/java/sonia/scm/security/DefaultCipherHandler.java @@ -164,7 +164,7 @@ public class DefaultCipherHandler implements CipherHandler { String result = null; try { - byte[] encodedInput = Base64.getDecoder().decode(value); + byte[] encodedInput = Base64.getUrlDecoder().decode(value); byte[] salt = new byte[SALT_LENGTH]; byte[] encoded = new byte[encodedInput.length - SALT_LENGTH]; @@ -221,7 +221,7 @@ public class DefaultCipherHandler implements CipherHandler { System.arraycopy(salt, 0, result, 0, SALT_LENGTH); System.arraycopy(encodedInput, 0, result, SALT_LENGTH, result.length - SALT_LENGTH); - res = new String(Base64.getEncoder().encode(result), ENCODING); + res = new String(Base64.getUrlEncoder().encode(result), ENCODING); } catch (IOException | GeneralSecurityException ex) { throw new CipherException("could not encode string", ex); } diff --git a/scm-core/src/main/java/sonia/scm/xml/XmlInstantAdapter.java b/scm-core/src/main/java/sonia/scm/xml/XmlInstantAdapter.java new file mode 100644 index 0000000000..9b8d718851 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/xml/XmlInstantAdapter.java @@ -0,0 +1,25 @@ +package sonia.scm.xml; + +import javax.xml.bind.annotation.adapters.XmlAdapter; +import java.time.Instant; +import java.time.format.DateTimeFormatter; +import java.time.temporal.TemporalAccessor; + +/** + * JAXB adapter for {@link Instant} objects. + * + * @since 2.0.0 + */ +public class XmlInstantAdapter extends XmlAdapter { + + @Override + public String marshal(Instant instant) { + return DateTimeFormatter.ISO_INSTANT.format(instant); + } + + @Override + public Instant unmarshal(String text) { + TemporalAccessor parsed = DateTimeFormatter.ISO_INSTANT.parse(text); + return Instant.from(parsed); + } +} diff --git a/scm-core/src/test/java/sonia/scm/xml/XmlInstantAdapterTest.java b/scm-core/src/test/java/sonia/scm/xml/XmlInstantAdapterTest.java new file mode 100644 index 0000000000..eb1ea86aee --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/xml/XmlInstantAdapterTest.java @@ -0,0 +1,47 @@ +package sonia.scm.xml; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junitpioneer.jupiter.TempDirectory; + +import javax.xml.bind.JAXB; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; +import java.nio.file.Path; +import java.time.Instant; + +import static org.junit.jupiter.api.Assertions.*; + +@ExtendWith(TempDirectory.class) +class XmlInstantAdapterTest { + + @Test + void shouldMarshalAndUnmarshalInstant(@TempDirectory.TempDir Path tempDirectory) { + Path path = tempDirectory.resolve("instant.xml"); + + Instant instant = Instant.now(); + InstantObject object = new InstantObject(instant); + JAXB.marshal(object, path.toFile()); + + InstantObject unmarshaled = JAXB.unmarshal(path.toFile(), InstantObject.class); + assertEquals(instant, unmarshaled.instant); + } + + @XmlRootElement(name = "instant-object") + @XmlAccessorType(XmlAccessType.FIELD) + public static class InstantObject { + + @XmlJavaTypeAdapter(XmlInstantAdapter.class) + private Instant instant; + + public InstantObject() { + } + + InstantObject(Instant instant) { + this.instant = instant; + } + } + +} diff --git a/scm-test/src/main/java/sonia/scm/store/InMemoryConfigurationStoreFactory.java b/scm-test/src/main/java/sonia/scm/store/InMemoryConfigurationStoreFactory.java index 2c5641bfd1..2180afdca2 100644 --- a/scm-test/src/main/java/sonia/scm/store/InMemoryConfigurationStoreFactory.java +++ b/scm-test/src/main/java/sonia/scm/store/InMemoryConfigurationStoreFactory.java @@ -42,8 +42,20 @@ package sonia.scm.store; */ public class InMemoryConfigurationStoreFactory implements ConfigurationStoreFactory { + private ConfigurationStore store; + + public InMemoryConfigurationStoreFactory() { + } + + public InMemoryConfigurationStoreFactory(ConfigurationStore store) { + this.store = store; + } + @Override public ConfigurationStore getStore(TypedStoreParameters storeParameters) { + if (store != null) { + return store; + } return new InMemoryConfigurationStore<>(); } } diff --git a/scm-test/src/main/java/sonia/scm/store/InMemoryDataStore.java b/scm-test/src/main/java/sonia/scm/store/InMemoryDataStore.java new file mode 100644 index 0000000000..06198d89bf --- /dev/null +++ b/scm-test/src/main/java/sonia/scm/store/InMemoryDataStore.java @@ -0,0 +1,53 @@ +package sonia.scm.store; + +import sonia.scm.security.KeyGenerator; +import sonia.scm.security.UUIDKeyGenerator; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * In memory store implementation of {@link DataStore}. + * + * @author Sebastian Sdorra + * + * @param type of stored object + */ +public class InMemoryDataStore implements DataStore { + + private final Map store = new HashMap<>(); + private KeyGenerator generator = new UUIDKeyGenerator(); + + @Override + public String put(T item) { + String key = generator.createKey(); + store.put(key, item); + return key; + } + + @Override + public void put(String id, T item) { + store.put(id, item); + } + + @Override + public Map getAll() { + return Collections.unmodifiableMap(store); + } + + @Override + public void clear() { + store.clear(); + } + + @Override + public void remove(String id) { + store.remove(id); + } + + @Override + public T get(String id) { + return store.get(id); + } +} diff --git a/scm-test/src/main/java/sonia/scm/store/InMemoryDataStoreFactory.java b/scm-test/src/main/java/sonia/scm/store/InMemoryDataStoreFactory.java new file mode 100644 index 0000000000..b0e95e9f9c --- /dev/null +++ b/scm-test/src/main/java/sonia/scm/store/InMemoryDataStoreFactory.java @@ -0,0 +1,26 @@ +package sonia.scm.store; + +/** + * In memory configuration store factory for testing purposes. + * + * @author Sebastian Sdorra + */ +public class InMemoryDataStoreFactory implements DataStoreFactory { + + private InMemoryDataStore store; + + public InMemoryDataStoreFactory() { + } + + public InMemoryDataStoreFactory(InMemoryDataStore store) { + this.store = store; + } + + @Override + public DataStore getStore(TypedStoreParameters storeParameters) { + if (store != null) { + return store; + } + return new InMemoryDataStore<>(); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/ScmServletModule.java b/scm-webapp/src/main/java/sonia/scm/ScmServletModule.java index 9555ad66b5..d7846dbac5 100644 --- a/scm-webapp/src/main/java/sonia/scm/ScmServletModule.java +++ b/scm-webapp/src/main/java/sonia/scm/ScmServletModule.java @@ -79,14 +79,14 @@ import sonia.scm.repository.spi.HookEventFacade; import sonia.scm.repository.xml.XmlRepositoryDAO; import sonia.scm.schedule.QuartzScheduler; import sonia.scm.schedule.Scheduler; +import sonia.scm.security.AccessTokenCookieIssuer; import sonia.scm.security.AuthorizationChangedEventProducer; import sonia.scm.security.CipherHandler; import sonia.scm.security.CipherUtil; import sonia.scm.security.ConfigurableLoginAttemptHandler; -import sonia.scm.security.DefaultJwtAccessTokenRefreshStrategy; +import sonia.scm.security.DefaultAccessTokenCookieIssuer; import sonia.scm.security.DefaultKeyGenerator; import sonia.scm.security.DefaultSecuritySystem; -import sonia.scm.security.JwtAccessTokenRefreshStrategy; import sonia.scm.security.KeyGenerator; import sonia.scm.security.LoginAttemptHandler; import sonia.scm.security.SecuritySystem; @@ -320,6 +320,7 @@ public class ScmServletModule extends ServletModule // bind events // bind(LastModifiedUpdateListener.class); + bind(AccessTokenCookieIssuer.class).to(DefaultAccessTokenCookieIssuer.class); bind(PushStateDispatcher.class).toProvider(PushStateDispatcherProvider.class); } diff --git a/scm-webapp/src/main/java/sonia/scm/security/AccessTokenCookieIssuer.java b/scm-webapp/src/main/java/sonia/scm/security/DefaultAccessTokenCookieIssuer.java similarity index 93% rename from scm-webapp/src/main/java/sonia/scm/security/AccessTokenCookieIssuer.java rename to scm-webapp/src/main/java/sonia/scm/security/DefaultAccessTokenCookieIssuer.java index bb1473dca6..fd3f0e0d6f 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/AccessTokenCookieIssuer.java +++ b/scm-webapp/src/main/java/sonia/scm/security/DefaultAccessTokenCookieIssuer.java @@ -51,12 +51,12 @@ import java.util.concurrent.TimeUnit; * @author Sebastian Sdorra * @since 2.0.0 */ -public final class AccessTokenCookieIssuer { +public final class DefaultAccessTokenCookieIssuer implements AccessTokenCookieIssuer { /** - * the logger for AccessTokenCookieIssuer + * the logger for DefaultAccessTokenCookieIssuer */ - private static final Logger LOG = LoggerFactory.getLogger(AccessTokenCookieIssuer.class); + private static final Logger LOG = LoggerFactory.getLogger(DefaultAccessTokenCookieIssuer.class); private final ScmConfiguration configuration; @@ -66,7 +66,7 @@ public final class AccessTokenCookieIssuer { * @param configuration scm main configuration */ @Inject - public AccessTokenCookieIssuer(ScmConfiguration configuration) { + public DefaultAccessTokenCookieIssuer(ScmConfiguration configuration) { this.configuration = configuration; } 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 8fb5929188..4418cb40a8 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/JwtAccessToken.java +++ b/scm-webapp/src/main/java/sonia/scm/security/JwtAccessToken.java @@ -87,6 +87,7 @@ public final class JwtAccessToken implements AccessToken { return ofNullable(claims.get(REFRESHABLE_UNTIL_CLAIM_KEY, Date.class)); } + @Override public Optional getParentKey() { return ofNullable(claims.get(PARENT_TOKEN_ID_CLAIM_KEY).toString()); } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/AuthenticationResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/AuthenticationResourceTest.java index 42428f9f77..1123dc94ce 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/AuthenticationResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/AuthenticationResourceTest.java @@ -18,6 +18,7 @@ import sonia.scm.security.AccessToken; import sonia.scm.security.AccessTokenBuilder; import sonia.scm.security.AccessTokenBuilderFactory; import sonia.scm.security.AccessTokenCookieIssuer; +import sonia.scm.security.DefaultAccessTokenCookieIssuer; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -46,7 +47,7 @@ public class AuthenticationResourceTest { @Mock private AccessTokenBuilder accessTokenBuilder; - private AccessTokenCookieIssuer cookieIssuer = new AccessTokenCookieIssuer(mock(ScmConfiguration.class)); + private AccessTokenCookieIssuer cookieIssuer = new DefaultAccessTokenCookieIssuer(mock(ScmConfiguration.class)); private static final String AUTH_JSON_TRILLIAN = "{\n" + "\t\"cookie\": true,\n" + diff --git a/scm-webapp/src/test/java/sonia/scm/security/AccessTokenCookieIssuerTest.java b/scm-webapp/src/test/java/sonia/scm/security/DefaultAccessTokenCookieIssuerTest.java similarity index 93% rename from scm-webapp/src/test/java/sonia/scm/security/AccessTokenCookieIssuerTest.java rename to scm-webapp/src/test/java/sonia/scm/security/DefaultAccessTokenCookieIssuerTest.java index 03cf174226..9c80cfc67b 100644 --- a/scm-webapp/src/test/java/sonia/scm/security/AccessTokenCookieIssuerTest.java +++ b/scm-webapp/src/test/java/sonia/scm/security/DefaultAccessTokenCookieIssuerTest.java @@ -20,11 +20,11 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @RunWith(MockitoJUnitRunner.class) -public class AccessTokenCookieIssuerTest { +public class DefaultAccessTokenCookieIssuerTest { private ScmConfiguration configuration; - private AccessTokenCookieIssuer issuer; + private DefaultAccessTokenCookieIssuer issuer; @Mock private HttpServletRequest request; @@ -41,7 +41,7 @@ public class AccessTokenCookieIssuerTest { @Before public void setUp() { configuration = new ScmConfiguration(); - issuer = new AccessTokenCookieIssuer(configuration); + issuer = new DefaultAccessTokenCookieIssuer(configuration); } @Test