diff --git a/core/src/main/java/org/eclipsefoundation/core/exception/FinalUnauthorizedException.java b/core/src/main/java/org/eclipsefoundation/core/exception/FinalUnauthorizedException.java
new file mode 100644
index 0000000000000000000000000000000000000000..6914a181b493e89c7d7eea0c8d551d45742b3dc8
--- /dev/null
+++ b/core/src/main/java/org/eclipsefoundation/core/exception/FinalUnauthorizedException.java
@@ -0,0 +1,17 @@
+package org.eclipsefoundation.core.exception;
+
+/**
+ * Represents an unauthorized request with no redirect (standard UnauthorizedException gets routed
+ * to OIDC login page when active, which is not desired).
+ *
+ * @author Martin Lowe
+ */
+public class FinalUnauthorizedException extends RuntimeException {
+
+  public FinalUnauthorizedException(String message) {
+    super(message);
+  }
+
+  /** */
+  private static final long serialVersionUID = 1L;
+}
diff --git a/core/src/main/java/org/eclipsefoundation/core/helper/CSRFHelper.java b/core/src/main/java/org/eclipsefoundation/core/helper/CSRFHelper.java
new file mode 100644
index 0000000000000000000000000000000000000000..28fc79671ae8ccd749c424f20cb340df7bdea07d
--- /dev/null
+++ b/core/src/main/java/org/eclipsefoundation/core/helper/CSRFHelper.java
@@ -0,0 +1,91 @@
+package org.eclipsefoundation.core.helper;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+
+import javax.annotation.PostConstruct;
+import javax.inject.Singleton;
+
+import org.eclipse.microprofile.config.inject.ConfigProperty;
+import org.eclipsefoundation.core.exception.FinalUnauthorizedException;
+import org.eclipsefoundation.core.model.AdditionalUserData;
+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.
+ * 
+ * @author Martin Lowe
+ *
+ */
+@Singleton
+public final class CSRFHelper {
+    public static final Logger LOGGER = LoggerFactory.getLogger(CSRFHelper.class);
+    public static final String CSRF_HEADER_NAME = "x-csrf-token";
+
+    @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;
+
+    @PostConstruct
+    void init() {
+        // create a secure Random impl using salt + timestamp bytes
+        rnd = new SecureRandom(Long.toString(System.currentTimeMillis()).getBytes());
+    }
+
+    /**
+     * 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);
+    }
+
+    /**
+     * 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
+     * @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) {
+        if (csrfEnabled) {
+            LOGGER.debug("Comparing following tokens:\n{}\n{}", aud == null ? null : aud.getCsrf(), passedCSRF);
+            if (aud == null || aud.getCsrf() == null) {
+                throw new FinalUnauthorizedException(
+                        "CSRF token not generated for current request and is required, refusing request");
+            } else if (passedCSRF == null) {
+                throw new FinalUnauthorizedException("No CSRF token passed for current request, refusing request");
+            } else if (!passedCSRF.equals(aud.getCsrf())) {
+                throw new FinalUnauthorizedException("CSRF tokens did not match, refusing request");
+            }
+        }
+    }
+}
diff --git a/core/src/main/java/org/eclipsefoundation/core/model/AdditionalUserData.java b/core/src/main/java/org/eclipsefoundation/core/model/AdditionalUserData.java
new file mode 100644
index 0000000000000000000000000000000000000000..bff6b53a55b35ac2bc07adac81b5921ee8b63501
--- /dev/null
+++ b/core/src/main/java/org/eclipsefoundation/core/model/AdditionalUserData.java
@@ -0,0 +1,18 @@
+package org.eclipsefoundation.core.model;
+
+import javax.enterprise.context.SessionScoped;
+
+@SessionScoped
+public class AdditionalUserData {
+  private String csrf;
+
+  /** @return the csrf */
+  public String getCsrf() {
+    return csrf;
+  }
+
+  /** @param csrf the csrf to set */
+  public void setCsrf(String csrf) {
+    this.csrf = csrf;
+  }
+}
diff --git a/core/src/main/java/org/eclipsefoundation/core/request/CSRFSecurityFilter.java b/core/src/main/java/org/eclipsefoundation/core/request/CSRFSecurityFilter.java
new file mode 100644
index 0000000000000000000000000000000000000000..041c7033669512f0ac5b4e45de21de2528444b76
--- /dev/null
+++ b/core/src/main/java/org/eclipsefoundation/core/request/CSRFSecurityFilter.java
@@ -0,0 +1,55 @@
+package org.eclipsefoundation.core.request;
+
+import java.io.IOException;
+
+import javax.inject.Inject;
+import javax.ws.rs.container.ContainerRequestContext;
+import javax.ws.rs.container.ContainerRequestFilter;
+import javax.ws.rs.ext.Provider;
+
+import org.eclipse.microprofile.config.inject.ConfigProperty;
+import org.eclipsefoundation.core.exception.FinalUnauthorizedException;
+import org.eclipsefoundation.core.helper.CSRFHelper;
+import org.eclipsefoundation.core.model.AdditionalUserData;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import io.undertow.httpcore.HttpMethodNames;
+
+/**
+ * Creates a security layer in front of mutation requests to require CSRF tokens (if enabled). This layer does not
+ * perform the check of the token in-case there are other conditions that would rebuff the request.
+ * 
+ * @author Martin Lowe
+ */
+@Provider
+public class CSRFSecurityFilter implements ContainerRequestFilter {
+    public static final Logger LOGGER = LoggerFactory.getLogger(CSRFSecurityFilter.class);
+
+    @ConfigProperty(name = "security.csrf.enabled", defaultValue = "false")
+    boolean csrfEnabled;
+
+    @Inject
+    CSRFHelper csrf;
+    @Inject
+    AdditionalUserData aud;
+
+    @Override
+    public void filter(ContainerRequestContext requestContext) throws IOException {
+        if (csrfEnabled) {
+            // check if the HTTP method indicates a mutation
+            String method = requestContext.getMethod();
+            if (HttpMethodNames.DELETE.equals(method) || HttpMethodNames.POST.equals(method)
+                    || HttpMethodNames.PUT.equals(method)) {
+                // check csrf token presence (not value)
+                String token = requestContext.getHeaderString(CSRFHelper.CSRF_HEADER_NAME);
+                if (token == null || "".equals(token.trim())) {
+                    throw new FinalUnauthorizedException("No CSRF token passed for mutation call, refusing connection");
+                } else {
+                    // run comparison. If error, exception will be thrown
+                    csrf.compareCSRF(aud, token);
+                }
+            }
+        }
+    }
+}
diff --git a/core/src/main/java/org/eclipsefoundation/core/resource/mapper/FinalUnauthorizedMapper.java b/core/src/main/java/org/eclipsefoundation/core/resource/mapper/FinalUnauthorizedMapper.java
new file mode 100644
index 0000000000000000000000000000000000000000..0e9037806dfd1e58bb9d070752f2a4d951442219
--- /dev/null
+++ b/core/src/main/java/org/eclipsefoundation/core/resource/mapper/FinalUnauthorizedMapper.java
@@ -0,0 +1,33 @@
+/* Copyright (c) 2019 Eclipse Foundation and others.
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Public License 2.0
+ * which is available at http://www.eclipse.org/legal/epl-v20.html,
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.eclipsefoundation.core.resource.mapper;
+
+import javax.ws.rs.core.Response;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+
+import org.eclipsefoundation.core.exception.FinalUnauthorizedException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Exception mapper to allow 403 to be thrown past auth barrier. Typical unauthorized exceptions
+ * cause redirects through OIDC layers which isn't always wanted
+ *
+ * @author Martin Lowe
+ */
+@Provider
+public class FinalUnauthorizedMapper implements ExceptionMapper<FinalUnauthorizedException> {
+  private static final Logger LOGGER = LoggerFactory.getLogger(FinalUnauthorizedMapper.class);
+
+  @Override
+  public Response toResponse(FinalUnauthorizedException exception) {
+    LOGGER.error(exception.getMessage(), exception);
+    // return an empty response with a server error response
+    return Response.status(403).build();
+  }
+}
diff --git a/core/src/main/java/org/eclipsefoundation/core/response/CSRFHeaderFilter.java b/core/src/main/java/org/eclipsefoundation/core/response/CSRFHeaderFilter.java
new file mode 100644
index 0000000000000000000000000000000000000000..4b95b864493db6bb2741db84179eb9dafcccdc5a
--- /dev/null
+++ b/core/src/main/java/org/eclipsefoundation/core/response/CSRFHeaderFilter.java
@@ -0,0 +1,43 @@
+package org.eclipsefoundation.core.response;
+
+import java.io.IOException;
+
+import javax.inject.Inject;
+import javax.ws.rs.container.ContainerRequestContext;
+import javax.ws.rs.container.ContainerResponseContext;
+import javax.ws.rs.container.ContainerResponseFilter;
+import javax.ws.rs.ext.Provider;
+
+import org.eclipse.microprofile.config.inject.ConfigProperty;
+import org.eclipsefoundation.core.helper.CSRFHelper;
+import org.eclipsefoundation.core.model.AdditionalUserData;
+
+/**
+ * Injects the CSRF header token into the response when enabled for a server.
+ *
+ * @author Martin Lowe
+ */
+@Provider
+public class CSRFHeaderFilter implements ContainerResponseFilter {
+    @ConfigProperty(name = "security.csrf.enabled", defaultValue = "false")
+    boolean csrfEnabled;
+
+    @Inject
+    CSRFHelper csrf;
+    @Inject
+    AdditionalUserData aud;
+
+    @Override
+    public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext)
+            throws IOException {
+        // only attach if CSRF is enabled for the current runtime
+        if (csrfEnabled) {
+            // generate a new token if none is yet present
+            if (aud.getCsrf() == null) {
+                aud.setCsrf(csrf.getNewCSRFToken());
+            }
+            // attach the current CSRF token as a header on the request
+            responseContext.getHeaders().add(CSRFHelper.CSRF_HEADER_NAME, aud.getCsrf());
+        }
+    }
+}
diff --git a/core/src/main/resources/application.properties b/core/src/main/resources/application.properties
index 8f6342c7c7631f9e48b57759fcb83a36d82820a6..f3f56dbf1916b83b3b904223c812ccda3f927323 100644
--- a/core/src/main/resources/application.properties
+++ b/core/src/main/resources/application.properties
@@ -1,5 +1,5 @@
 ## OAUTH CONFIG
