diff --git a/core/pom.xml b/core/pom.xml
index acbd6aedbed731a2f1276834e3d3e374a22c10b0..875d11918615021be30d323b85de588b741340c1 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -6,7 +6,7 @@
org.eclipsefoundation
quarkus-commons
- 0.6.1-SNAPSHOT
+ 0.6.2-SNAPSHOT
../pom.xml
@@ -35,7 +35,7 @@
io.quarkus
- quarkus-elytron-security-oauth2
+ quarkus-oidc
io.quarkus
@@ -57,6 +57,11 @@
rest-assured
test
+
+ io.quarkus
+ quarkus-junit5-mockito
+ test
+
com.google.auto.value
diff --git a/core/src/main/java/org/eclipsefoundation/core/config/CSRFGeneratorProvider.java b/core/src/main/java/org/eclipsefoundation/core/config/CSRFGeneratorProvider.java
new file mode 100644
index 0000000000000000000000000000000000000000..f74b1822cefd422a7fe503ae8aa92af45e994a8d
--- /dev/null
+++ b/core/src/main/java/org/eclipsefoundation/core/config/CSRFGeneratorProvider.java
@@ -0,0 +1,28 @@
+package org.eclipsefoundation.core.config;
+
+import javax.enterprise.context.Dependent;
+import javax.enterprise.inject.Produces;
+
+import org.eclipsefoundation.core.model.CSRFGenerator;
+import org.eclipsefoundation.core.model.CSRFGenerator.DefaultCSRFGenerator;
+
+import io.quarkus.arc.DefaultBean;
+import io.quarkus.arc.Unremovable;
+
+/**
+ * Allows for the implementation of other mechanisms to provide CSRF tokens while enabling a default mechanism which
+ * uses random values at runtime to create sufficiently hardened values.
+ *
+ * @author Martin Lowe
+ *
+ */
+@Dependent
+@Unremovable
+public class CSRFGeneratorProvider {
+
+ @Produces
+ @DefaultBean
+ public CSRFGenerator generator() {
+ return new DefaultCSRFGenerator();
+ }
+}
diff --git a/core/src/main/java/org/eclipsefoundation/core/helper/CSRFHelper.java b/core/src/main/java/org/eclipsefoundation/core/helper/CSRFHelper.java
index ceab0cc49aa9d04ef2db38131447ec4c6dba05b9..5d0d986182e7cb8491b3c5c9c797632e744dbbd8 100644
--- a/core/src/main/java/org/eclipsefoundation/core/helper/CSRFHelper.java
+++ b/core/src/main/java/org/eclipsefoundation/core/helper/CSRFHelper.java
@@ -1,19 +1,20 @@
package org.eclipsefoundation.core.helper;
+import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
-import java.security.SecureRandom;
+import javax.enterprise.inject.Instance;
+import javax.inject.Inject;
import javax.inject.Singleton;
+import javax.servlet.http.HttpServletRequest;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.eclipsefoundation.core.exception.FinalUnauthorizedException;
import org.eclipsefoundation.core.model.AdditionalUserData;
+import org.eclipsefoundation.core.model.CSRFGenerator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import io.undertow.util.HexConverter;
-
/**
* Helper class for interacting with CSRF tokens within the server. Generates secure CSRF tokens and compares them to
* the copy that exists within the current session object.
@@ -25,66 +26,84 @@ import io.undertow.util.HexConverter;
public final class CSRFHelper {
public static final Logger LOGGER = LoggerFactory.getLogger(CSRFHelper.class);
- @ConfigProperty(name = "security.token.salt", defaultValue = "short-salt")
- String salt;
-
@ConfigProperty(name = "security.csrf.enabled", defaultValue = "false")
boolean csrfEnabled;
+ @ConfigProperty(name = "security.csrf.enabled.distributed-mode", defaultValue = "false")
+ Instance distributedMode;
- // cryptographically secure random number generator
- private SecureRandom rnd;
+ @Inject
+ CSRFGenerator generator;
/**
* Generate a new CSRF token that has been hardened to make it more difficult to predict.
*
* @return a cryptographically-secure CSRF token to use in a session.
*/
- public String getNewCSRFToken() {
- // use a random value salted with a configured static value
- byte[] bytes = rnd().generateSeed(24);
- String secureRnd = new String(bytes);
- // create a secure random secret to embed in the user session
- String preHash = secureRnd + salt;
+ public String getNewCSRFToken(HttpServletRequest httpServletRequest) {
+ return generator.getCSRFToken(httpServletRequest);
+ }
- // create new digest to hash the result
- MessageDigest md;
- try {
- md = MessageDigest.getInstance("SHA-256");
- } catch (NoSuchAlgorithmException e) {
- throw new IllegalStateException("Could not find SHA-256 algorithm to encode CSRF token", e);
+ /**
+ * Checks the current method of CSRF storage (session vs distributed) and returns the stored CSRF value.
+ *
+ * @param aud the current local session data
+ * @param httpServletRequest the request context
+ * @return the CSRF token for the current request.
+ */
+ public String getSessionCSRFToken(AdditionalUserData aud, HttpServletRequest httpServletRequest) {
+ if (distributedMode.get()) {
+ return generator.getCSRFToken(httpServletRequest);
+ } else {
+ return aud.getCsrf();
}
- // hash the results using the message digest
- byte[] array = md.digest(preHash.getBytes());
- // convert back to a hex string to act as a token
- return HexConverter.convertToHexString(array);
+ }
+
+ /**
+ * Legacy method for comapring CSRF values using the session data object. This has been upgraded to be
+ * {@link CSRFHelper#compareCSRF(String, String)} to better accommodate distributed mode options.
+ *
+ * @param aud the session data for current user
+ * @param passedToken the passed CSRF header data
+ * @throws FinalUnauthorizedException when CSRF token is missing in the user data, the passed header value, or does
+ * not match
+ */
+ @Deprecated(forRemoval = true)
+ public void compareCSRF(AdditionalUserData aud, String passedToken) {
+ compareCSRF(aud.getCsrf(), passedToken);
}
/**
* Compares the passed CSRF token to the token for the current user session.
*
- * @param aud session data for current user
- * @param passedCSRF the passed CSRF header data
+ * @param expectedToken the stored CSRF reference value
+ * @param passedToken the passed CSRF header data
* @throws FinalUnauthorizedException when CSRF token is missing in the user data, the passed header value, or does
* not match
*/
- public void compareCSRF(AdditionalUserData aud, String passedCSRF) {
+ public void compareCSRF(String expectedToken, String passedToken) {
if (csrfEnabled) {
- LOGGER.debug("Comparing following tokens:\n{}\n{}", aud == null ? null : aud.getCsrf(), passedCSRF);
- if (aud == null || aud.getCsrf() == null) {
+ LOGGER.debug("Comparing following tokens:\n{}\n{}", expectedToken == null ? null : expectedToken,
+ passedToken);
+ if (expectedToken == null) {
throw new FinalUnauthorizedException(
"CSRF token not generated for current request and is required, refusing request");
- } else if (passedCSRF == null) {
+ } else if (passedToken == null) {
throw new FinalUnauthorizedException("No CSRF token passed for current request, refusing request");
- } else if (!passedCSRF.equals(aud.getCsrf())) {
+ } else if (!verifyConstantTime(expectedToken, passedToken)) {
throw new FinalUnauthorizedException("CSRF tokens did not match, refusing request");
}
}
}
- private SecureRandom rnd() {
- if (this.rnd == null) {
- this.rnd = new SecureRandom(Long.toString(System.currentTimeMillis()).getBytes());
+ private static boolean verifyConstantTime(String expectedToken, String token) {
+ if (expectedToken == token) {
+ return true;
+ }
+ if (expectedToken == null || token == null) {
+ return false;
}
- return rnd;
+ return MessageDigest.isEqual(expectedToken.getBytes(StandardCharsets.US_ASCII),
+ token.getBytes(StandardCharsets.US_ASCII));
}
+
}
diff --git a/core/src/main/java/org/eclipsefoundation/core/helper/DateTimeHelper.java b/core/src/main/java/org/eclipsefoundation/core/helper/DateTimeHelper.java
index fc102a53e1d581de42e62ffbc1a5ff27e8d48c2d..53bd89eaeda1c02406666c672f240eaae9f27eb5 100644
--- a/core/src/main/java/org/eclipsefoundation/core/helper/DateTimeHelper.java
+++ b/core/src/main/java/org/eclipsefoundation/core/helper/DateTimeHelper.java
@@ -7,6 +7,7 @@
package org.eclipsefoundation.core.helper;
import java.time.ZoneId;
+import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
@@ -16,49 +17,75 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
- * Central implementation for handling date time conversion in the service.
- * Class uses Java8 DateTime formatters, creating an internal format that
- * represents RFC 3339
+ * Central implementation for handling date time conversion in the service. Class uses Java8 DateTime formatters,
+ * creating an internal format that represents RFC 3339
*
* @author Martin Lowe
*/
public class DateTimeHelper {
- private static final Logger LOGGER = LoggerFactory.getLogger(DateTimeHelper.class);
- public static final String RAW_RFC_3339_FORMAT = "uuuu-MM-dd'T'HH:mm:ssXXX";
- private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern(RAW_RFC_3339_FORMAT);
+ private static final Logger LOGGER = LoggerFactory.getLogger(DateTimeHelper.class);
+ public static final String RAW_RFC_3339_FORMAT = "uuuu-MM-dd'T'HH:mm:ssXXX";
+ private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern(RAW_RFC_3339_FORMAT);
- /**
- * Converts RFC 3339 compliant date string to date object. If non compliant
- * string is passed, issue is logged and null is returned. If negative UTC
- * timezone (-00:00) is passed, UTC time zone is assumed.
- *
- * @param dateString an RFC 3339 date string.
- * @return a date object representing time in date string, or null if not in RFC
- * 3339 format.
- */
- public static Date toRFC3339(String dateString) {
- if (dateString.isBlank()) return null;
- try {
- return Date.from(ZonedDateTime.parse(dateString, formatter).toInstant());
- } catch (DateTimeParseException e) {
- LOGGER.warn("Could not parse date from string '{}'", dateString, e);
- return null;
- }
- }
-
- /**
- * Converts passed date to RFC 3339 compliant date string. Time is adjusted to
- * be in UTC time.
- *
- * @param date the date object to convert to RFC 3339 format.
- * @return the RFC 3339 format date string.
- */
- public static String toRFC3339(Date date) {
- if (date == null) return null;
- return formatter.format(date.toInstant().atZone(ZoneId.of("UTC")));
- }
+ /**
+ * Converts RFC 3339 compliant date string to date object. If non compliant string is passed, issue is logged and
+ * null is returned. If negative UTC timezone (-00:00) is passed, UTC time zone is assumed.
+ *
+ * @param dateString an RFC 3339 date string.
+ * @return a date object representing time in date string, or null if not in RFC 3339 format.
+ */
+ public static Date toRFC3339(String dateString) {
+ if (dateString.isBlank())
+ return null;
+ try {
+ return Date.from(ZonedDateTime.parse(dateString, formatter).toInstant());
+ } catch (DateTimeParseException e) {
+ LOGGER.warn("Could not parse date from string '{}'", dateString, e);
+ return null;
+ }
+ }
- // hide constructor
- private DateTimeHelper() {
- }
+ /**
+ * Converts passed date to RFC 3339 compliant date string. Time is adjusted to be in UTC time.
+ *
+ * @param date the date object to convert to RFC 3339 format.
+ * @return the RFC 3339 format date string.
+ */
+ public static String toRFC3339(Date date) {
+ if (date == null)
+ return null;
+ return formatter.format(date.toInstant().atZone(ZoneId.of("UTC")));
+ }
+
+ /**
+ * Return the current instant as a datetime object zoned to UTC.
+ *
+ * @return the current localdatetime in UTC
+ */
+ public static ZonedDateTime now() {
+ return ZonedDateTime.now(ZoneOffset.UTC);
+ }
+
+ /**
+ * Gets the current epoch milli according to UTC
+ *
+ * @return the current epoch milli in UTC
+ */
+ public static long getMillis() {
+ return now().toInstant().toEpochMilli();
+ }
+
+ /**
+ * Returns the epoch milli of the time passed with UTC assumed.
+ *
+ * @param time the time object to retrieve epoch millis from
+ * @return the epoch millis of the passed time object in UTC
+ */
+ public static long getMillis(ZonedDateTime time) {
+ return time.toInstant().toEpochMilli();
+ }
+
+ // hide constructor
+ private DateTimeHelper() {
+ }
}
diff --git a/core/src/main/java/org/eclipsefoundation/core/model/CSRFGenerator.java b/core/src/main/java/org/eclipsefoundation/core/model/CSRFGenerator.java
new file mode 100644
index 0000000000000000000000000000000000000000..e7d51bfbdc372b0a4fb1bb28a3744848c77b8999
--- /dev/null
+++ b/core/src/main/java/org/eclipsefoundation/core/model/CSRFGenerator.java
@@ -0,0 +1,63 @@
+package org.eclipsefoundation.core.model;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Optional;
+import java.util.UUID;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.eclipse.microprofile.config.ConfigProvider;
+
+import io.undertow.util.HexConverter;
+
+/**
+ * Interface used to generate random hardened tokens for use in requests.
+ *
+ * @author Martin Lowe
+ *
+ */
+public interface CSRFGenerator {
+
+ /**
+ * Generates a random secure CSRF token to be used in requests. This token should be cryptographically secure and
+ * random to ensure that it cannot be generated from an external source.
+ *
+ * @param requestContext current request context that can be used for fingerprinting the request if required.
+ * @return a random encoded string token
+ */
+ public String getCSRFToken(HttpServletRequest httpServletRequest);
+
+ /**
+ * Default implementation of the CSRF generator, will use secure randoms generated on the fly at runtime to generate
+ * random seed values as base of token. These values will be hashed and salted to provide extra hardening of the
+ * value to make it harder to predict.
+ *
+ * @author Martin Lowe
+ *
+ */
+ public static class DefaultCSRFGenerator implements CSRFGenerator {
+
+ @Override
+ public String getCSRFToken(HttpServletRequest httpServletRequest) {
+ Optional salt = ConfigProvider.getConfig().getOptionalValue("security.csrf.token.salt",
+ String.class);
+ // use a random value salted with a configured static value
+ String secureBase = UUID.randomUUID().toString();
+ // create a secure random secret to embed in the user session
+ String preHash = secureBase + salt.orElseGet(() -> "short-salt");
+
+ // create new digest to hash the result
+ MessageDigest md;
+ try {
+ md = MessageDigest.getInstance("SHA-256");
+ } catch (NoSuchAlgorithmException e) {
+ throw new IllegalStateException("Could not find SHA-256 algorithm to encode CSRF token", e);
+ }
+ // hash the results using the message digest
+ byte[] array = md.digest(preHash.getBytes());
+ // convert back to a hex string to act as a token
+ return HexConverter.convertToHexString(array);
+ }
+ }
+}
diff --git a/core/src/main/java/org/eclipsefoundation/core/request/CSRFSecurityFilter.java b/core/src/main/java/org/eclipsefoundation/core/request/CSRFSecurityFilter.java
index f54b4c0528a6c069b9e167faa95cbe26d6991afc..2bd5f7cf9460da19f3504ef3973218385c52dafc 100644
--- a/core/src/main/java/org/eclipsefoundation/core/request/CSRFSecurityFilter.java
+++ b/core/src/main/java/org/eclipsefoundation/core/request/CSRFSecurityFilter.java
@@ -4,8 +4,10 @@ import java.io.IOException;
import javax.enterprise.inject.Instance;
import javax.inject.Inject;
+import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerRequestFilter;
+import javax.ws.rs.core.Context;
import javax.ws.rs.ext.Provider;
import org.eclipse.microprofile.config.inject.ConfigProperty;
@@ -30,7 +32,11 @@ public class CSRFSecurityFilter implements ContainerRequestFilter {
@ConfigProperty(name = "security.csrf.enabled", defaultValue = "false")
Instance csrfEnabled;
+ @ConfigProperty(name = "security.csrf.enabled.distributed-mode", defaultValue = "false")
+ Instance distributedMode;
+ @Context
+ HttpServletRequest httpServletRequest;
@Inject
Instance csrf;
@Inject
@@ -47,9 +53,13 @@ public class CSRFSecurityFilter implements ContainerRequestFilter {
String token = requestContext.getHeaderString(RequestHeaderNames.CSRF_TOKEN);
if (token == null || "".equals(token.trim())) {
throw new FinalUnauthorizedException("No CSRF token passed for mutation call, refusing connection");
+ } else if (distributedMode.get()) {
+ // distributed mode should return the stored token if it exists
+ csrf.get().compareCSRF(csrf.get().getNewCSRFToken(httpServletRequest), token);
} else {
// run comparison. If error, exception will be thrown
- csrf.get().compareCSRF(aud, token);
+ csrf.get().compareCSRF(aud.getCsrf(), token);
+
}
}
}
diff --git a/core/src/main/java/org/eclipsefoundation/core/response/CSRFHeaderFilter.java b/core/src/main/java/org/eclipsefoundation/core/response/CSRFHeaderFilter.java
index 95e413279ee242c8d901ac8832d1f733401096b0..3ba930f867ee0d57b16471f722e52b897e4b0f04 100644
--- a/core/src/main/java/org/eclipsefoundation/core/response/CSRFHeaderFilter.java
+++ b/core/src/main/java/org/eclipsefoundation/core/response/CSRFHeaderFilter.java
@@ -4,9 +4,11 @@ import java.io.IOException;
import javax.enterprise.inject.Instance;
import javax.inject.Inject;
+import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerResponseContext;
import javax.ws.rs.container.ContainerResponseFilter;
+import javax.ws.rs.core.Context;
import javax.ws.rs.ext.Provider;
import org.eclipse.microprofile.config.inject.ConfigProperty;
@@ -23,6 +25,11 @@ import org.eclipsefoundation.core.namespace.RequestHeaderNames;
public class CSRFHeaderFilter implements ContainerResponseFilter {
@ConfigProperty(name = "security.csrf.enabled", defaultValue = "false")
Instance csrfEnabled;
+ @ConfigProperty(name = "security.csrf.enabled.distributed-mode", defaultValue = "false")
+ Instance distributedMode;
+
+ @Context
+ HttpServletRequest httpServletRequest;
@Inject
CSRFHelper csrf;
@@ -34,12 +41,14 @@ public class CSRFHeaderFilter implements ContainerResponseFilter {
throws IOException {
// only attach if CSRF is enabled for the current runtime
if (csrfEnabled.get()) {
- // generate a new token if none is yet present
- if (aud.getCsrf() == null) {
- aud.setCsrf(csrf.getNewCSRFToken());
+ // generate the token
+ String token = csrf.getNewCSRFToken(httpServletRequest);
+ // store token in session if not distributed mode
+ if (aud.getCsrf() == null && !distributedMode.get()) {
+ aud.setCsrf(token);
}
// attach the current CSRF token as a header on the request
- responseContext.getHeaders().add(RequestHeaderNames.CSRF_TOKEN, aud.getCsrf());
+ responseContext.getHeaders().add(RequestHeaderNames.CSRF_TOKEN, token);
}
}
}
diff --git a/core/src/test/java/org/eclipsefoundation/core/authenticated/helper/CSRFHelperTest.java b/core/src/test/java/org/eclipsefoundation/core/authenticated/helper/CSRFHelperTest.java
index 7cc023d79135d25443a4805b639e1038fe9cb42f..a184e5cf80901953f1802a9edc4c4e853ef460da 100644
--- a/core/src/test/java/org/eclipsefoundation/core/authenticated/helper/CSRFHelperTest.java
+++ b/core/src/test/java/org/eclipsefoundation/core/authenticated/helper/CSRFHelperTest.java
@@ -1,13 +1,15 @@
package org.eclipsefoundation.core.authenticated.helper;
import javax.inject.Inject;
+import javax.servlet.http.HttpServletRequest;
import org.eclipsefoundation.core.exception.FinalUnauthorizedException;
import org.eclipsefoundation.core.helper.CSRFHelper;
-import org.eclipsefoundation.core.model.AdditionalUserData;
import org.eclipsefoundation.core.test.AuthenticatedTestProfile;
import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.junit.TestProfile;
@@ -23,55 +25,38 @@ class CSRFHelperTest {
@Inject
CSRFHelper csrf;
+
+ private static HttpServletRequest mockRequest;
+ @BeforeAll
+ public static void setup() {
+ CSRFHelperTest.mockRequest = Mockito.mock(HttpServletRequest.class);
+ }
+
@Test
void compareCSRF_validToken() {
// generate a token to use in test
- String csrfToken = csrf.getNewCSRFToken();
- // create session object with given CSRF token
- AdditionalUserData aud = new AdditionalUserData();
- aud.setCsrf(csrfToken);
+ String csrfToken = csrf.getNewCSRFToken(mockRequest);
// this should not throw as the tokens match
- Assertions.assertDoesNotThrow(() -> csrf.compareCSRF(aud, csrfToken));
+ Assertions.assertDoesNotThrow(() -> csrf.compareCSRF(csrfToken, csrfToken));
}
@Test
void compareCSRF_invalidToken() {
// generate a token to use in test
- String csrfToken = csrf.getNewCSRFToken();
- // create session object with given CSRF token
- AdditionalUserData aud = new AdditionalUserData();
- aud.setCsrf(csrfToken);
+ String csrfToken = csrf.getNewCSRFToken(mockRequest);
// this should throw as the tokens are not the same
- Assertions.assertThrows(FinalUnauthorizedException.class, () -> csrf.compareCSRF(aud, "some-other-value"));
+ Assertions.assertThrows(FinalUnauthorizedException.class, () -> csrf.compareCSRF(csrfToken, "some-other-value"));
}
@Test
void compareCSRF_noSubmittedToken() {
// generate a token to use in test
- String csrfToken = csrf.getNewCSRFToken();
- // create session object with given CSRF token
- AdditionalUserData aud = new AdditionalUserData();
- aud.setCsrf(csrfToken);
-
+ String csrfToken = csrf.getNewCSRFToken(mockRequest);
// this should throw as the tokens are not the same
- Assertions.assertThrows(FinalUnauthorizedException.class, () -> csrf.compareCSRF(aud, null));
-
- // reset token value as its cleared between requests
- aud.setCsrf(csrfToken);
- Assertions.assertThrows(FinalUnauthorizedException.class, () -> csrf.compareCSRF(aud, ""));
- }
-
- @Test
- void compareCSRF_noGeneratedToken() {
- // simulates a session object with no CSRF data (no previous calls)
- AdditionalUserData aud = new AdditionalUserData();
-
- String sampleCSRF = csrf.getNewCSRFToken();
- Assertions.assertThrows(FinalUnauthorizedException.class, () -> csrf.compareCSRF(aud, null));
- Assertions.assertThrows(FinalUnauthorizedException.class, () -> csrf.compareCSRF(aud, ""));
- Assertions.assertThrows(FinalUnauthorizedException.class, () -> csrf.compareCSRF(aud, sampleCSRF));
+ Assertions.assertThrows(FinalUnauthorizedException.class, () -> csrf.compareCSRF(csrfToken, null));
+ Assertions.assertThrows(FinalUnauthorizedException.class, () -> csrf.compareCSRF(csrfToken, ""));
}
}
diff --git a/core/src/test/java/org/eclipsefoundation/core/test/TestResource.java b/core/src/test/java/org/eclipsefoundation/core/test/TestResource.java
index 3a64b1a666de6177ae69fd58ab1629f8d1aaf896..1b96423c7a66775a4240be311d3cc9e5c2ddd48d 100644
--- a/core/src/test/java/org/eclipsefoundation/core/test/TestResource.java
+++ b/core/src/test/java/org/eclipsefoundation/core/test/TestResource.java
@@ -39,7 +39,7 @@ public class TestResource {
@GET
public Response get(@HeaderParam(value = RequestHeaderNames.CSRF_TOKEN) String passedCsrf) {
// check CSRF manually as its a get request
- csrf.compareCSRF(aud, passedCsrf);
+ csrf.compareCSRF(aud.getCsrf(), passedCsrf);
return Response.ok().build();
}
diff --git a/persistence/deployment/pom.xml b/persistence/deployment/pom.xml
index 1705a1ce9f1161bf111c667c5d2d5a0adbcd21ca..08ea49ccd8fb8c0d50aeae1f415bd62974c19522 100644
--- a/persistence/deployment/pom.xml
+++ b/persistence/deployment/pom.xml
@@ -1,75 +1,74 @@
-
- 4.0.0
-
- org.eclipsefoundation
- quarkus-persistence-parent
- 0.6.1-SNAPSHOT
-
- quarkus-persistence-deployment
- Persistence - Deployment
-
-
- io.quarkus
- quarkus-arc-deployment
-
-
- io.quarkus
- quarkus-core-deployment
-
-
- org.eclipsefoundation
- quarkus-persistence
- ${project.version}
-
-
- io.quarkus
- quarkus-resteasy-deployment
-
-
- io.quarkus
- quarkus-resteasy-jackson-deployment
-
-
- io.quarkus
- quarkus-undertow-deployment
-
-
- io.quarkus
- quarkus-smallrye-health-deployment
-
-
- io.quarkus
- quarkus-elytron-security-oauth2-deployment
-
-
- io.quarkus
- quarkus-cache-deployment
-
-
- io.quarkus
- quarkus-hibernate-orm-deployment
-
-
- io.quarkus
- quarkus-jdbc-mariadb-deployment
-
-
-
-
-
- maven-compiler-plugin
-
-
-
- io.quarkus
- quarkus-extension-processor
- ${quarkus.version}
-
-
-
-
-
-
-
+
+ 4.0.0
+
+ org.eclipsefoundation
+ quarkus-persistence-parent
+ 0.6.2-SNAPSHOT
+
+ quarkus-persistence-deployment
+ Persistence - Deployment
+
+
+ io.quarkus
+ quarkus-arc-deployment
+
+
+ io.quarkus
+ quarkus-core-deployment
+
+
+ org.eclipsefoundation
+ quarkus-persistence
+ ${project.version}
+
+
+ io.quarkus
+ quarkus-resteasy-deployment
+
+
+ io.quarkus
+ quarkus-resteasy-jackson-deployment
+
+
+ io.quarkus
+ quarkus-undertow-deployment
+
+
+ io.quarkus
+ quarkus-smallrye-health-deployment
+
+
+ io.quarkus
+ quarkus-oidc-deployment
+
+
+ io.quarkus
+ quarkus-cache-deployment
+
+
+ io.quarkus
+ quarkus-hibernate-orm-deployment
+
+
+ io.quarkus
+ quarkus-jdbc-mariadb-deployment
+
+
+
+
+
+ maven-compiler-plugin
+
+
+
+ io.quarkus
+ quarkus-extension-processor
+ ${quarkus.version}
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/persistence/deployment/src/main/java/org/eclipsefoundation/persistence/deployment/QuarkusPersistenceProcessor.java b/persistence/deployment/src/main/java/org/eclipsefoundation/persistence/deployment/QuarkusPersistenceProcessor.java
index 91a48c2651b2d46fe6f025980557649b5127ea53..85434c8a1ee2c5455f56370c9ac91bb5690f4f4e 100644
--- a/persistence/deployment/src/main/java/org/eclipsefoundation/persistence/deployment/QuarkusPersistenceProcessor.java
+++ b/persistence/deployment/src/main/java/org/eclipsefoundation/persistence/deployment/QuarkusPersistenceProcessor.java
@@ -3,12 +3,16 @@ package org.eclipsefoundation.persistence.deployment;
import java.util.Set;
import javax.enterprise.context.ApplicationScoped;
+import javax.enterprise.context.Dependent;
+import javax.inject.Singleton;
+import org.eclipsefoundation.persistence.config.DistributedCSRFProvider;
import org.eclipsefoundation.persistence.config.ParameterizedSQLStatementBuilderConfiguration;
import org.eclipsefoundation.persistence.dao.impl.DefaultHibernateDao;
import org.eclipsefoundation.persistence.dao.impl.PlaceholderPersistenceDao;
import org.eclipsefoundation.persistence.dto.BareNode;
import org.eclipsefoundation.persistence.dto.NodeBase;
+import org.eclipsefoundation.persistence.dto.DistributedCSRFToken.DistributedCSRFTokenFilter;
import org.eclipsefoundation.persistence.model.ParameterizedSQLStatementBuilder;
import org.eclipsefoundation.persistence.response.ResultsHeaderCachedResponse;
import org.eclipsefoundation.persistence.service.impl.DefaultFilterService;
@@ -25,8 +29,7 @@ class QuarkusPersistenceProcessor {
@BuildStep
RuntimeInitializedClassBuildItem parameterizedSQLStatementBuilderConfiguration() {
- return new RuntimeInitializedClassBuildItem(
- ParameterizedSQLStatementBuilder.class.getCanonicalName());
+ return new RuntimeInitializedClassBuildItem(ParameterizedSQLStatementBuilder.class.getCanonicalName());
}
/**
@@ -50,6 +53,13 @@ class QuarkusPersistenceProcessor {
// register the Hibernate DAO
beanProducer.produce(
AdditionalBeanBuildItem.builder().setUnremovable().addBeanClass(DefaultHibernateDao.class).build());
+ // distributed provider makes use of DAO, and can only be used if persistence units exist
+ beanProducer.produce(
+ AdditionalBeanBuildItem.builder().setUnremovable().addBeanClass(DistributedCSRFProvider.class)
+ .setDefaultScope(DotName.createSimple(Dependent.class.getName())).build());
+ beanProducer.produce(
+ AdditionalBeanBuildItem.builder().setUnremovable().addBeanClass(DistributedCSRFTokenFilter.class)
+ .setDefaultScope(DotName.createSimple(Singleton.class.getName())).build());
} else {
// register placeholder to unblock downstream dependencies. This can be removed
// if a search extension is implemented
diff --git a/persistence/pom.xml b/persistence/pom.xml
index 567aba21d48f3322c4653092c29a7aad042bd2da..417fec69f8fad4393bad4624b9ad3511be77f0ea 100644
--- a/persistence/pom.xml
+++ b/persistence/pom.xml
@@ -4,13 +4,13 @@
4.0.0
org.eclipsefoundation
quarkus-persistence-parent
- 0.6.1-SNAPSHOT
+ 0.6.2-SNAPSHOT
pom
Persistence - Parent
org.eclipsefoundation
quarkus-commons
- 0.6.1-SNAPSHOT
+ 0.6.2-SNAPSHOT
../pom.xml
diff --git a/persistence/runtime/pom.xml b/persistence/runtime/pom.xml
index cc5e1af21e06017ee4e78800f5ec40254b31de9e..23c29385e21a7e152fe919ebaa0f439e4f7eb641 100644
--- a/persistence/runtime/pom.xml
+++ b/persistence/runtime/pom.xml
@@ -4,7 +4,7 @@
org.eclipsefoundation
quarkus-persistence-parent
- 0.6.1-SNAPSHOT
+ 0.6.2-SNAPSHOT
quarkus-persistence
Persistence - Runtime
diff --git a/persistence/runtime/src/main/java/org/eclipsefoundation/persistence/config/DistributedCSRFProvider.java b/persistence/runtime/src/main/java/org/eclipsefoundation/persistence/config/DistributedCSRFProvider.java
new file mode 100644
index 0000000000000000000000000000000000000000..91f9ad9932f7d4bc1a8f0356a0b9bb0ad3ebab9a
--- /dev/null
+++ b/persistence/runtime/src/main/java/org/eclipsefoundation/persistence/config/DistributedCSRFProvider.java
@@ -0,0 +1,25 @@
+package org.eclipsefoundation.persistence.config;
+
+import javax.enterprise.context.ApplicationScoped;
+import javax.enterprise.inject.Produces;
+
+import org.eclipsefoundation.core.model.CSRFGenerator;
+import org.eclipsefoundation.persistence.dao.impl.DefaultHibernateDao;
+import org.eclipsefoundation.persistence.dto.DistributedCSRFToken.DistributedCSRFTokenFilter;
+import org.eclipsefoundation.persistence.model.DistributedCSRFGenerator;
+
+import io.quarkus.arc.Priority;
+import io.quarkus.arc.properties.IfBuildProperty;
+import io.quarkus.arc.properties.UnlessBuildProperty;
+
+public class DistributedCSRFProvider {
+
+ @Produces
+ @Priority(5)
+ @ApplicationScoped
+ @IfBuildProperty(name = "security.csrf.enabled.distributed-mode", stringValue = "true")
+ @UnlessBuildProperty(name = "security.csrf.distributed-mode.default-provider", stringValue = "false", enableIfMissing = true)
+ public CSRFGenerator distributedGenerator(DefaultHibernateDao defaultDao, DistributedCSRFTokenFilter filter) {
+ return new DistributedCSRFGenerator(defaultDao, filter);
+ }
+}
diff --git a/persistence/runtime/src/main/java/org/eclipsefoundation/persistence/dto/DistributedCSRFToken.java b/persistence/runtime/src/main/java/org/eclipsefoundation/persistence/dto/DistributedCSRFToken.java
new file mode 100644
index 0000000000000000000000000000000000000000..134888762cc51743839ad7e21dd6c08362739a9d
--- /dev/null
+++ b/persistence/runtime/src/main/java/org/eclipsefoundation/persistence/dto/DistributedCSRFToken.java
@@ -0,0 +1,145 @@
+package org.eclipsefoundation.persistence.dto;
+
+import java.time.ZonedDateTime;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import javax.persistence.Entity;
+import javax.persistence.Id;
+import javax.persistence.Table;
+import javax.ws.rs.core.MultivaluedMap;
+
+import org.eclipsefoundation.persistence.dto.filter.DtoFilter;
+import org.eclipsefoundation.persistence.model.DistributedCSRFGenerator;
+import org.eclipsefoundation.persistence.model.DtoTable;
+import org.eclipsefoundation.persistence.model.ParameterizedSQLStatement;
+import org.eclipsefoundation.persistence.model.ParameterizedSQLStatementBuilder;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+
+@Entity
+@Table
+public class DistributedCSRFToken extends BareNode {
+ public static final DtoTable TABLE = new DtoTable(DistributedCSRFToken.class, "dct");
+
+ @Id
+ private String token;
+ private String userAgent;
+ private String ipAddress;
+ private String user;
+ private ZonedDateTime timestamp;
+
+ @Override
+ @JsonIgnore
+ public Object getId() {
+ return getToken();
+ }
+
+ /**
+ * @return the token
+ */
+ public String getToken() {
+ return token;
+ }
+
+ /**
+ * @param token the token to set
+ */
+ public void setToken(String token) {
+ this.token = token;
+ }
+
+ /**
+ * @return the userAgent
+ */
+ public String getUserAgent() {
+ return userAgent;
+ }
+
+ /**
+ * @param userAgent the userAgent to set
+ */
+ public void setUserAgent(String userAgent) {
+ this.userAgent = userAgent;
+ }
+
+ /**
+ * @return the ipAddress
+ */
+ public String getIpAddress() {
+ return ipAddress;
+ }
+
+ /**
+ * @param ipAddress the ipAddress to set
+ */
+ public void setIpAddress(String ipAddress) {
+ this.ipAddress = ipAddress;
+ }
+
+ /**
+ * @return the user
+ */
+ public String getUser() {
+ return user;
+ }
+
+ /**
+ * @param user the user to set
+ */
+ public void setUser(String user) {
+ this.user = user;
+ }
+
+ /**
+ * @return the timestamp
+ */
+ public ZonedDateTime getTimestamp() {
+ return timestamp;
+ }
+
+ /**
+ * @param timestamp the timestamp to set
+ */
+ public void setTimestamp(ZonedDateTime timestamp) {
+ this.timestamp = timestamp;
+ }
+
+ @Singleton
+ public static class DistributedCSRFTokenFilter implements DtoFilter {
+ @Inject
+ ParameterizedSQLStatementBuilder builder;
+
+ @Override
+ public ParameterizedSQLStatement getFilters(MultivaluedMap params, boolean isRoot) {
+ ParameterizedSQLStatement stmt = builder.build(TABLE);
+ if (isRoot) {
+ // IP check
+ String ip = params.getFirst(DistributedCSRFGenerator.IP_PARAM);
+ if (ip != null) {
+ stmt.addClause(new ParameterizedSQLStatement.Clause(TABLE.getAlias() + ".ipAddress = ?",
+ new Object[] { ip }));
+ }
+ // UA check
+ String ua = params.getFirst(DistributedCSRFGenerator.USERAGENT_PARAM);
+ if (ua != null) {
+ stmt.addClause(new ParameterizedSQLStatement.Clause(TABLE.getAlias() + ".userAgent = ?",
+ new Object[] { ua }));
+ }
+ // user check
+ String user = params.getFirst(DistributedCSRFGenerator.USER_PARAM);
+ if (user != null) {
+ stmt.addClause(new ParameterizedSQLStatement.Clause(TABLE.getAlias() + ".user = ?",
+ new Object[] { user }));
+ }
+ }
+
+ return stmt;
+ }
+
+ @Override
+ public Class getType() {
+ return DistributedCSRFToken.class;
+ }
+ }
+}
diff --git a/persistence/runtime/src/main/java/org/eclipsefoundation/persistence/model/DistributedCSRFGenerator.java b/persistence/runtime/src/main/java/org/eclipsefoundation/persistence/model/DistributedCSRFGenerator.java
new file mode 100644
index 0000000000000000000000000000000000000000..7124aa93a051569f51febf9c67dc761d9c9edbed
--- /dev/null
+++ b/persistence/runtime/src/main/java/org/eclipsefoundation/persistence/model/DistributedCSRFGenerator.java
@@ -0,0 +1,112 @@
+package org.eclipsefoundation.persistence.model;
+
+import java.net.URI;
+import java.security.Principal;
+import java.util.Arrays;
+import java.util.List;
+import java.util.StringTokenizer;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.core.MultivaluedMap;
+
+import org.eclipsefoundation.core.helper.DateTimeHelper;
+import org.eclipsefoundation.core.model.CSRFGenerator.DefaultCSRFGenerator;
+import org.eclipsefoundation.core.model.FlatRequestWrapper;
+import org.eclipsefoundation.persistence.dao.PersistenceDao;
+import org.eclipsefoundation.persistence.dto.DistributedCSRFToken;
+import org.eclipsefoundation.persistence.dto.DistributedCSRFToken.DistributedCSRFTokenFilter;
+import org.jboss.resteasy.specimpl.MultivaluedMapImpl;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import io.undertow.httpcore.HttpHeaderNames;
+
+/**
+ * Using IP address and useragent, create a fingerprint for the current user that will be used to access a stored
+ * distributed CSRF token. If one does not yet exist, one will be generated using the default method, stored for future
+ * reference, and returned.
+ *
+ * @author Martin Lowe
+ *
+ */
+public class DistributedCSRFGenerator extends DefaultCSRFGenerator {
+ public static final Logger LOGGER = LoggerFactory.getLogger(DistributedCSRFGenerator.class);
+
+ public static final String USER_PARAM = "user";
+ public static final String USERAGENT_PARAM = "useragent";
+ public static final String IP_PARAM = "ip";
+
+ private PersistenceDao manager;
+ private DistributedCSRFTokenFilter filter;
+
+ public DistributedCSRFGenerator(PersistenceDao manager, DistributedCSRFTokenFilter filter) {
+ LOGGER.info("Distributed CSRF strategy is enabled.");
+ this.manager = manager;
+ this.filter = filter;
+ }
+
+ @Override
+ public String getCSRFToken(HttpServletRequest httpServletRequest) {
+ // generate a non-root query
+ MultivaluedMap params = getQueryParameters(httpServletRequest);
+ RDBMSQuery q = new RDBMSQuery<>(
+ new FlatRequestWrapper(URI.create(httpServletRequest.getRequestURL().toString())), filter, params);
+ q.setRoot(false);
+ // attempt to read existing tokens
+ List tokens = manager.get(q);
+ if (tokens.isEmpty()) {
+ // generate a new token to be stored in the distributed persistence table
+ String token = super.getCSRFToken(httpServletRequest);
+ DistributedCSRFToken t = new DistributedCSRFToken();
+ t.setIpAddress(params.getFirst(IP_PARAM));
+ t.setToken(token);
+ t.setUser(params.getFirst(USER_PARAM));
+ t.setUserAgent(params.getFirst(USERAGENT_PARAM));
+ t.setTimestamp(DateTimeHelper.now());
+ List createdTokens = manager.add(q, Arrays.asList(t));
+ if (!createdTokens.isEmpty()) {
+ LOGGER.trace("Generated distributed CSRF token for current fingerprint; IP: {}, UA: {}, token: {}",
+ t.getIpAddress(), t.getUserAgent(), token);
+ return token;
+ }
+ } else {
+ DistributedCSRFToken t = tokens.get(0);
+ LOGGER.trace("Found existing distributed CSRF token for current fingerprint; IP: {}, UA: {}, token: {}",
+ t.getIpAddress(), t.getUserAgent(), t.getToken());
+ return t.getToken();
+ }
+ LOGGER.info("Error while generating/retrieving CSRF entry for current user; IP: {}, UA: {}",
+ params.getFirst(IP_PARAM), params.getFirst(USERAGENT_PARAM));
+ return null;
+ }
+
+ public void destroyCurrentToken(HttpServletRequest httpServletRequest) {
+ MultivaluedMap params = getQueryParameters(httpServletRequest);
+ LOGGER.error("Destroying retrieving CSRF entry for current user; IP: {}, UA: {}", params.getFirst(IP_PARAM),
+ params.getFirst(USERAGENT_PARAM));
+ manager.delete(new RDBMSQuery<>(
+ new FlatRequestWrapper(URI.create(httpServletRequest.getRequestURL().toString())), filter, params));
+ }
+
+ private MultivaluedMap getQueryParameters(HttpServletRequest httpServletRequest) {
+ // get the markers used to identify a user (outside of a unique session ID)
+ String ipAddr = getClientIpAddress(httpServletRequest);
+ String userAgent = httpServletRequest.getHeader(HttpHeaderNames.USER_AGENT);
+ Principal user = httpServletRequest.getUserPrincipal();
+ MultivaluedMap params = new MultivaluedMapImpl<>();
+ params.add(IP_PARAM, ipAddr);
+ params.add(USERAGENT_PARAM, userAgent);
+ params.add(USER_PARAM, user != null ? user.getName() : null);
+ return params;
+ }
+
+ private String getClientIpAddress(HttpServletRequest request) {
+ String forwardedFor = request.getHeader(HttpHeaderNames.X_FORWARDED_FOR);
+ if (forwardedFor == null) {
+ return request.getRemoteAddr();
+ } else {
+ // get the first most address in forwarded chain
+ return new StringTokenizer(forwardedFor, ",").nextToken().trim();
+ }
+ }
+}
diff --git a/pom.xml b/pom.xml
index 7ded90c8d997292a3b898b8a58688104d519465e..9a57239b8ff4d66d64e03021be13111c7065b7ae 100644
--- a/pom.xml
+++ b/pom.xml
@@ -4,7 +4,7 @@
org.eclipsefoundation
quarkus-commons
Java SDK Commons
- 0.6.1-SNAPSHOT
+ 0.6.2-SNAPSHOT
pom
3.8.1
diff --git a/search/pom.xml b/search/pom.xml
index c20670b64b0ee76566820c11a620a95b2841c355..07f0a2b64a519eba2af4a79bd00ba8e94ceb45d4 100644
--- a/search/pom.xml
+++ b/search/pom.xml
@@ -6,7 +6,7 @@
org.eclipsefoundation
quarkus-commons
- 0.6.1-SNAPSHOT
+ 0.6.2-SNAPSHOT
../pom.xml