From 444685234b5b29741ba8785777a5b4a864c515ea Mon Sep 17 00:00:00 2001 From: Martin Lowe Date: Mon, 7 Mar 2022 13:14:35 -0500 Subject: [PATCH 1/3] Add support for distributed CSRF tokens (via persistence) Current standard CSRF functionality is default, with the distributed CSRF tokens able to be enabled through distributed mode flag. Additionally, adding persistence DTO to your default datasource and adding the subsequent table to your database is required. Some extra code is required to enable this functionality on a non-default datasource. Ported some code for centralized date time functionalities to standardize access to zoned datetimes. --- core/pom.xml | 9 +- .../core/config/CSRFGeneratorProvider.java | 28 ++++ .../core/helper/CSRFHelper.java | 43 +---- .../core/helper/DateTimeHelper.java | 105 ++++++++----- .../core/model/CSRFGenerator.java | 72 +++++++++ .../core/response/CSRFHeaderFilter.java | 17 +- .../authenticated/helper/CSRFHelperTest.java | 18 ++- persistence/deployment/pom.xml | 147 +++++++++--------- .../QuarkusPersistenceProcessor.java | 14 +- persistence/pom.xml | 4 +- persistence/runtime/pom.xml | 2 +- .../config/DistributedCSRFProvider.java | 21 +++ .../persistence/dto/DistributedCSRFToken.java | 145 +++++++++++++++++ .../model/DistributedCSRFGenerator.java | 112 +++++++++++++ pom.xml | 2 +- search/pom.xml | 2 +- 16 files changed, 575 insertions(+), 166 deletions(-) create mode 100644 core/src/main/java/org/eclipsefoundation/core/config/CSRFGeneratorProvider.java create mode 100644 core/src/main/java/org/eclipsefoundation/core/model/CSRFGenerator.java create mode 100644 persistence/runtime/src/main/java/org/eclipsefoundation/persistence/config/DistributedCSRFProvider.java create mode 100644 persistence/runtime/src/main/java/org/eclipsefoundation/persistence/dto/DistributedCSRFToken.java create mode 100644 persistence/runtime/src/main/java/org/eclipsefoundation/persistence/model/DistributedCSRFGenerator.java diff --git a/core/pom.xml b/core/pom.xml index acbd6ae..875d119 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 0000000..f74b182 --- /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 ceab0cc..0caf278 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,16 @@ package org.eclipsefoundation.core.helper; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; - +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,38 +22,19 @@ 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; - // 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; - - // 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); + public String getNewCSRFToken(HttpServletRequest httpServletRequest) { + return generator.getCSRFToken(httpServletRequest); } /** @@ -80,11 +58,4 @@ public final class CSRFHelper { } } } - - private SecureRandom rnd() { - if (this.rnd == null) { - this.rnd = new SecureRandom(Long.toString(System.currentTimeMillis()).getBytes()); - } - return rnd; - } } 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 fc102a5..53bd89e 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 0000000..6ae7ea5 --- /dev/null +++ b/core/src/main/java/org/eclipsefoundation/core/model/CSRFGenerator.java @@ -0,0 +1,72 @@ +package org.eclipsefoundation.core.model; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Optional; + +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 { + private SecureRandom rnd; + + @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 + 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.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); + } + + private SecureRandom rnd() { + if (this.rnd == null) { + this.rnd = new SecureRandom(Long.toString(System.currentTimeMillis()).getBytes()); + } + return rnd; + } + } +} 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 95e4132..3ba930f 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 7cc023d..a56e392 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,16 @@ 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,11 +26,18 @@ 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(); + String csrfToken = csrf.getNewCSRFToken(mockRequest); // create session object with given CSRF token AdditionalUserData aud = new AdditionalUserData(); aud.setCsrf(csrfToken); @@ -39,7 +49,7 @@ class CSRFHelperTest { @Test void compareCSRF_invalidToken() { // generate a token to use in test - String csrfToken = csrf.getNewCSRFToken(); + String csrfToken = csrf.getNewCSRFToken(mockRequest); // create session object with given CSRF token AdditionalUserData aud = new AdditionalUserData(); aud.setCsrf(csrfToken); @@ -51,7 +61,7 @@ class CSRFHelperTest { @Test void compareCSRF_noSubmittedToken() { // generate a token to use in test - String csrfToken = csrf.getNewCSRFToken(); + String csrfToken = csrf.getNewCSRFToken(mockRequest); // create session object with given CSRF token AdditionalUserData aud = new AdditionalUserData(); aud.setCsrf(csrfToken); @@ -69,7 +79,7 @@ class CSRFHelperTest { // simulates a session object with no CSRF data (no previous calls) AdditionalUserData aud = new AdditionalUserData(); - String sampleCSRF = csrf.getNewCSRFToken(); + String sampleCSRF = csrf.getNewCSRFToken(mockRequest); Assertions.assertThrows(FinalUnauthorizedException.class, () -> csrf.compareCSRF(aud, null)); Assertions.assertThrows(FinalUnauthorizedException.class, () -> csrf.compareCSRF(aud, "")); Assertions.assertThrows(FinalUnauthorizedException.class, () -> csrf.compareCSRF(aud, sampleCSRF)); diff --git a/persistence/deployment/pom.xml b/persistence/deployment/pom.xml index 1705a1c..08ea49c 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 91a48c2..85434c8 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 567aba2..417fec6 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 cc5e1af..23c2938 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 0000000..cbb708e --- /dev/null +++ b/persistence/runtime/src/main/java/org/eclipsefoundation/persistence/config/DistributedCSRFProvider.java @@ -0,0 +1,21 @@ +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.properties.IfBuildProperty; + +public class DistributedCSRFProvider { + + @Produces + @ApplicationScoped + @IfBuildProperty(name = "security.csrf.enabled.distributed-mode", stringValue = "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 0000000..1348887 --- /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 0000000..7124aa9 --- /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 7ded90c..9a57239 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 c20670b..07f0a2b 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 -- GitLab From 53798e3db010a708279dd4f039435e7a76c7105c Mon Sep 17 00:00:00 2001 From: Martin Lowe Date: Mon, 7 Mar 2022 14:46:32 -0500 Subject: [PATCH 2/3] Added configuration setting to disable default provider method This can be used to provide your own datasource to the generator when not using the default datasource/dao. --- .../persistence/config/DistributedCSRFProvider.java | 4 ++++ 1 file changed, 4 insertions(+) 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 index cbb708e..91f9ad9 100644 --- a/persistence/runtime/src/main/java/org/eclipsefoundation/persistence/config/DistributedCSRFProvider.java +++ b/persistence/runtime/src/main/java/org/eclipsefoundation/persistence/config/DistributedCSRFProvider.java @@ -8,13 +8,17 @@ 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); } -- GitLab From cec260b77218558c7227a52cdf494fa49cb46717 Mon Sep 17 00:00:00 2001 From: Martin Lowe Date: Tue, 8 Mar 2022 09:53:41 -0500 Subject: [PATCH 3/3] Update request CSRF filter to be able to check distributed tokens --- .../core/helper/CSRFHelper.java | 62 ++++++++++++++++--- .../core/model/CSRFGenerator.java | 15 +---- .../core/request/CSRFSecurityFilter.java | 12 +++- .../authenticated/helper/CSRFHelperTest.java | 33 ++-------- .../core/test/TestResource.java | 2 +- 5 files changed, 74 insertions(+), 50 deletions(-) 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 0caf278..5d0d986 100644 --- a/core/src/main/java/org/eclipsefoundation/core/helper/CSRFHelper.java +++ b/core/src/main/java/org/eclipsefoundation/core/helper/CSRFHelper.java @@ -1,5 +1,9 @@ package org.eclipsefoundation.core.helper; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; + +import javax.enterprise.inject.Instance; import javax.inject.Inject; import javax.inject.Singleton; import javax.servlet.http.HttpServletRequest; @@ -24,6 +28,8 @@ public final class CSRFHelper { @ConfigProperty(name = "security.csrf.enabled", defaultValue = "false") boolean csrfEnabled; + @ConfigProperty(name = "security.csrf.enabled.distributed-mode", defaultValue = "false") + Instance distributedMode; @Inject CSRFGenerator generator; @@ -37,25 +43,67 @@ public final class CSRFHelper { return generator.getCSRFToken(httpServletRequest); } + /** + * 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(); + } + } + + /** + * 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 static boolean verifyConstantTime(String expectedToken, String token) { + if (expectedToken == token) { + return true; + } + if (expectedToken == null || token == null) { + return false; + } + return MessageDigest.isEqual(expectedToken.getBytes(StandardCharsets.US_ASCII), + token.getBytes(StandardCharsets.US_ASCII)); + } + } diff --git a/core/src/main/java/org/eclipsefoundation/core/model/CSRFGenerator.java b/core/src/main/java/org/eclipsefoundation/core/model/CSRFGenerator.java index 6ae7ea5..e7d51bf 100644 --- a/core/src/main/java/org/eclipsefoundation/core/model/CSRFGenerator.java +++ b/core/src/main/java/org/eclipsefoundation/core/model/CSRFGenerator.java @@ -2,8 +2,8 @@ package org.eclipsefoundation.core.model; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; import java.util.Optional; +import java.util.UUID; import javax.servlet.http.HttpServletRequest; @@ -37,17 +37,15 @@ public interface CSRFGenerator { * */ public static class DefaultCSRFGenerator implements CSRFGenerator { - private SecureRandom rnd; @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 - byte[] bytes = rnd().generateSeed(24); - String secureRnd = new String(bytes); + String secureBase = UUID.randomUUID().toString(); // create a secure random secret to embed in the user session - String preHash = secureRnd + salt.orElseGet(() -> "short-salt"); + String preHash = secureBase + salt.orElseGet(() -> "short-salt"); // create new digest to hash the result MessageDigest md; @@ -61,12 +59,5 @@ public interface CSRFGenerator { // convert back to a hex string to act as a token return HexConverter.convertToHexString(array); } - - private SecureRandom rnd() { - if (this.rnd == null) { - this.rnd = new SecureRandom(Long.toString(System.currentTimeMillis()).getBytes()); - } - return rnd; - } } } 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 f54b4c0..2bd5f7c 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/test/java/org/eclipsefoundation/core/authenticated/helper/CSRFHelperTest.java b/core/src/test/java/org/eclipsefoundation/core/authenticated/helper/CSRFHelperTest.java index a56e392..a184e5c 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 @@ -5,7 +5,6 @@ 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; @@ -38,50 +37,26 @@ class CSRFHelperTest { void compareCSRF_validToken() { // generate a token to use in test String csrfToken = csrf.getNewCSRFToken(mockRequest); - // create session object with given CSRF token - AdditionalUserData aud = new AdditionalUserData(); - aud.setCsrf(csrfToken); // 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(mockRequest); - // create session object with given CSRF token - AdditionalUserData aud = new AdditionalUserData(); - aud.setCsrf(csrfToken); // 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(mockRequest); - // create session object with given CSRF token - AdditionalUserData aud = new AdditionalUserData(); - aud.setCsrf(csrfToken); - // 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(mockRequest); - 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 3a64b1a..1b96423 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(); } -- GitLab