-quarkus.oauth2.enabled=true
+quarkus.oauth2.enabled=false
 quarkus.oauth2.introspection-url=http://accounts.eclipse.org/oauth2/introspect
 eclipse.oauth.override=false
 
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
new file mode 100644
index 0000000000000000000000000000000000000000..7cc023d79135d25443a4805b639e1038fe9cb42f
--- /dev/null
+++ b/core/src/test/java/org/eclipsefoundation/core/authenticated/helper/CSRFHelperTest.java
@@ -0,0 +1,77 @@
+package org.eclipsefoundation.core.authenticated.helper;
+
+import javax.inject.Inject;
+
+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.Test;
+
+import io.quarkus.test.junit.QuarkusTest;
+import io.quarkus.test.junit.TestProfile;
+
+/**
+ * Test CSRF functionality from the helper directly using the authentication secured test profile.
+ * 
+ * @author Martin Lowe
+ */
+@QuarkusTest
+@TestProfile(AuthenticatedTestProfile.class)
+class CSRFHelperTest {
+
+    @Inject
+    CSRFHelper csrf;
+
+    @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);
+
+        // this should not throw as the tokens match
+        Assertions.assertDoesNotThrow(() -> csrf.compareCSRF(aud, 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);
+
+        // this should throw as the tokens are not the same
+        Assertions.assertThrows(FinalUnauthorizedException.class, () -> csrf.compareCSRF(aud, "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);
+
+        // 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));
+    }
+}
diff --git a/core/src/test/java/org/eclipsefoundation/core/authenticated/request/CSRFSecurityFilterTest.java b/core/src/test/java/org/eclipsefoundation/core/authenticated/request/CSRFSecurityFilterTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..6fe2780723e5b4c364717d0f5dd709b6288e0500
--- /dev/null
+++ b/core/src/test/java/org/eclipsefoundation/core/authenticated/request/CSRFSecurityFilterTest.java
@@ -0,0 +1,63 @@
+package org.eclipsefoundation.core.authenticated.request;
+
+import static io.restassured.RestAssured.given;
+
+import org.eclipsefoundation.core.helper.CSRFHelper;
+import org.eclipsefoundation.core.test.AuthenticatedTestProfile;
+import org.junit.jupiter.api.Test;
+
+import io.quarkus.test.junit.QuarkusTest;
+import io.quarkus.test.junit.TestProfile;
+import io.restassured.filter.session.SessionFilter;
+import io.restassured.response.Response;
+
+/**
+ * Test the CSRF security filter which can block requests based on presence of CSRF token. This makes use of the
+ * authenticated test profile to reduce complexity of testing other facets of the core lib that have no interactions
+ * with security.
+ * 
+ * @author Martin Lowe
+ *
+ */
+@QuarkusTest
+@TestProfile(AuthenticatedTestProfile.class)
+class CSRFSecurityFilterTest {
+
+    @Test
+    void validateNoToken() {
+        // expect rebuff as no CSRF token was passed
+        given().when().get("/test").then().statusCode(403);
+        given().when().post("/test").then().statusCode(403);
+        given().when().put("/test").then().statusCode(403);
+        given().when().delete("/test").then().statusCode(403);
+    }
+
+    @Test
+    void validateWrongToken() {
+        // do a good request to trigger the build of the header internally
+        given().when().get("/test/unguarded").then().statusCode(200);
+        // expect rebuff as no CSRF token was passed
+        given().header(CSRFHelper.CSRF_HEADER_NAME, "bad-header-value").when().get("/test").then().statusCode(403);
+        given().header(CSRFHelper.CSRF_HEADER_NAME, "bad-header-value").when().post("/test").then().statusCode(403);
+        given().header(CSRFHelper.CSRF_HEADER_NAME, "bad-header-value").when().put("/test").then().statusCode(403);
+        given().header(CSRFHelper.CSRF_HEADER_NAME, "bad-header-value").when().delete("/test").then().statusCode(403);
+    }
+
+    @Test
+    void validateRightCSRFToken() {
+        SessionFilter sessionFilter = new SessionFilter();
+        // do a good request to trigger the build of the header internally
+        Response r = given().filter(sessionFilter).when().get("/test/unguarded");
+        String expectedHeader = r.getHeader(CSRFHelper.CSRF_HEADER_NAME);
+
+        // expect rebuff as no CSRF token was passed
+        given().filter(sessionFilter).header(CSRFHelper.CSRF_HEADER_NAME, expectedHeader).when().post("/test").then()
+                .statusCode(200);
+        given().filter(sessionFilter).header(CSRFHelper.CSRF_HEADER_NAME, expectedHeader).when().delete("/test").then()
+                .statusCode(200);
+        given().filter(sessionFilter).header(CSRFHelper.CSRF_HEADER_NAME, expectedHeader).when().put("/test").then()
+                .statusCode(200);
+        given().filter(sessionFilter).header(CSRFHelper.CSRF_HEADER_NAME, expectedHeader).when().get("/test").then()
+                .statusCode(200);
+    }
+}
diff --git a/core/src/test/java/org/eclipsefoundation/core/config/MockRoleAugmentor.java b/core/src/test/java/org/eclipsefoundation/core/config/MockRoleAugmentor.java
deleted file mode 100644
index 2d3f7ecb255acec11d55d49941096226a4ca9173..0000000000000000000000000000000000000000
--- a/core/src/test/java/org/eclipsefoundation/core/config/MockRoleAugmentor.java
+++ /dev/null
@@ -1,37 +0,0 @@
-package org.eclipsefoundation.core.config;
-
-import javax.enterprise.context.ApplicationScoped;
-
-import io.quarkus.security.identity.AuthenticationRequestContext;
-import io.quarkus.security.identity.SecurityIdentity;
-import io.quarkus.security.identity.SecurityIdentityAugmentor;
-import io.quarkus.security.runtime.QuarkusSecurityIdentity;
-import io.smallrye.mutiny.Uni;
-
-/**
- * Custom override for test classes that ignores current login state and sets
- * all users as admin always. This should only ever be used in testing.
- * 
- * @author Martin Lowe
- */
-@ApplicationScoped
-public class MockRoleAugmentor implements SecurityIdentityAugmentor {
-
-	@Override
-	public int priority() {
-		return 0;
-	}
-
-	@Override
-	public Uni<SecurityIdentity> augment(SecurityIdentity identity, AuthenticationRequestContext context) {
-		// create a new builder and copy principal, attributes, credentials and roles
-		// from the original
-		QuarkusSecurityIdentity.Builder builder = QuarkusSecurityIdentity.builder()
-				.setPrincipal(identity.getPrincipal()).addAttributes(identity.getAttributes())
-				.addCredentials(identity.getCredentials()).addRoles(identity.getRoles());
-
-		// add custom role source here
-		builder.addRole("marketplace_admin_access");
-		return context.runBlocking(builder::build);
-	}
-}
\ No newline at end of file
diff --git a/core/src/test/java/org/eclipsefoundation/core/model/RequestWrapperMock.java b/core/src/test/java/org/eclipsefoundation/core/model/RequestWrapperMock.java
deleted file mode 100644
index bf44a3ef6ca22734333d2e8b78605aa74b9421bc..0000000000000000000000000000000000000000
--- a/core/src/test/java/org/eclipsefoundation/core/model/RequestWrapperMock.java
+++ /dev/null
@@ -1,16 +0,0 @@
-/* Copyright (c) 2019 Eclipse Foundation and others.
- * This program and the accompanying materials are made available
- * under the terms of the Eclipse Public License 2.0
- * which is available at http://www.eclipse.org/legal/epl-v20.html,
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.eclipsefoundation.core.model;
-
-/**
- * Wraps RequestWrapper for access outside of request scope in tests.
- * 
- * @author Martin Lowe
- */
-public class RequestWrapperMock extends RequestWrapper {
-	
-}
diff --git a/core/src/test/java/org/eclipsefoundation/core/test/AuthenticatedTestProfile.java b/core/src/test/java/org/eclipsefoundation/core/test/AuthenticatedTestProfile.java
new file mode 100644
index 0000000000000000000000000000000000000000..1f6362cc8c1059aa166c3fd3e9003e241d2face2
--- /dev/null
+++ b/core/src/test/java/org/eclipsefoundation/core/test/AuthenticatedTestProfile.java
@@ -0,0 +1,32 @@
+package org.eclipsefoundation.core.test;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import io.quarkus.test.junit.QuarkusTestProfile;
+
+/**
+ * Used to enable authentication profile in testing for tests that use this profile. Note that tests that use this
+ * profile should be grouped in a single package to ensure back to back runs to not increase test run time. More
+ * available on https://quarkus.io/blog/quarkus-test-profiles/.
+ * 
+ * @author Martin Lowe
+ */
+public class AuthenticatedTestProfile implements QuarkusTestProfile {
+
+    // private immutable copy of the configs for auth state
+    private static final Map<String, String> CONFIG_OVERRIDES;
+    static {
+        Map<String, String> tmp = new HashMap<>();
+        tmp.put("quarkus.oauth2.enabled", "true");
+        tmp.put("security.csrf.enabled", "true");
+        tmp.put("security.token.salt", "sample-salt-value-64^%$6DG54$DG46%Eas6egf54s%1#g5");
+        CONFIG_OVERRIDES = Collections.unmodifiableMap(tmp);
+    }
+
+    @Override
+    public Map<String, String> getConfigOverrides() {
+        return CONFIG_OVERRIDES;
+    }
+}
diff --git a/core/src/test/java/org/eclipsefoundation/core/test/TestResource.java b/core/src/test/java/org/eclipsefoundation/core/test/TestResource.java
new file mode 100644
index 0000000000000000000000000000000000000000..27edb6929a9d579598a5e517d4811c4837e5efed
--- /dev/null
+++ b/core/src/test/java/org/eclipsefoundation/core/test/TestResource.java
@@ -0,0 +1,87 @@
+package org.eclipsefoundation.core.test;
+
+import javax.inject.Inject;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.HeaderParam;
+import javax.ws.rs.POST;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
+import org.eclipsefoundation.core.helper.CSRFHelper;
+import org.eclipsefoundation.core.model.AdditionalUserData;
+
+/**
+ * Test resource for testing basic responses and security/authentication measures.
+ * 
+ * @author Martin Lowe
+ *
+ */
+@Path("/test")
+@Produces(MediaType.APPLICATION_JSON)
+public class TestResource {
+
+    @Inject
+    CSRFHelper csrf;
+    @Inject
+    AdditionalUserData aud;
+
+    /**
+     * Basic sample GET call that can be gated through CSRF if enabled to protect the data further.
+     * 
+     * @param passedCsrf the passed CSRF header value
+     * @return empty ok response if CSRF is disabled or properly passed, 403 response otherwise.
+     */
+    @GET
+    public Response get(@HeaderParam(value = CSRFHelper.CSRF_HEADER_NAME) String passedCsrf) {
+        // check CSRF manually as its a get request
+        csrf.compareCSRF(aud, passedCsrf);
+        return Response.ok().build();
+    }
+
+    /**
+     * Basic sample GET call that is not gated through CSRF. This could represent a user data endpoint, or a straight
+     * CSRF endpoint to trigger the header to be returned if enabled.
+     * 
+     * @return empty ok response
+     */
+    @GET
+    @Path("unguarded")
+    public Response get() {
+        // check CSRF manually as its a get request
+        return Response.ok().build();
+    }
+
+    /**
+     * Basic POST call that can be used to assist in validating filters.
+     * 
+     * @return empty ok response if CSRF is disabled or properly passed, 403 response otherwise.
+     */
+    @POST
+    public Response post() {
+        return Response.ok().build();
+    }
+
+    /**
+     * Basic PUT call that can be used to assist in validating filters.
+     * 
+     * @return empty ok response if CSRF is disabled or properly passed, 403 response otherwise.
+     */
+    @PUT
+    public Response put() {
+        return Response.ok().build();
+    }
+
+    /**
+     * Basic DELETE call that can be used to assist in validating filters.
+     * 
+     * @return empty ok response if CSRF is disabled or properly passed, 403 response otherwise.
+     */
+    @DELETE
+    public Response delete() {
+        return Response.ok().build();
+    }
+}
diff --git a/persistence/runtime/src/main/java/org/eclipsefoundation/persistence/dao/PersistenceDao.java b/persistence/runtime/src/main/java/org/eclipsefoundation/persistence/dao/PersistenceDao.java
index a487384c176ba9c130ada98de9427aa8674b8be0..47b9f031ae249caa2459deef80ff844710879a8a 100644
--- a/persistence/runtime/src/main/java/org/eclipsefoundation/persistence/dao/PersistenceDao.java
+++ b/persistence/runtime/src/main/java/org/eclipsefoundation/persistence/dao/PersistenceDao.java
@@ -17,52 +17,60 @@ import org.eclipsefoundation.persistence.model.RDBMSQuery;
 import io.quarkus.runtime.StartupEvent;
 
 /**
- * Interface for classes communicating with MongoDB. Assumes that reactive
- * stream asynchronous calls are used rather than blocking methods.
+ * Interface for classes communicating with MongoDB. Assumes that reactive stream asynchronous calls are used rather
+ * than blocking methods.
  * 
  * @author Martin Lowe
  */
 public interface PersistenceDao extends HealthCheck {
 
-	/**
-	 * Retrieves a list of typed results given the query passed.
-	 * 
-	 * @param q the query object for the current operation
-	 * @return a future result set of objects of type set in query
-	 */
-	<T extends BareNode> List<T> get(RDBMSQuery<T> q);
+    /**
+     * Retrieves a list of typed results given the query passed.
+     * 
+     * @param q the query object for the current operation
+     * @return a future result set of objects of type set in query
+     */
+    <T extends BareNode> List<T> get(RDBMSQuery<T> q);
 
-	/**
-	 * Adds a list of typed documents to the currently active database and schema,
-	 * using the query object to access the document type.
-	 * 
-	 * @param <T>       the type of document to post
-	 * @param q         the query object for the current operation
-	 * @param documents the list of typed documents to add to the database instance.
-	 * @return a future Void result indicating success on return.
-	 */
-	<T extends BareNode> List<T> add(RDBMSQuery<T> q, List<T> documents);
+    /**
+     * Adds a list of typed documents to the currently active database and schema, using the query object to access the
+     * document type.
+     * 
+     * @param <T> the type of document to post
+     * @param q the query object for the current operation
+     * @param documents the list of typed documents to add to the database instance.
+     * @return a future Void result indicating success on return.
+     */
+    <T extends BareNode> List<T> add(RDBMSQuery<T> q, List<T> documents);
 
-	/**
-	 * Deletes documents that match the given query.
-	 * 
-	 * @param <T> the type of document that is being deleted
-	 * @param q   the query object for the current operation
-	 * @return a future deletion result indicating whether the operation was
-	 *         successful
-	 */
-	<T extends BareNode> void delete(RDBMSQuery<T> q);
+    /**
+     * Deletes documents that match the given query.
+     * 
+     * @param <T> the type of document that is being deleted
+     * @param q the query object for the current operation
+     * @return a future deletion result indicating whether the operation was successful
+     */
+    <T extends BareNode> void delete(RDBMSQuery<T> q);
 
-	/**
-	 * Counts the number of filtered results of the given document type present.
-	 * 
-	 * @param q the query object for the current operation
-	 * @return a long result representing the number of results available for the
-	 *         given query and docuement type.
-	 */
-	Long count(RDBMSQuery<?> q);
+    /**
+     * Counts the number of filtered results of the given document type present.
+     * 
+     * @param q the query object for the current operation
+     * @return a long result representing the number of results available for the given query and docuement type.
+     */
+    Long count(RDBMSQuery<?> q);
 
-	default void startup(@Observes StartupEvent event) {
-		// intentionally empty
-	}
+    /**
+     * Retrieves a reference of an object to be used in operations on the server. This object is a proxy meant to help
+     * build FK relationships, but can be used in other operations as well.
+     * 
+     * @param id the ID of the object to retrieve
+     * @param type the type of object that should be retrieved
+     * @return a reference to the DB object if found, null otherwise
+     */
+    <T extends BareNode> T getReference(Object id, Class<T> type);
+
+    default void startup(@Observes StartupEvent event) {
+        // intentionally empty
+    }
 }
diff --git a/persistence/runtime/src/main/java/org/eclipsefoundation/persistence/dao/impl/DefaultHibernateDao.java b/persistence/runtime/src/main/java/org/eclipsefoundation/persistence/dao/impl/DefaultHibernateDao.java
index c79367f8c160e299cc9d2b803332b47ac3430efd..0cf2cbd6219bd28a3aa2196fde9fda88ae5d421f 100644
--- a/persistence/runtime/src/main/java/org/eclipsefoundation/persistence/dao/impl/DefaultHibernateDao.java
+++ b/persistence/runtime/src/main/java/org/eclipsefoundation/persistence/dao/impl/DefaultHibernateDao.java
@@ -32,145 +32,153 @@ import org.slf4j.LoggerFactory;
  * @author Martin Lowe
  */
 public class DefaultHibernateDao implements PersistenceDao {
-	private static final Logger LOGGER = LoggerFactory.getLogger(DefaultHibernateDao.class);
-
-	@Inject
-	EntityManager em;
-
-	@ConfigProperty(name = "eclipse.db.default.limit")
-	int defaultLimit;
-
-	@ConfigProperty(name = "eclipse.db.default.limit.max")
-	int defaultMax;
-
-	@ConfigProperty(name = "eclipse.db.maintenance", defaultValue = "false")
-	boolean maintenanceFlag;
-
-	@Override
-	public <T extends BareNode> List<T> get(RDBMSQuery<T> q) {
-		if (maintenanceFlag) {
-			throw new MaintenanceException();
-		}
-		if (LOGGER.isDebugEnabled()) {
-			LOGGER.debug("Querying DB using the following query: {}", q.getFilter().getSelectSql());
-		}
-
-		// build base query
-		TypedQuery<T> query = em.createQuery(q.getFilter().getSelectSql(), q.getDocType());
-
-		// add ordinal parameters
-		int ord = 1;
-		for (Clause c : q.getFilter().getClauses()) {
-			for (Object param : c.getParams()) {
-				query.setParameter(ord++, param);
-			}
-		}
-
-		// check if result set should be limited
-		if (q.getDTOFilter().useLimit()) {
-			query = query.setFirstResult(getOffset(q)).setMaxResults(getLimit(q));
-		}
-		// run the query
-		return query.getResultList();
-	}
-
-	@Transactional
-	@Override
-	public <T extends BareNode> List<T> add(RDBMSQuery<T> q, List<T> documents) {
-		if (maintenanceFlag) {
-			throw new MaintenanceException();
-		}
-		if (LOGGER.isDebugEnabled()) {
-			LOGGER.debug("Adding {} documents to DB of type {}", documents.size(), q.getDocType().getSimpleName());
-		}
-		// for each doc, check if update or create
-		List<T> updatedDocs = new ArrayList<>(documents.size());
-		for (T doc : documents) {
-			T ref = doc;
-			if (doc.getId() != null) {
-				// ensure this object exists before merging on it
-				if (em.find(q.getDocType(), doc.getId()) != null) {
-					LOGGER.debug("Merging document with existing document with id '{}'", doc.getId());
-					ref = em.merge(doc);
-				} else {
-					LOGGER.debug("Persisting new document with id '{}'", doc.getId());
-					em.persist(doc);
-				}
-			} else {
-				LOGGER.debug("Persisting new document with generated UUID ID");
-				em.persist(doc);
-			}
-			// add the ref to the output list
-			updatedDocs.add(ref);
-		}
-		return updatedDocs;
-	}
-
-	@Transactional
-	@Override
-	public <T extends BareNode> void delete(RDBMSQuery<T> q) {
-		if (maintenanceFlag) {
-			throw new MaintenanceException();
-		}
-
-		if (LOGGER.isDebugEnabled()) {
-			LOGGER.debug("Removing documents from DB using the following query: {}", q);
-		}
-		// retrieve results for the given deletion query to delete using entity manager
-		List<T> results = get(q);
-		if (results.isEmpty()) {
-			throw new NoResultException("Could not find any documents with given filters");
-		}
-		// remove all matched documents
-		results.forEach(em::remove);
-	}
-
-	@Transactional
-	@Override
-	public Long count(RDBMSQuery<?> q) {
-		if (maintenanceFlag) {
-			throw new MaintenanceException();
-		}
-		if (LOGGER.isDebugEnabled()) {
-			LOGGER.debug("Counting documents in DB that match the following query: {}", q.getFilter().getCountSql());
-		}
-
-		// build base query
-		TypedQuery<Long> query = em.createQuery(q.getFilter().getCountSql(), Long.class);
-		// add ordinal parameters
-		int ord = 1;
-		for (Clause c : q.getFilter().getClauses()) {
-			for (Object param : c.getParams()) {
-				query.setParameter(ord++, param);
-			}
-		}
-		return query.getSingleResult();
-	}
-
-	private int getLimit(RDBMSQuery<?> q) {
-		return q.getLimit() > 0 ? Math.min(q.getLimit(), defaultMax) : defaultLimit;
-	}
-
-	private int getOffset(RDBMSQuery<?> q) {
-		// allow for manual offsetting
-		int manualOffset = q.getManualOffset();
-		if (manualOffset > 0) {
-			return manualOffset;
-		}
-		// if first page, no offset
-		if (q.getPage() <= 1) {
-			return 0;
-		}
-		int limit = getLimit(q);
-		return (limit * q.getPage()) - limit;
-	}
-
-	@Override
-	public HealthCheckResponse call() {
-		HealthCheckResponseBuilder b = HealthCheckResponse.named("DB readiness");
-		if (maintenanceFlag) {
-			return b.down().withData("error", "Maintenance flag is set").build();
-		}
-		return b.up().build();
-	}
+    private static final Logger LOGGER = LoggerFactory.getLogger(DefaultHibernateDao.class);
+
+    @Inject
+    EntityManager em;
+
+    @ConfigProperty(name = "eclipse.db.default.limit")
+    int defaultLimit;
+
+    @ConfigProperty(name = "eclipse.db.default.limit.max")
+    int defaultMax;
+
+    @ConfigProperty(name = "eclipse.db.maintenance", defaultValue = "false")
+    boolean maintenanceFlag;
+
+    @Override
+    public <T extends BareNode> List<T> get(RDBMSQuery<T> q) {
+        if (maintenanceFlag) {
+            throw new MaintenanceException();
+        }
+        if (LOGGER.isDebugEnabled()) {
+            LOGGER.debug("Querying DB using the following query: {}", q.getFilter().getSelectSql());
+        }
+
+        // build base query
+        TypedQuery<T> query = em.createQuery(q.getFilter().getSelectSql(), q.getDocType());
+
+        // add ordinal parameters
+        int ord = 1;
+        for (Clause c : q.getFilter().getClauses()) {
+            for (Object param : c.getParams()) {
+                query.setParameter(ord++, param);
+            }
+        }
+
+        // check if result set should be limited
+        if (q.getDTOFilter().useLimit()) {
+            query = query.setFirstResult(getOffset(q)).setMaxResults(getLimit(q));
+        }
+        // run the query
+        return query.getResultList();
+    }
+
+    @Transactional
+    @Override
+    public <T extends BareNode> List<T> add(RDBMSQuery<T> q, List<T> documents) {
+        if (maintenanceFlag) {
+            throw new MaintenanceException();
+        }
+        if (LOGGER.isDebugEnabled()) {
+            LOGGER.debug("Adding {} documents to DB of type {}", documents.size(), q.getDocType().getSimpleName());
+        }
+        // for each doc, check if update or create
+        List<T> updatedDocs = new ArrayList<>(documents.size());
+        for (T doc : documents) {
+            T ref = doc;
+            if (doc.getId() != null) {
+                // ensure this object exists before merging on it
+                if (em.find(q.getDocType(), doc.getId()) != null) {
+                    LOGGER.debug("Merging document with existing document with id '{}'", doc.getId());
+                    ref = em.merge(doc);
+                } else {
+                    LOGGER.debug("Persisting new document with id '{}'", doc.getId());
+                    em.persist(doc);
+                }
+            } else {
+                LOGGER.debug("Persisting new document with generated UUID ID");
+                em.persist(doc);
+            }
+            // add the ref to the output list
+            updatedDocs.add(ref);
+        }
+        return updatedDocs;
+    }
+
+    @Transactional
+    @Override
+    public <T extends BareNode> void delete(RDBMSQuery<T> q) {
+        if (maintenanceFlag) {
+            throw new MaintenanceException();
+        }
+
+        if (LOGGER.isDebugEnabled()) {
+            LOGGER.debug("Removing documents from DB using the following query: {}", q);
+        }
+        // retrieve results for the given deletion query to delete using entity manager
+        List<T> results = get(q);
+        if (results.isEmpty()) {
+            throw new NoResultException("Could not find any documents with given filters");
+        }
+        // remove all matched documents
+        results.forEach(em::remove);
+    }
+
+    @Transactional
+    @Override
+    public Long count(RDBMSQuery<?> q) {
+        if (maintenanceFlag) {
+            throw new MaintenanceException();
+        }
+        if (LOGGER.isDebugEnabled()) {
+            LOGGER.debug("Counting documents in DB that match the following query: {}", q.getFilter().getCountSql());
+        }
+
+        // build base query
+        TypedQuery<Long> query = em.createQuery(q.getFilter().getCountSql(), Long.class);
+        // add ordinal parameters
+        int ord = 1;
+        for (Clause c : q.getFilter().getClauses()) {
+            for (Object param : c.getParams()) {
+                query.setParameter(ord++, param);
+            }
+        }
+        return query.getSingleResult();
+    }
+
+    @Override
+    public <T extends BareNode> T getReference(Object id, Class<T> type) {
+        if (maintenanceFlag) {
+            throw new MaintenanceException();
+        }
+        return em.getReference(type, id);
+    }
+
+    private int getLimit(RDBMSQuery<?> q) {
+        return q.getLimit() > 0 ? Math.min(q.getLimit(), defaultMax) : defaultLimit;
+    }
+
+    private int getOffset(RDBMSQuery<?> q) {
+        // allow for manual offsetting
+        int manualOffset = q.getManualOffset();
+        if (manualOffset > 0) {
+            return manualOffset;
+        }
+        // if first page, no offset
+        if (q.getPage() <= 1) {
+            return 0;
+        }
+        int limit = getLimit(q);
+        return (limit * q.getPage()) - limit;
+    }
+
+    @Override
+    public HealthCheckResponse call() {
+        HealthCheckResponseBuilder b = HealthCheckResponse.named("DB readiness");
+        if (maintenanceFlag) {
+            return b.down().withData("error", "Maintenance flag is set").build();
+        }
+        return b.up().build();
+    }
 }
diff --git a/persistence/runtime/src/main/java/org/eclipsefoundation/persistence/dao/impl/PlaceholderPersistenceDao.java b/persistence/runtime/src/main/java/org/eclipsefoundation/persistence/dao/impl/PlaceholderPersistenceDao.java
index 785823bfb6cd32736380508d8ca87baa514bd33c..6927687af76718caff70fcbd33cd58a01aa4e6f2 100644
--- a/persistence/runtime/src/main/java/org/eclipsefoundation/persistence/dao/impl/PlaceholderPersistenceDao.java
+++ b/persistence/runtime/src/main/java/org/eclipsefoundation/persistence/dao/impl/PlaceholderPersistenceDao.java
@@ -42,4 +42,9 @@ public class PlaceholderPersistenceDao implements PersistenceDao {
 		throw new IllegalStateException("Placeholder DAO should not be used in running instances");
 	}
 
+    @Override
+    public <T extends BareNode> T getReference(Object id, Class<T> type) {
+        throw new IllegalStateException("Placeholder DAO should not be used in running instances");
+    }
+
 }
diff --git a/persistence/runtime/src/main/java/org/eclipsefoundation/persistence/dto/BareNode.java b/persistence/runtime/src/main/java/org/eclipsefoundation/persistence/dto/BareNode.java
index ecbad1b1206fd625f17e3dbe1f6b70d709601b00..b2cd9949967135d72f95c169a7fa1f1a70490df8 100644
--- a/persistence/runtime/src/main/java/org/eclipsefoundation/persistence/dto/BareNode.java
+++ b/persistence/runtime/src/main/java/org/eclipsefoundation/persistence/dto/BareNode.java
@@ -41,7 +41,7 @@ public abstract class BareNode {
 		if (getClass() != obj.getClass()) {
 			return false;
 		}
-		NodeBase other = (NodeBase) obj;
+		BareNode other = (BareNode) obj;
 		return super.equals(obj) && Objects.equals(getId(), other.getId());
 	}
 }