diff --git a/example/src/main/java/eu/webeid/example/config/SameSiteCookieConfiguration.java b/example/src/main/java/eu/webeid/example/config/CookieConfiguration.java similarity index 64% rename from example/src/main/java/eu/webeid/example/config/SameSiteCookieConfiguration.java rename to example/src/main/java/eu/webeid/example/config/CookieConfiguration.java index 74602523..8cb28694 100644 --- a/example/src/main/java/eu/webeid/example/config/SameSiteCookieConfiguration.java +++ b/example/src/main/java/eu/webeid/example/config/CookieConfiguration.java @@ -23,13 +23,16 @@ package eu.webeid.example.config; import org.apache.tomcat.util.http.Rfc6265CookieProcessor; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.boot.web.embedded.tomcat.TomcatContextCustomizer; +import org.springframework.boot.web.server.WebServerFactoryCustomizer; +import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration -public class SameSiteCookieConfiguration implements WebMvcConfigurer { +public class CookieConfiguration implements WebMvcConfigurer { @Bean public TomcatContextCustomizer configureSameSiteCookies() { @@ -39,4 +42,16 @@ public TomcatContextCustomizer configureSameSiteCookies() { context.setCookieProcessor(cookieProcessor); }; } + + @Bean + @ConditionalOnExpression("'${web-eid-auth-token.validation.local-origin}'.startsWith('http:')") + public WebServerFactoryCustomizer httpSessionCookieCustomizer() { + return factory -> factory.addInitializers(servletContext -> servletContext.getSessionCookieConfig().setName("JSESSIONID")); + } + + @Bean + @ConditionalOnExpression("'${web-eid-auth-token.validation.local-origin}'.startsWith('https:')") + public WebServerFactoryCustomizer httpsSessionCookieCustomizer() { + return factory -> factory.addInitializers(servletContext -> servletContext.getSessionCookieConfig().setName("__Host-JSESSIONID")); + } } diff --git a/example/src/main/java/eu/webeid/example/config/YAMLConfig.java b/example/src/main/java/eu/webeid/example/config/YAMLConfig.java index 234a8569..d42cc3e8 100644 --- a/example/src/main/java/eu/webeid/example/config/YAMLConfig.java +++ b/example/src/main/java/eu/webeid/example/config/YAMLConfig.java @@ -22,6 +22,13 @@ package eu.webeid.example.config; +import java.net.InetAddress; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.UnknownHostException; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; @@ -32,6 +39,8 @@ @ConfigurationProperties(prefix = "web-eid-auth-token.validation") public class YAMLConfig { + private static final Logger LOG = LoggerFactory.getLogger(YAMLConfig.class); + @Value("local-origin") private String localOrigin; @@ -49,6 +58,22 @@ public String getLocalOrigin() { } public void setLocalOrigin(String localOrigin) { + if (StringUtils.endsWith(localOrigin, "/")) { + throw new IllegalArgumentException("Configuration parameter local-origin cannot end with '/': " + localOrigin); + } + if (StringUtils.startsWith(localOrigin, "http:")) { + try { + if (InetAddress.getByName(new URI(localOrigin).getHost()).isLoopbackAddress()) { + this.localOrigin = localOrigin.replaceFirst("^http:", "https:"); + LOG.warn("Configuration local-origin contains http protocol {}, which is not supported. Replacing it with secure {}", localOrigin, this.localOrigin); + return; + } + } catch (URISyntaxException e) { + LOG.error("Configuration parameter origin-local does not contain an URL: {}", localOrigin, e); + } catch (UnknownHostException e) { + LOG.error("Unable to determine if origin-local {} is loopback address", localOrigin, e); + } + } this.localOrigin = localOrigin; } diff --git a/example/src/main/resources/application.properties b/example/src/main/resources/application.properties index 7d70ac4e..cbb42d2a 100644 --- a/example/src/main/resources/application.properties +++ b/example/src/main/resources/application.properties @@ -1,2 +1 @@ spring.profiles.active=dev -server.servlet.session.cookie.name=__Host-JSESSIONID \ No newline at end of file diff --git a/example/src/test/java/eu/webeid/example/config/CookieHttpTest.java b/example/src/test/java/eu/webeid/example/config/CookieHttpTest.java new file mode 100644 index 00000000..fa406b6d --- /dev/null +++ b/example/src/test/java/eu/webeid/example/config/CookieHttpTest.java @@ -0,0 +1,28 @@ +package eu.webeid.example.config; + +import static org.assertj.core.api.Assertions.assertThat; + +import jakarta.servlet.ServletContext; +import jakarta.servlet.SessionCookieConfig; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext; +import org.springframework.test.context.TestPropertySource; + +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +@TestPropertySource(properties = {"web-eid-auth-token.validation.local-origin=http://localhost"}) +class CookieHttpTest { + + @Autowired + private ServletWebServerApplicationContext context; + + @Test + void whenLocalOriginStartsWithHttp_thenCookeDoesNotHaveHostPrefix() { + ServletContext servletContext = context.getServletContext(); + SessionCookieConfig cookieConfig = servletContext.getSessionCookieConfig(); + assertThat(cookieConfig.getName()).isEqualTo("JSESSIONID"); + } + +} diff --git a/example/src/test/java/eu/webeid/example/config/CookieHttpsTest.java b/example/src/test/java/eu/webeid/example/config/CookieHttpsTest.java new file mode 100644 index 00000000..1040093a --- /dev/null +++ b/example/src/test/java/eu/webeid/example/config/CookieHttpsTest.java @@ -0,0 +1,28 @@ +package eu.webeid.example.config; + +import static org.assertj.core.api.Assertions.assertThat; + +import jakarta.servlet.ServletContext; +import jakarta.servlet.SessionCookieConfig; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext; +import org.springframework.test.context.TestPropertySource; + +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +@TestPropertySource(properties = {"web-eid-auth-token.validation.local-origin=https://localhost"}) +class CookieHttpsTest { + + @Autowired + private ServletWebServerApplicationContext context; + + @Test + void whenLocalOriginStartsWithHttp_thenCookeDoesNotHaveHostPrefix() { + ServletContext servletContext = context.getServletContext(); + SessionCookieConfig cookieConfig = servletContext.getSessionCookieConfig(); + assertThat(cookieConfig.getName()).isEqualTo("__Host-JSESSIONID"); + } + +} diff --git a/example/src/test/java/eu/webeid/example/config/YAMLConfigTest.java b/example/src/test/java/eu/webeid/example/config/YAMLConfigTest.java new file mode 100644 index 00000000..62a80c03 --- /dev/null +++ b/example/src/test/java/eu/webeid/example/config/YAMLConfigTest.java @@ -0,0 +1,69 @@ +package eu.webeid.example.config; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.apache.commons.lang3.StringUtils; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class YAMLConfigTest { + + @ValueSource(strings = { + "http://localhost", + "http://localhost:8080", + "http://127.0.0.1", + "http://127.0.0.1:8080", + "http://[::1]", + "http://[::1]:8080" + }) + @ParameterizedTest + void givenLocalOriginHttpLoopbackAddress_whenParsingLocalOrigin_thenItIsReplacedWithHttps(String origin) { + YAMLConfig yamlConfig = new YAMLConfig(); + yamlConfig.setLocalOrigin(origin); + assertThat(yamlConfig.getLocalOrigin()).isEqualTo(origin.replaceFirst("^http:", "https:")); + } + + @ValueSource(strings = { + "https://localhost", + "https://localhost:8080", + "https://127.0.0.1", + "https://127.0.0.1:8080", + "https://[::1]", + "https://[::1]:8080", + }) + @ParameterizedTest + void givenLocalOriginHttpsLoopbackAddress_whenParsingLocalOrigin_thenOriginalIsKept(String origin) { + YAMLConfig yamlConfig = new YAMLConfig(); + yamlConfig.setLocalOrigin(origin); + assertThat(yamlConfig.getLocalOrigin()).isEqualTo(origin); + } + + @ValueSource(strings = { + "http://somename.app", + "http://somename.app:8080", + "http://8.8.8.8", + "http://8.8.8.8:8080", + "http://[2001:4860:4860::8888]", + "http://[2001:4860:4860::8888]:8080", + }) + @ParameterizedTest + void givenLocalOriginHttpNonLoopbackAddress_whenParsingLocalOrigin_thenOriginalIsKept(String origin) { + YAMLConfig yamlConfig = new YAMLConfig(); + yamlConfig.setLocalOrigin(origin); + assertThat(yamlConfig.getLocalOrigin()).isEqualTo(origin); + } + + @ValueSource(strings = { + "https://localhost/", + "https://localhost:8080/" + }) + @ParameterizedTest + void givenLocalOriginThatEndsWithSlash_whenParsingLocalOrigin_thenExceptionIsThrown(String origin) { + YAMLConfig yamlConfig = new YAMLConfig(); + assertThatThrownBy(() -> yamlConfig.setLocalOrigin(origin)) + .hasMessage("Configuration parameter local-origin cannot end with '/': " + origin); + } +}