diff --git a/.gitignore b/.gitignore
index 5b19cb510be3d966e9ba7ccf260b70a6fc08f226..89c5f5e5d92547f081c1c3ed4fe0f855f48bba21 100644
--- a/.gitignore
+++ b/.gitignore
@@ -38,6 +38,7 @@ nb-configuration.xml
 # Local environment
 /volumes
 .env
+config/mariadb/main/init.d/eclipsefoundation.sql
 
 /config/**/*secret.properties
 /node_modules
diff --git a/Makefile b/Makefile
index e259db79cd2880f3c3bf91742eef45c9db0dc3a7..67f9e3478097f0b8d59da3967a331b39b05c0019 100644
--- a/Makefile
+++ b/Makefile
@@ -3,7 +3,7 @@ SHELL = /bin/bash
 pre-setup:;
 	@echo "Creating environment file from template"
 	@rm -f .env && envsubst < config/.env.sample > .env
-	[ -f ./config/mariadb/init.d/eclipsefoundation.sql ] && echo "EF DB dump already exists, skipping fetch" || scp api-vm1:~/webdev/sql/eclipsefoundation.sql ./config/mariadb/init.d/
+	[ -f ./config/mariadb/main/init.d/eclipsefoundation.sql ] && echo "EF DB dump already exists, skipping fetch" || (scp api-vm1:~/webdev/sql/eclipsefoundation.sql.gz ./config/mariadb/main/init.d/ && gzip -d ./config/mariadb/main/init.d/eclipsefoundation.sql.gz)
 
 setup:;
 	@echo "Generating secret files from templates using environment file + variables"
diff --git a/config/mariadb/init.d/init.sql b/config/mariadb/main/init.d/init.sql
similarity index 100%
rename from config/mariadb/init.d/init.sql
rename to config/mariadb/main/init.d/init.sql
diff --git a/docker-compose.yaml b/docker-compose.yaml
index 88e830e13eb3385266e0b561c4a936d5204ca3be..7915d878460522c1e03a5f22a089988c46924217 100644
--- a/docker-compose.yaml
+++ b/docker-compose.yaml
@@ -50,6 +50,6 @@ services:
       MYSQL_ROOT_PASSWORD: ${MARIADB_PASSWORD}
       MYSQL_DATABASE: ${KEYCLOAK_DB}
     volumes:
-      - ./config/mariadb/init.d:/docker-entrypoint-initdb.d
+      - ./config/mariadb/main/init.d:/docker-entrypoint-initdb.d
       - ./volumes/mariadb:/var/lib/mysql
       - ./config/mariadb/conf:/etc/mysql/conf.d
diff --git a/pom.xml b/pom.xml
index 5024ebb7007b583d489d7abc7d6e7083f740498f..e4dc83483eb94f1f82294fa1da0e103ec8a88f6c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1,5 +1,7 @@
 <?xml version="1.0"?>
-<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+<project
+  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"
+  xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
   <modelVersion>4.0.0</modelVersion>
   <groupId>org.eclipsefoundation</groupId>
   <artifactId>eclipse-openvsx-api</artifactId>
@@ -10,14 +12,13 @@
     <maven.compiler.release>11</maven.compiler.release>
     <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
     <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
-    <quarkus.platform.artifact-id>quarkus-bom</quarkus.platform.artifact-id>
+    <quarkus.platform.artifact-id> quarkus-bom</quarkus.platform.artifact-id>
     <quarkus.platform.group-id>io.quarkus.platform</quarkus.platform.group-id>
-    <quarkus.platform.version>2.11.2.Final</quarkus.platform.version>
+    <quarkus.platform.version> 2.14.2.Final</quarkus.platform.version>
     <surefire-plugin.version>3.0.0-M5</surefire-plugin.version>
     <auto-value.version>1.8.2</auto-value.version>
-    <hibernate.version>5.5.6.Final</hibernate.version>
-    <eclipse-api-version>0.7.0-SNAPSHOT</eclipse-api-version>
-    <fdndb-api-version>1.0-SNAPSHOT</fdndb-api-version>
+    <eclipse-api-version> 0.7.1</eclipse-api-version>
+    <fdndb-api-version>1.0.3</fdndb-api-version>
   </properties>
   <repositories>
     <repository>
@@ -62,111 +63,31 @@
       <artifactId>foundationdb-api-client</artifactId>
       <version>${fdndb-api-version}</version>
     </dependency>
-    <dependency>
-      <groupId>io.quarkus</groupId>
-      <artifactId>quarkus-resteasy</artifactId>
-    </dependency>
-    <dependency>
-      <groupId>io.quarkus</groupId>
-      <artifactId>quarkus-resteasy-jackson</artifactId>
-    </dependency>
     <dependency>
       <groupId>io.quarkus</groupId>
       <artifactId>quarkus-oidc-client</artifactId>
     </dependency>
-    <dependency>
-      <groupId>io.quarkus</groupId>
-      <artifactId>quarkus-oidc-client-filter</artifactId>
-    </dependency>
-    <dependency>
-      <groupId>io.quarkus</groupId>
-      <artifactId>quarkus-rest-client</artifactId>
-    </dependency>
-    <dependency>
-      <groupId>io.quarkus</groupId>
-      <artifactId>quarkus-cache</artifactId>
-    </dependency>
-    <dependency>
-      <groupId>io.quarkus</groupId>
-      <artifactId>quarkus-arc</artifactId>
-    </dependency>
-    <dependency>
-      <groupId>io.quarkus</groupId>
-      <artifactId>quarkus-hibernate-validator</artifactId>
-    </dependency>
-    <dependency>
-      <groupId>org.apache.commons</groupId>
-      <artifactId>commons-text</artifactId>
-      <version>1.9</version>
-    </dependency>
 
     <!-- Annotation preprocessors - reduce all of the boiler plate -->
-    <dependency>
-      <groupId>com.google.auto.value</groupId>
-      <artifactId>auto-value</artifactId>
-      <version>${auto-value.version}</version>
-      <scope>provided</scope>
-    </dependency>
     <dependency>
       <groupId>com.google.auto.value</groupId>
       <artifactId>auto-value-annotations</artifactId>
       <version>${auto-value.version}</version>
     </dependency>
-    <dependency>
-      <groupId>com.google.code.findbugs</groupId>
-      <artifactId>jsr305</artifactId>
-      <version>3.0.0</version>
-    </dependency>
 
     <!-- Testing dependencies only -->
-    <dependency>
-      <groupId>io.quarkus</groupId>
-      <artifactId>quarkus-junit5</artifactId>
-      <scope>test</scope>
-    </dependency>
-    <dependency>
-      <groupId>io.rest-assured</groupId>
-      <artifactId>rest-assured</artifactId>
-      <scope>test</scope>
-    </dependency>
-    <dependency>
-      <groupId>io.rest-assured</groupId>
-      <artifactId>json-schema-validator</artifactId>
-      <scope>test</scope>
-    </dependency>
-    <dependency>
-      <groupId>io.quarkus</groupId>
-      <artifactId>quarkus-junit5-mockito</artifactId>
-      <scope>test</scope>
-    </dependency>
     <dependency>
       <groupId>org.eclipsefoundation</groupId>
       <artifactId>quarkus-test-common</artifactId>
       <version>${eclipse-api-version}</version>
-    </dependency>
-
-    <!-- Following H2/devservices deps are made to circumvent need for docker -->
-    <dependency>
-      <groupId>io.quarkus</groupId>
-      <artifactId>quarkus-devservices-h2</artifactId>
-      <scope>test</scope>
-    </dependency>
-    <dependency>
-      <groupId>io.quarkus</groupId>
-      <artifactId>quarkus-jdbc-h2</artifactId>
       <scope>test</scope>
     </dependency>
-    <dependency>
-      <groupId>com.h2database</groupId>
-      <artifactId>h2</artifactId>
-      <scope>test</scope>
-    </dependency>
-    <!-- Flyway specific dependencies, used to setup tables in test -->
     <dependency>
       <groupId>io.quarkus</groupId>
-      <artifactId>quarkus-flyway</artifactId>
+      <artifactId>quarkus-junit5</artifactId>
       <scope>test</scope>
     </dependency>
+
   </dependencies>
   <build>
     <plugins>
@@ -235,10 +156,8 @@
                 </goals>
                 <configuration>
                   <systemPropertyVariables>
-                    <native.image.path>
-                      ${project.build.directory}/${project.build.finalName}-runner</native.image.path>
-                    <java.util.logging.manager>
-                      org.jboss.logmanager.LogManager</java.util.logging.manager>
+                    <native.image.path> ${project.build.directory}/${project.build.finalName}-runner</native.image.path>
+                    <java.util.logging.manager> org.jboss.logmanager.LogManager</java.util.logging.manager>
                     <maven.home>${maven.home}</maven.home>
                   </systemPropertyVariables>
                 </configuration>
@@ -248,8 +167,7 @@
         </plugins>
       </build>
       <properties>
-        <quarkus.package.type>
-          native</quarkus.package.type>
+        <quarkus.package.type> native</quarkus.package.type>
       </properties>
     </profile>
   </profiles>
diff --git a/spec/openapi.yaml b/spec/openapi.yaml
index 1f22b2cc85a2666f83b6d8933cdda231d35dc91f..955e3303c2c16f9b65020eddb8a041ceca0e9415 100644
--- a/spec/openapi.yaml
+++ b/spec/openapi.yaml
@@ -29,6 +29,10 @@ paths:
                 $ref: "#/components/schemas/PublisherAgreement"
         404:
           description: Agreement not found.
+          content:
+            application/json:
+              schema:
+                $ref: "#/components/schemas/Error"
         500:
           description: Error while retrieving data
     post:
@@ -158,3 +162,18 @@ components:
         github_handle:
           type: string
           description: The GitHub username of the user. This must match what the Eclipse Foundation has on file for the user to successfully sign the puglisher agreement.
+
+    Error:
+      type: object
+      properties:
+        status_code:
+          type: integer
+          description: HTTP response code
+        message:
+          type: string
+          description: Message containing error information
+        url:
+          oneOf:
+            - type: string
+            - type: "null"
+          description: The URL
diff --git a/src/main/java/org/eclipsefoundation/openvsx/api/DrupalOAuthAPI.java b/src/main/java/org/eclipsefoundation/openvsx/api/DrupalOAuthAPI.java
new file mode 100644
index 0000000000000000000000000000000000000000..c68ed5f8c7d83b21f7def793ad5e045af2845864
--- /dev/null
+++ b/src/main/java/org/eclipsefoundation/openvsx/api/DrupalOAuthAPI.java
@@ -0,0 +1,36 @@
+/*********************************************************************
+* Copyright (c) 2023 Eclipse Foundation.
+*
+* This program and the accompanying materials are made
+* available under the terms of the Eclipse Public License 2.0
+* which is available at https://www.eclipse.org/legal/epl-2.0/
+*
+* Author: Zachary Sabourin <zachary.sabourin@eclipse-foundation.org>
+*
+* SPDX-License-Identifier: EPL-2.0
+**********************************************************************/
+package org.eclipsefoundation.openvsx.api;
+
+import javax.enterprise.context.ApplicationScoped;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+
+import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
+import org.eclipsefoundation.openvsx.api.models.DrupalOAuthData;
+
+/**
+ * Drupal OAuth2 token validation API binding
+ */
+@Path("oauth2/tokens")
+@Produces(MediaType.APPLICATION_JSON)
+@ApplicationScoped
+@RegisterRestClient(configKey = "accounts-api")
+public interface DrupalOAuthAPI {
+
+    @GET
+    @Path("{token}")
+    DrupalOAuthData getTokenInfo(@PathParam("token") String token);
+}
\ No newline at end of file
diff --git a/src/main/java/org/eclipsefoundation/openvsx/api/EclipseAPI.java b/src/main/java/org/eclipsefoundation/openvsx/api/EclipseAPI.java
new file mode 100644
index 0000000000000000000000000000000000000000..39659d78e03b3196c9e679dbb066f4005818db2c
--- /dev/null
+++ b/src/main/java/org/eclipsefoundation/openvsx/api/EclipseAPI.java
@@ -0,0 +1,37 @@
+/*********************************************************************
+* Copyright (c) 2023 Eclipse Foundation.
+*
+* This program and the accompanying materials are made
+* available under the terms of the Eclipse Public License 2.0
+* which is available at https://www.eclipse.org/legal/epl-2.0/
+*
+* Author: Zachary Sabourin <zachary.sabourin@eclipse-foundation.org>
+*
+* SPDX-License-Identifier: EPL-2.0
+**********************************************************************/
+package org.eclipsefoundation.openvsx.api;
+
+import javax.enterprise.context.ApplicationScoped;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+
+import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
+import org.eclipsefoundation.openvsx.api.models.EfUser;
+
+@ApplicationScoped
+@Produces("application/json")
+@RegisterRestClient(configKey = "eclipse-api")
+public interface EclipseAPI {
+
+    /**
+     * Retrieves user objects that matches the given Github username.
+     * 
+     * @param username GH handle to match against EF user
+     * @return the matching Eclipse account or null
+     */
+    @GET
+    @Path("/github/profile/{username}")
+    EfUser getUserByGithubName(@PathParam("username") String username);
+}
diff --git a/src/main/java/org/eclipsefoundation/openvsx/api/PeopleAPI.java b/src/main/java/org/eclipsefoundation/openvsx/api/PeopleAPI.java
new file mode 100644
index 0000000000000000000000000000000000000000..6e6377b2b277b9e63c9a0042bd2ce75c85b6883b
--- /dev/null
+++ b/src/main/java/org/eclipsefoundation/openvsx/api/PeopleAPI.java
@@ -0,0 +1,56 @@
+/*********************************************************************
+* Copyright (c) 2023 Eclipse Foundation.
+*
+* This program and the accompanying materials are made
+* available under the terms of the Eclipse Public License 2.0
+* which is available at https://www.eclipse.org/legal/epl-2.0/
+*
+* Author: Zachary Sabourin <zachary.sabourin@eclipse-foundation.org>
+*
+* SPDX-License-Identifier: EPL-2.0
+**********************************************************************/
+package org.eclipsefoundation.openvsx.api;
+
+import java.util.List;
+
+import javax.annotation.security.RolesAllowed;
+import javax.enterprise.context.ApplicationScoped;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
+import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
+import org.eclipsefoundation.foundationdb.client.model.PeopleDocumentData;
+
+import io.quarkus.oidc.client.filter.OidcClientFilter;
+
+@Path("people")
+@OidcClientFilter
+@Produces(MediaType.APPLICATION_JSON)
+@RegisterRestClient(configKey = "fdndb-api")
+@ApplicationScoped
+public interface PeopleAPI {
+
+    @GET
+    @Path("{personId}/documents")
+    @RolesAllowed("fdb_read_people_documents")
+    List<PeopleDocumentData> getPeopleDocuments(@PathParam("personId") String personId,
+            @QueryParam("documentID") String documentId);
+
+    @PUT
+    @Path("{personId}/documents")
+    @RolesAllowed("fdb_write_people_documents")
+    List<PeopleDocumentData> createPeopleDocument(@PathParam("personId") String personId, PeopleDocumentData src);
+
+    @DELETE
+    @Path("{personId}/documents/{documentId}/{effectiveDate}")
+    @RolesAllowed("fdb_write_people_documents")
+    Response deletePeopleDocument(@PathParam("personId") String personId, @PathParam("documentId") String documentId,
+            @PathParam("effectiveDate") String effectiveDate);
+}
diff --git a/src/main/java/org/eclipsefoundation/openvsx/api/models/AgreementSigningRequest.java b/src/main/java/org/eclipsefoundation/openvsx/api/models/AgreementSigningRequest.java
new file mode 100644
index 0000000000000000000000000000000000000000..1b6a9bd15667f7455959f49b61e401d8418690a7
--- /dev/null
+++ b/src/main/java/org/eclipsefoundation/openvsx/api/models/AgreementSigningRequest.java
@@ -0,0 +1,40 @@
+/*********************************************************************
+* Copyright (c) 2023 Eclipse Foundation.
+*
+* This program and the accompanying materials are made
+* available under the terms of the Eclipse Public License 2.0
+* which is available at https://www.eclipse.org/legal/epl-2.0/
+*
+* Author: Zachary Sabourin <zachary.sabourin@eclipse-foundation.org>
+*
+* SPDX-License-Identifier: EPL-2.0
+**********************************************************************/
+package org.eclipsefoundation.openvsx.api.models;
+
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;
+import com.google.auto.value.AutoValue;
+
+@AutoValue
+@JsonDeserialize(builder = AutoValue_AgreementSigningRequest.Builder.class)
+public abstract class AgreementSigningRequest {
+
+    public abstract String getVersion();
+
+    public abstract String getGithubHandle();
+
+    public static Builder builder() {
+        return new AutoValue_AgreementSigningRequest.Builder();
+    }
+
+    @AutoValue.Builder
+    @JsonPOJOBuilder(withPrefix = "set")
+    public abstract static class Builder {
+
+        public abstract Builder setVersion(String version);
+
+        public abstract Builder setGithubHandle(String handle);
+
+        public abstract AgreementSigningRequest build();
+    }
+}
diff --git a/src/main/java/org/eclipsefoundation/openvsx/api/models/DrupalOAuthData.java b/src/main/java/org/eclipsefoundation/openvsx/api/models/DrupalOAuthData.java
new file mode 100644
index 0000000000000000000000000000000000000000..917479b96318572043ca7104c516a476d7031c2d
--- /dev/null
+++ b/src/main/java/org/eclipsefoundation/openvsx/api/models/DrupalOAuthData.java
@@ -0,0 +1,55 @@
+/*********************************************************************
+* Copyright (c) 2023 Eclipse Foundation.
+*
+* This program and the accompanying materials are made
+* available under the terms of the Eclipse Public License 2.0
+* which is available at https://www.eclipse.org/legal/epl-2.0/
+*
+* Author: Zachary Sabourin <zachary.sabourin@eclipse-foundation.org>
+*
+* SPDX-License-Identifier: EPL-2.0
+**********************************************************************/
+package org.eclipsefoundation.openvsx.api.models;
+
+import javax.annotation.Nullable;
+
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;
+import com.google.auto.value.AutoValue;
+
+@AutoValue
+@JsonDeserialize(builder = AutoValue_DrupalOAuthData.Builder.class)
+public abstract class DrupalOAuthData {
+
+    public abstract String getClientId();
+
+    @Nullable
+    public abstract String getUserId();
+
+    public abstract String getAccessToken();
+
+    public abstract long getExpires();
+
+    public abstract String getScope();
+
+    public static Builder builder() {
+        return new AutoValue_DrupalOAuthData.Builder();
+    }
+
+    @AutoValue.Builder
+    @JsonPOJOBuilder(withPrefix = "set")
+    public abstract static class Builder {
+
+        public abstract Builder setClientId(String id);
+
+        public abstract Builder setUserId(@Nullable String id);
+
+        public abstract Builder setAccessToken(String token);
+
+        public abstract Builder setExpires(long expires);
+
+        public abstract Builder setScope(String scope);
+
+        public abstract DrupalOAuthData build();
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/org/eclipsefoundation/openvsx/api/models/EfUser.java b/src/main/java/org/eclipsefoundation/openvsx/api/models/EfUser.java
new file mode 100644
index 0000000000000000000000000000000000000000..6f3dd60d2db8b55642fabe66313dc83eaf8f56fd
--- /dev/null
+++ b/src/main/java/org/eclipsefoundation/openvsx/api/models/EfUser.java
@@ -0,0 +1,40 @@
+/*********************************************************************
+* Copyright (c) 2023 Eclipse Foundation.
+*
+* This program and the accompanying materials are made
+* available under the terms of the Eclipse Public License 2.0
+* which is available at https://www.eclipse.org/legal/epl-2.0/
+*
+* Author: Zachary Sabourin <zachary.sabourin@eclipse-foundation.org>
+*
+* SPDX-License-Identifier: EPL-2.0
+**********************************************************************/
+package org.eclipsefoundation.openvsx.api.models;
+
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;
+import com.google.auto.value.AutoValue;
+
+@AutoValue
+@JsonDeserialize(builder = AutoValue_EfUser.Builder.class)
+public abstract class EfUser {
+
+    public abstract String getName();
+
+    public abstract String getGithubHandle();
+
+    public static Builder builder() {
+        return new AutoValue_EfUser.Builder();
+    }
+
+    @AutoValue.Builder
+    @JsonPOJOBuilder(withPrefix = "set")
+    public abstract static class Builder {
+
+        public abstract Builder setName(String name);
+
+        public abstract Builder setGithubHandle(String handle);
+
+        public abstract EfUser build();
+    }
+}
diff --git a/src/main/java/org/eclipsefoundation/openvsx/helpers/DrupalAuthHelper.java b/src/main/java/org/eclipsefoundation/openvsx/helpers/DrupalAuthHelper.java
new file mode 100644
index 0000000000000000000000000000000000000000..57d7e1e43c4bd865cfddd1f2aff62eba8ba4a203
--- /dev/null
+++ b/src/main/java/org/eclipsefoundation/openvsx/helpers/DrupalAuthHelper.java
@@ -0,0 +1,65 @@
+/*********************************************************************
+* Copyright (c) 2023 Eclipse Foundation.
+*
+* This program and the accompanying materials are made
+* available under the terms of the Eclipse Public License 2.0
+* which is available at https://www.eclipse.org/legal/epl-2.0/
+*
+* Author: Zachary Sabourin <zachary.sabourin@eclipse-foundation.org>
+*
+* SPDX-License-Identifier: EPL-2.0
+**********************************************************************/
+package org.eclipsefoundation.openvsx.helpers;
+
+import java.time.Instant;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Defines helper methods for Drupal OAuth2 token validation.
+ */
+public class DrupalAuthHelper {
+
+    /**
+     * Validates whether a given expiry date has passed.
+     * 
+     * @param expiryDate The token expiry date in seconds since epoch.
+     * @return True if expiry date is older than current time. False if not.
+     */
+    public static boolean isExpired(long expiryDate) {
+        return expiryDate <= Instant.now().getEpochSecond();
+    }
+
+    /**
+     * Validates whether the token scopes and valid scopes are the same.
+     * 
+     * @param tokenScope  The space-separated token scopes.
+     * @param validScopes The list of valid scopes.
+     * @return Returns false if any token scopes are not in the list of valid
+     *         scopes. True if all match.
+     */
+    public static boolean hasScopes(String tokenScope, List<String> validScopes) {
+
+        List<String> tokenScopes = Arrays.asList(tokenScope.split(" "));
+
+        Collections.sort(tokenScopes);
+        Collections.sort(validScopes);
+
+        return tokenScopes.containsAll(validScopes);
+    }
+
+    /**
+     * Validates whether the the token's client id matches a desired client id.
+     * 
+     * @param clientId       The client id associated with the token
+     * @param validClientIds The valid client ids allowed in the service.
+     * @return True if ids match. False if not.
+     */
+    public static boolean hasValidclientId(String clientId, List<String> validClientIds) {
+        return validClientIds.stream().anyMatch(id -> id.equalsIgnoreCase(clientId));
+    }
+
+    private DrupalAuthHelper() {
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/org/eclipsefoundation/openvsx/request/OAuthFilter.java b/src/main/java/org/eclipsefoundation/openvsx/request/OAuthFilter.java
new file mode 100644
index 0000000000000000000000000000000000000000..b9f05e61d9c6a05b2f8f455398b9b76459d1d651
--- /dev/null
+++ b/src/main/java/org/eclipsefoundation/openvsx/request/OAuthFilter.java
@@ -0,0 +1,83 @@
+/*********************************************************************
+* Copyright (c) 2023 Eclipse Foundation.
+*
+* This program and the accompanying materials are made
+* available under the terms of the Eclipse Public License 2.0
+* which is available at https://www.eclipse.org/legal/epl-2.0/
+*
+* Author: Zachary Sabourin <zachary.sabourin@eclipse-foundation.org>
+*
+* SPDX-License-Identifier: EPL-2.0
+**********************************************************************/
+package org.eclipsefoundation.openvsx.request;
+
+import java.io.IOException;
+import java.util.List;
+
+import javax.enterprise.inject.Instance;
+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.FinalForbiddenException;
+import org.eclipsefoundation.openvsx.api.models.DrupalOAuthData;
+import org.eclipsefoundation.openvsx.services.DrupalOAuthService;
+import org.jboss.resteasy.util.HttpHeaderNames;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Provider
+public class OAuthFilter implements ContainerRequestFilter {
+    private static final Logger LOGGER = LoggerFactory.getLogger(OAuthFilter.class);
+
+    public static final String TOKEN_STATUS = "tokenStatus";
+
+    @ConfigProperty(name = "eclipse.openvsx.scopes", defaultValue = "openvsx_publisher_agreement")
+    Instance<List<String>> validScopes;
+
+    @ConfigProperty(name = "eclipse.openvsx.clients")
+    Instance<List<String>> validClientIds;
+
+    @ConfigProperty(name = "eclipse.openvsx.oauth-filter.enabled", defaultValue = "false")
+    Instance<Boolean> isEnabled;
+
+    @Inject
+    DrupalOAuthService oauthService;
+
+    @Override
+    public void filter(ContainerRequestContext requestContext) throws IOException {
+        if (Boolean.TRUE.equals(isEnabled.get())) {
+            String token = stripBearerToken(requestContext.getHeaderString(HttpHeaderNames.AUTHORIZATION));
+            DrupalOAuthData tokenStatus = oauthService.validateTokenStatus(token, validScopes.get(),
+                    validClientIds.get());
+            if (tokenStatus != null && tokenStatus.getUserId() != null) {
+                requestContext.setProperty(TOKEN_STATUS, tokenStatus);
+            }
+        }
+    }
+
+    /**
+     * Strips the bearer token down to just the token. Throws a
+     * FinalForbiddenException if the token is in an invalid format.
+     * 
+     * @param header the bearer token suthorization header
+     * @return The raw token string.
+     */
+    private String stripBearerToken(String header) {
+        if (header == null || header.equals("")) {
+            throw new FinalForbiddenException("Invalid token");
+        }
+
+        String[] splits = header.split(" ");
+
+        // Invalid token if no space or more than one space
+        if (splits.length != 2 || !splits[0].equals("Bearer")) {
+            LOGGER.error("Invalid auth header: {}", header);
+            throw new FinalForbiddenException("Invalid token");
+        }
+
+        return splits[1];
+    }
+}
diff --git a/src/main/java/org/eclipsefoundation/openvsx/resources/GreetingResource.java b/src/main/java/org/eclipsefoundation/openvsx/resources/GreetingResource.java
deleted file mode 100644
index f85612ff7962ec49652ba3e668f47ee9193fb3a5..0000000000000000000000000000000000000000
--- a/src/main/java/org/eclipsefoundation/openvsx/resources/GreetingResource.java
+++ /dev/null
@@ -1,24 +0,0 @@
-/*********************************************************************
-* Copyright (c) 2023 Eclipse Foundation.
-*
-* This program and the accompanying materials are made
-* available under the terms of the Eclipse Public License 2.0
-* which is available at https://www.eclipse.org/legal/epl-2.0/
-*
-* Author: Zachary Sabourin <zachary.sabourin@eclipse-foundation.org>
-*
-* SPDX-License-Identifier: EPL-2.0
-**********************************************************************/
-package org.eclipsefoundation.openvsx.resources;
-
-import javax.ws.rs.GET;
-import javax.ws.rs.Path;
-
-@Path("/hello")
-public class GreetingResource {
-
-    @GET
-    public String hello() {
-        return "Hello RESTEasy";
-    }
-}
\ No newline at end of file
diff --git a/src/main/java/org/eclipsefoundation/openvsx/resources/PublisherAgreementResource.java b/src/main/java/org/eclipsefoundation/openvsx/resources/PublisherAgreementResource.java
new file mode 100644
index 0000000000000000000000000000000000000000..95f8555a191b24936e0a0fd99cf0311b555ec6db
--- /dev/null
+++ b/src/main/java/org/eclipsefoundation/openvsx/resources/PublisherAgreementResource.java
@@ -0,0 +1,95 @@
+/*********************************************************************
+* Copyright (c) 2023 Eclipse Foundation.
+*
+* This program and the accompanying materials are made
+* available under the terms of the Eclipse Public License 2.0
+* which is available at https://www.eclipse.org/legal/epl-2.0/
+*
+* Author: Zachary Sabourin <zachary.sabourin@eclipse-foundation.org>
+*
+* SPDX-License-Identifier: EPL-2.0
+**********************************************************************/
+package org.eclipsefoundation.openvsx.resources;
+
+import javax.inject.Inject;
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.BadRequestException;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.NotFoundException;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.Response;
+
+import org.eclipsefoundation.core.exception.FinalForbiddenException;
+import org.eclipsefoundation.foundationdb.client.model.PeopleDocumentData;
+import org.eclipsefoundation.openvsx.api.models.AgreementSigningRequest;
+import org.eclipsefoundation.openvsx.api.models.DrupalOAuthData;
+import org.eclipsefoundation.openvsx.request.OAuthFilter;
+import org.eclipsefoundation.openvsx.services.PublisherAgreementService;
+
+@Path("publisher_agreement")
+public class PublisherAgreementResource {
+
+    @Context
+    HttpServletRequest request;
+
+    @Inject
+    PublisherAgreementService agreementService;
+
+    @GET
+    public Response getAgreement() {
+        // Endpoint uses currently logged in user. Token must have user_id
+        return getAgreementForUser(extractUserIdFromToken());
+    }
+
+    @POST
+    public Response createAgreement(AgreementSigningRequest body) {
+
+        // Endpoint uses currently logged in user. Token must have user_id
+        PeopleDocumentData result = agreementService.createPublisherAgreement(extractUserIdFromToken(), body);
+        if (result == null) {
+            throw new BadRequestException("Unable to create Publisher Agreement with current request parameters");
+        }
+
+        return Response.ok(result).build();
+    }
+
+    @GET
+    @Path("{efUsername}")
+    public Response getAgreementForUser(@PathParam("efUsername") String username) {
+        PeopleDocumentData result = agreementService.getPublisherAgreement(username);
+        if (result == null) {
+            throw new NotFoundException(String.format("Unable to find agreement for user: %s", username));
+        }
+
+        return Response.ok(result).build();
+    }
+
+    @DELETE
+    @Path("{efUsername}")
+    public Response deleteAgreementForUser(@PathParam("efUsername") String username) {
+        if (!agreementService.deletePublisherAgreement(username)) {
+            throw new BadRequestException(String.format("Unable to delete agreement for user: %s", username));
+        }
+
+        return Response.ok().build();
+    }
+
+    /**
+     * Extracts the user_id from the token data in order to determine a user is
+     * signed in. Throws a FinalForbiddenException if there is no user associated
+     * with this token.
+     * 
+     * @return The token user id.
+     */
+    private String extractUserIdFromToken() {
+        DrupalOAuthData tokenStatus = (DrupalOAuthData) request.getAttribute(OAuthFilter.TOKEN_STATUS);
+        if (tokenStatus == null || tokenStatus.getUserId() == null) {
+            throw new FinalForbiddenException("No user associated with this token");
+        }
+        return tokenStatus.getUserId();
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/org/eclipsefoundation/openvsx/services/DrupalOAuthService.java b/src/main/java/org/eclipsefoundation/openvsx/services/DrupalOAuthService.java
new file mode 100644
index 0000000000000000000000000000000000000000..d544c79ccf838e62cbf4f7f8993804d603d4103f
--- /dev/null
+++ b/src/main/java/org/eclipsefoundation/openvsx/services/DrupalOAuthService.java
@@ -0,0 +1,34 @@
+/*********************************************************************
+* Copyright (c) 2023 Eclipse Foundation.
+*
+* This program and the accompanying materials are made
+* available under the terms of the Eclipse Public License 2.0
+* which is available at https://www.eclipse.org/legal/epl-2.0/
+*
+* Author: Zachary Sabourin <zachary.sabourin@eclipse-foundation.org>
+*
+* SPDX-License-Identifier: EPL-2.0
+**********************************************************************/
+package org.eclipsefoundation.openvsx.services;
+
+import java.util.List;
+
+import org.eclipsefoundation.openvsx.api.models.DrupalOAuthData;
+
+/**
+ * Defines a Drupal Oauth validation service.
+ */
+public interface DrupalOAuthService {
+
+    /**
+     * Validates an OAuth2 token using a list of valid scopes. Also validates token
+     * expiry and client id. Returns an OAuthTokenStatus entity containing token
+     * validity information as well as the basis for denial if token deemed invalid.
+     * 
+     * @param token         The token string.
+     * @param validScopes   A list of valid scopes for this token.
+     * @param validClientId A valid client_id for this token.
+     * @return A OAuthTokenStatus entity containing status info.
+     */
+    DrupalOAuthData validateTokenStatus(String token, List<String> validScopes, List<String> validClientIds);
+}
\ No newline at end of file
diff --git a/src/main/java/org/eclipsefoundation/openvsx/services/PublisherAgreementService.java b/src/main/java/org/eclipsefoundation/openvsx/services/PublisherAgreementService.java
new file mode 100644
index 0000000000000000000000000000000000000000..77531710bfd8df50e3604308decce8269cb273a3
--- /dev/null
+++ b/src/main/java/org/eclipsefoundation/openvsx/services/PublisherAgreementService.java
@@ -0,0 +1,54 @@
+/*********************************************************************
+* Copyright (c) 2023 Eclipse Foundation.
+*
+* This program and the accompanying materials are made
+* available under the terms of the Eclipse Public License 2.0
+* which is available at https://www.eclipse.org/legal/epl-2.0/
+*
+* Author: Zachary Sabourin <zachary.sabourin@eclipse-foundation.org>
+*
+* SPDX-License-Identifier: EPL-2.0
+**********************************************************************/
+package org.eclipsefoundation.openvsx.services;
+
+import org.eclipsefoundation.foundationdb.client.model.PeopleDocumentData;
+import org.eclipsefoundation.openvsx.api.models.AgreementSigningRequest;
+
+/**
+ * Defines the service for fetching, creating and deleting openvsx publisher
+ * agreements.
+ */
+public interface PublisherAgreementService {
+
+    /**
+     * Retrieves the most recent openvsx publisher agreement for the given user.
+     * 
+     * @param username The desired user's EF username.
+     * @return A PeopleDocumentData entity or null.
+     */
+    public PeopleDocumentData getPublisherAgreement(String username);
+
+    /**
+     * Attempts to create a publisher agreement for the given user using the request
+     * body contining the user GH handle and document version. Validates the GH
+     * handle exists and is tied to the desired username. Returns null if the GH
+     * handle is incorrect, if it can't create the signed document, or if it
+     * encounters an error.
+     * 
+     * @param username The desired user's EF username.
+     * @param request  The rrequest body containing the GH handle and coument
+     *                 version.
+     * @return A PeopleDocumentData entity or null.
+     */
+    public PeopleDocumentData createPublisherAgreement(String username, AgreementSigningRequest request);
+
+    /**
+     * Attempts to delete the most recent publisher agreement for the given user and
+     * removes the entry from the cache. Returns false if document can not be found.
+     * Returns true if deletion was successful.
+     * 
+     * @param username the desired user's EF username.
+     * @return True if success, false if not.
+     */
+    public boolean deletePublisherAgreement(String username);
+}
diff --git a/src/main/java/org/eclipsefoundation/openvsx/services/impl/DefaultDrupalOAuthService.java b/src/main/java/org/eclipsefoundation/openvsx/services/impl/DefaultDrupalOAuthService.java
new file mode 100644
index 0000000000000000000000000000000000000000..191f845e526d6ba0ef7f09d6d8365a68ede0d68a
--- /dev/null
+++ b/src/main/java/org/eclipsefoundation/openvsx/services/impl/DefaultDrupalOAuthService.java
@@ -0,0 +1,59 @@
+/*********************************************************************
+* Copyright (c) 2023 Eclipse Foundation.
+*
+* This program and the accompanying materials are made
+* available under the terms of the Eclipse Public License 2.0
+* which is available at https://www.eclipse.org/legal/epl-2.0/
+*
+* Author: Zachary Sabourin <zachary.sabourin@eclipse-foundation.org>
+*
+* SPDX-License-Identifier: EPL-2.0
+**********************************************************************/
+package org.eclipsefoundation.openvsx.services.impl;
+
+import java.util.List;
+
+import javax.enterprise.context.ApplicationScoped;
+import javax.inject.Inject;
+
+import org.eclipse.microprofile.rest.client.inject.RestClient;
+import org.eclipsefoundation.core.exception.FinalForbiddenException;
+import org.eclipsefoundation.openvsx.api.DrupalOAuthAPI;
+import org.eclipsefoundation.openvsx.api.models.DrupalOAuthData;
+import org.eclipsefoundation.openvsx.helpers.DrupalAuthHelper;
+import org.eclipsefoundation.openvsx.services.DrupalOAuthService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@ApplicationScoped
+public class DefaultDrupalOAuthService implements DrupalOAuthService {
+    private static final Logger LOGGER = LoggerFactory.getLogger(DefaultDrupalOAuthService.class);
+
+    @Inject
+    @RestClient
+    DrupalOAuthAPI oauthAPI;
+
+    @Override
+    public DrupalOAuthData validateTokenStatus(String token, List<String> validScopes, List<String> validClientIds) {
+        try {
+            LOGGER.debug("Validating token: {}", token);
+
+            DrupalOAuthData tokenData = oauthAPI.getTokenInfo(token);
+
+            if (DrupalAuthHelper.isExpired(tokenData.getExpires())) {
+                throw new FinalForbiddenException("The Authorization token is expired");
+            }
+            if (!DrupalAuthHelper.hasScopes(tokenData.getScope(), validScopes)) {
+                throw new FinalForbiddenException("The Authorization token does not have the required permissions/scopes");
+            }
+            if (!DrupalAuthHelper.hasValidclientId(tokenData.getClientId(), validClientIds)) {
+                throw new FinalForbiddenException("The Authorization token has an Invalid client_id");
+            }
+
+            return tokenData;
+        } catch (Exception e) {
+            LOGGER.error("Error validating token", e);
+            throw new FinalForbiddenException("Error validating token");
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/org/eclipsefoundation/openvsx/services/impl/DefaultPublisherAgreementService.java b/src/main/java/org/eclipsefoundation/openvsx/services/impl/DefaultPublisherAgreementService.java
new file mode 100644
index 0000000000000000000000000000000000000000..c2f8b61d28e3ce17eb1ede007a63f0405b52f8ed
--- /dev/null
+++ b/src/main/java/org/eclipsefoundation/openvsx/services/impl/DefaultPublisherAgreementService.java
@@ -0,0 +1,129 @@
+/*********************************************************************
+* Copyright (c) 2023 Eclipse Foundation.
+*
+* This program and the accompanying materials are made
+* available under the terms of the Eclipse Public License 2.0
+* which is available at https://www.eclipse.org/legal/epl-2.0/
+*
+* Author: Zachary Sabourin <zachary.sabourin@eclipse-foundation.org>
+*
+* SPDX-License-Identifier: EPL-2.0
+**********************************************************************/
+package org.eclipsefoundation.openvsx.services.impl;
+
+import java.util.Date;
+import java.util.List;
+import java.util.Optional;
+
+import javax.enterprise.context.ApplicationScoped;
+import javax.inject.Inject;
+import javax.ws.rs.core.Response;
+
+import org.eclipse.microprofile.config.inject.ConfigProperty;
+import org.eclipse.microprofile.rest.client.inject.RestClient;
+import org.eclipsefoundation.core.helper.DateTimeHelper;
+import org.eclipsefoundation.core.service.CachingService;
+import org.eclipsefoundation.core.service.CachingService.ParameterizedCacheKey;
+import org.eclipsefoundation.foundationdb.client.model.PeopleDocumentData;
+import org.eclipsefoundation.openvsx.api.EclipseAPI;
+import org.eclipsefoundation.openvsx.api.PeopleAPI;
+import org.eclipsefoundation.openvsx.api.models.AgreementSigningRequest;
+import org.eclipsefoundation.openvsx.api.models.EfUser;
+import org.eclipsefoundation.openvsx.services.PublisherAgreementService;
+import org.jboss.resteasy.specimpl.MultivaluedMapImpl;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@ApplicationScoped
+public class DefaultPublisherAgreementService implements PublisherAgreementService {
+    private static final Logger LOGGER = LoggerFactory.getLogger(DefaultPublisherAgreementService.class);
+
+    @ConfigProperty(name = "eclipse.openvsx.doc-id")
+    String openvsxDocId;
+
+    @Inject
+    @RestClient
+    PeopleAPI peopleAPI;
+
+    @Inject
+    @RestClient
+    EclipseAPI eclipseAPI;
+
+    @Inject
+    CachingService cache;
+
+    @Override
+    public PeopleDocumentData getPublisherAgreement(String username) {
+        try {
+            Optional<List<PeopleDocumentData>> results = cache.get(username, new MultivaluedMapImpl<>(),
+                    PeopleDocumentData.class, () -> peopleAPI.getPeopleDocuments(username, openvsxDocId));
+
+            if (results.isEmpty() || results.get().isEmpty()) {
+                LOGGER.error("Unable to find agreements for user with name: {}", username);
+                return null;
+            }
+
+            // Sort by date. Returning most recent
+            return results.get().stream()
+                    .sorted((pDoc1, pDoc2) -> pDoc2.getEffectiveDate().compareTo(pDoc1.getEffectiveDate()))
+                    .findFirst().orElse(null);
+        } catch (Exception e) {
+            LOGGER.error("Error while fetching publisher agreements for user: {}", username, e);
+            return null;
+        }
+    }
+
+    @Override
+    public PeopleDocumentData createPublisherAgreement(String username, AgreementSigningRequest body) {
+        try {
+            Optional<EfUser> ghResult = cache.get("gh: " + body.getGithubHandle(), new MultivaluedMapImpl<>(),
+                    Response.class, () -> eclipseAPI.getUserByGithubName(body.getGithubHandle()));
+
+            if (ghResult.isEmpty() || ghResult.get() == null) {
+                LOGGER.error("Unable to find account for GH user: {}", body.getGithubHandle());
+                return null;
+            }
+
+            // Ensure username in URL matches GH fetch username
+            if (!ghResult.get().getName().equalsIgnoreCase(username)) {
+                LOGGER.error("Invalid GH handle for use: {}", username);
+                return null;
+            }
+
+            PeopleDocumentData fdnDbRequest = PeopleDocumentData.builder()
+                    .setPersonID(username)
+                    .setDocumentID(openvsxDocId)
+                    .setVersion(Double.parseDouble(body.getVersion()))
+                    .setEffectiveDate(new Date())
+                    .setReceivedDate(new Date())
+                    .build();
+
+            // Submit new document to fdnDb for creation, can fail if invalid version is set
+            List<PeopleDocumentData> results = peopleAPI.createPeopleDocument(username, fdnDbRequest);
+            if (results == null || results.isEmpty()) {
+                LOGGER.error("Unable to create agreement for user with name: {}", username);
+                return null;
+            }
+
+            return results.get(0);
+
+        } catch (Exception e) {
+            LOGGER.error("Error while creating publisher agreement", e);
+            return null;
+        }
+    }
+
+    @Override
+    public boolean deletePublisherAgreement(String username) {
+        // Get most recent document if it exists
+        PeopleDocumentData mostRecent = getPublisherAgreement(username);
+        if (mostRecent == null) {
+            return false;
+        }
+
+        // Delete and remove from cache entry
+        peopleAPI.deletePeopleDocument(username, openvsxDocId, DateTimeHelper.toRFC3339(mostRecent.getEffectiveDate()));
+        cache.remove(new ParameterizedCacheKey(PeopleDocumentData.class, username, new MultivaluedMapImpl<>()));
+        return true;
+    }
+}
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index 841a00313cfd4a91cc1a48cafd8ca307ffbe276f..0847c4171abc63f0c6fbe02702b6eaf248c22d79 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -1,8 +1,16 @@
 quarkus.http.root-path=/openvsx
 
-fdndb-api/mp-rest/url=http://localhost:8095
-fdndb-api/mp-rest/scope=javax.inject.Singleton
+fdndb-api/mp-rest/url=http://foundationdb:8095
+%dev.fdndb-api/mp-rest/url=http://localhost:10112
+
+accounts-api/mp-rest/url=https://accounts.eclipse.org
+%dev.accounts-api/mp-rest/url=https://accounts-staging.eclipse.org
+
+eclipse-api/mp-rest/url=https://api.eclipse.org
 
 quarkus.oidc.enabled=false
 
+eclipse.openvsx.oauth-filter.enabled=true
+%dev.eclipse.openvsx.oauth-filter.enabled=true
+
 quarkus.log.level=INFO
\ No newline at end of file
diff --git a/src/test/java/org/eclipsefoundation/openvsx/helpers/DrupalAuthHelperTest.java b/src/test/java/org/eclipsefoundation/openvsx/helpers/DrupalAuthHelperTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..097774335b9a807edb9c1123c68c097c55e1e4f8
--- /dev/null
+++ b/src/test/java/org/eclipsefoundation/openvsx/helpers/DrupalAuthHelperTest.java
@@ -0,0 +1,117 @@
+/*********************************************************************
+* Copyright (c) 2023 Eclipse Foundation.
+*
+* This program and the accompanying materials are made
+* available under the terms of the Eclipse Public License 2.0
+* which is available at https://www.eclipse.org/legal/epl-2.0/
+*
+* Author: Zachary Sabourin <zachary.sabourin@eclipse-foundation.org>
+*
+* SPDX-License-Identifier: EPL-2.0
+**********************************************************************/
+package org.eclipsefoundation.openvsx.helpers;
+
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import io.quarkus.test.junit.QuarkusTest;
+
+@QuarkusTest
+class DrupalAuthHelperTest {
+
+    @Test
+    void testExpiry_notExpired() {
+        Assertions.assertFalse(DrupalAuthHelper.isExpired(Instant.now().getEpochSecond() + 20000));
+        Assertions.assertFalse(DrupalAuthHelper.isExpired(Instant.now().getEpochSecond() + 3000));
+        Assertions.assertFalse(DrupalAuthHelper.isExpired(Instant.now().getEpochSecond() + 100));
+    }
+
+    @Test
+    void testExpiry_expired() {
+        Assertions.assertTrue(DrupalAuthHelper.isExpired(Instant.now().getEpochSecond() - 20000));
+        Assertions.assertTrue(DrupalAuthHelper.isExpired(Instant.now().getEpochSecond() - 3000));
+        Assertions.assertTrue(DrupalAuthHelper.isExpired(Instant.now().getEpochSecond() - 100));
+    }
+
+    @Test
+    void testScope_success() {
+        List<String> validScopes = new ArrayList<>();
+        validScopes.add("read");
+        validScopes.add("write");
+        validScopes.add("admin");
+
+        Assertions.assertTrue(DrupalAuthHelper.hasScopes("read write admin", validScopes));
+
+        validScopes = new ArrayList<>();
+        validScopes.add("read");
+        validScopes.add("write");
+
+        Assertions.assertTrue(DrupalAuthHelper.hasScopes("read write", validScopes));
+
+        validScopes = new ArrayList<>();
+        validScopes.add("read");
+        validScopes.add("admin");
+
+        Assertions.assertTrue(DrupalAuthHelper.hasScopes("read admin", validScopes));
+
+        validScopes = new ArrayList<>();
+        validScopes.add("read");
+
+        Assertions.assertTrue(DrupalAuthHelper.hasScopes("read", validScopes));
+    }
+
+    @Test
+    void testScope_failure() {
+        List<String> validScopes = new ArrayList<>();
+        validScopes.add("read");
+        validScopes.add("write");
+        validScopes.add("admin");
+
+        Assertions.assertFalse(DrupalAuthHelper.hasScopes("read write guy", validScopes));
+
+        validScopes = new ArrayList<>();
+        validScopes.add("read");
+        validScopes.add("write");
+
+        Assertions.assertFalse(DrupalAuthHelper.hasScopes("read", validScopes));
+
+        validScopes = new ArrayList<>();
+        validScopes.add("read");
+        validScopes.add("admin");
+
+        Assertions.assertFalse(DrupalAuthHelper.hasScopes("admin,read", validScopes));
+
+        validScopes = new ArrayList<>();
+        validScopes.add("read");
+
+        Assertions.assertFalse(DrupalAuthHelper.hasScopes("admin", validScopes));
+    }
+
+    @Test
+    void testClientId_success() {
+        List<String> validIds = new ArrayList<>();
+        validIds.add("client-id");
+        validIds.add("test-id");
+        validIds.add("valid-id");
+
+        Assertions.assertTrue(DrupalAuthHelper.hasValidclientId("client-id", validIds));
+        Assertions.assertTrue(DrupalAuthHelper.hasValidclientId("test-id", validIds));
+        Assertions.assertTrue(DrupalAuthHelper.hasValidclientId("valid-id", validIds));
+    }
+
+    @Test
+    void testClientId_failure() {
+        List<String> validIds = new ArrayList<>();
+        validIds.add("client-id");
+        validIds.add("test-id");
+        validIds.add("valid-id");
+
+        Assertions.assertFalse(DrupalAuthHelper.hasValidclientId("invalid-id", validIds));
+        Assertions.assertFalse(DrupalAuthHelper.hasValidclientId("not-id", validIds));
+        Assertions.assertFalse(DrupalAuthHelper.hasValidclientId("no-valid", validIds));
+    }
+}
\ No newline at end of file
diff --git a/src/test/java/org/eclipsefoundation/openvsx/resources/GreetingResourceTest.java b/src/test/java/org/eclipsefoundation/openvsx/resources/GreetingResourceTest.java
deleted file mode 100644
index 317b00f858d93688eadd1b2197d7390336589e61..0000000000000000000000000000000000000000
--- a/src/test/java/org/eclipsefoundation/openvsx/resources/GreetingResourceTest.java
+++ /dev/null
@@ -1,33 +0,0 @@
-/*********************************************************************
-* Copyright (c) 2023 Eclipse Foundation.
-*
-* This program and the accompanying materials are made
-* available under the terms of the Eclipse Public License 2.0
-* which is available at https://www.eclipse.org/legal/epl-2.0/
-*
-* Author: Zachary Sabourin <zachary.sabourin@eclipse-foundation.org>
-*
-* SPDX-License-Identifier: EPL-2.0
-**********************************************************************/
-package org.eclipsefoundation.openvsx.resources;
-
-import static io.restassured.RestAssured.given;
-import static org.hamcrest.CoreMatchers.is;
-
-import org.junit.jupiter.api.Test;
-
-import io.quarkus.test.junit.QuarkusTest;
-
-@QuarkusTest
-class GreetingResourceTest {
-
-    @Test
-    void testHelloEndpoint() {
-        given()
-                .when().get("/hello")
-                .then()
-                .statusCode(200)
-                .body(is("Hello RESTEasy"));
-    }
-
-}
\ No newline at end of file
diff --git a/src/test/java/org/eclipsefoundation/openvsx/resources/PublisherAgreementResourceTest.java b/src/test/java/org/eclipsefoundation/openvsx/resources/PublisherAgreementResourceTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..c1ceb4c419af7c38f67a84acc8442c169fca2424
--- /dev/null
+++ b/src/test/java/org/eclipsefoundation/openvsx/resources/PublisherAgreementResourceTest.java
@@ -0,0 +1,237 @@
+/*********************************************************************
+* Copyright (c) 2023 Eclipse Foundation.
+*
+* This program and the accompanying materials are made
+* available under the terms of the Eclipse Public License 2.0
+* which is available at https://www.eclipse.org/legal/epl-2.0/
+*
+* Author: Zachary Sabourin <zachary.sabourin@eclipse-foundation.org>
+*
+* SPDX-License-Identifier: EPL-2.0
+**********************************************************************/
+package org.eclipsefoundation.openvsx.resources;
+
+import java.util.Map;
+import java.util.Optional;
+
+import javax.inject.Inject;
+
+import org.eclipsefoundation.openvsx.api.models.AgreementSigningRequest;
+import org.eclipsefoundation.openvsx.test.helpers.SchemaNamespaceHelper;
+import org.eclipsefoundation.testing.helpers.TestCaseHelper;
+import org.eclipsefoundation.testing.templates.RestAssuredTemplates;
+import org.eclipsefoundation.testing.templates.RestAssuredTemplates.EndpointTestCase;
+import org.junit.jupiter.api.Test;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import io.quarkus.test.junit.QuarkusTest;
+import io.restassured.http.ContentType;
+
+@QuarkusTest
+class PublisherAgreementResourceTest {
+    public static final String BASE_URL = "publisher_agreement";
+    public static final String USER_URL = BASE_URL + "/{efusername}";
+
+    public static final Optional<Map<String, Object>> userCreds = Optional
+            .of(Map.of("Authorization", "Bearer token2"));
+
+    public static final Optional<Map<String, Object>> userNoDocCreds = Optional
+            .of(Map.of("Authorization", "Bearer token5"));
+
+    public static final Optional<Map<String, Object>> invalidCreds = Optional
+            .of(Map.of("Authorization", "Bearer token1"));
+
+    public static final EndpointTestCase GET_CURRENT_SUCCESS = TestCaseHelper
+            .prepareTestCase(BASE_URL, new String[] {}, SchemaNamespaceHelper.PUBLISHER_AGREEMENT_SCHEMA_PATH)
+            .setHeaderParams(userCreds).build();
+
+    public static final EndpointTestCase GET_CURRENT_NOT_FOUND = TestCaseHelper
+            .prepareTestCase(BASE_URL, new String[] {}, SchemaNamespaceHelper.ERROR_SCHEMA_PATH)
+            .setStatusCode(404).setHeaderParams(userNoDocCreds).build();
+
+    public static final EndpointTestCase BAD_CREDS = TestCaseHelper
+            .prepareTestCase(BASE_URL, new String[] {}, null).setStatusCode(403)
+            .setHeaderParams(invalidCreds).build();
+
+    public static final EndpointTestCase POST_CURRENT_INVALID_HANDLE = TestCaseHelper
+            .prepareTestCase(BASE_URL, new String[] {}, SchemaNamespaceHelper.ERROR_SCHEMA_PATH)
+            .setStatusCode(400).setHeaderParams(userCreds).build();
+
+    public static final EndpointTestCase GET_USER_SUCCESS = TestCaseHelper
+            .prepareTestCase(USER_URL, new String[] { "fakeuser" },
+                    SchemaNamespaceHelper.PUBLISHER_AGREEMENT_SCHEMA_PATH)
+            .setHeaderParams(userNoDocCreds).build();
+
+    public static final EndpointTestCase GET_USER_NOT_FOUND = TestCaseHelper
+            .prepareTestCase(USER_URL, new String[] { "name" }, SchemaNamespaceHelper.ERROR_SCHEMA_PATH)
+            .setStatusCode(404).setHeaderParams(userNoDocCreds).build();
+
+    public static final EndpointTestCase FOR_USER_BAD_CREDS = TestCaseHelper
+            .prepareTestCase(USER_URL, new String[] { "fakeuser" },
+                    SchemaNamespaceHelper.PUBLISHER_AGREEMENT_SCHEMA_PATH)
+            .setStatusCode(403).setHeaderParams(invalidCreds).build();
+
+    public static final EndpointTestCase DELETE_FOR_USER_INVALID_USER = TestCaseHelper
+            .prepareTestCase(USER_URL, new String[] { "other" }, SchemaNamespaceHelper.ERROR_SCHEMA_PATH)
+            .setStatusCode(400).setHeaderParams(userCreds).build();
+
+    @Inject
+    ObjectMapper mapper;
+
+    /*
+     * GET CURRENT USER
+     */
+    @Test
+    void testGet_currentUser_success() {
+        RestAssuredTemplates.testGet(GET_CURRENT_SUCCESS);
+    }
+
+    @Test
+    void testGet_currentUser_success_validateResponseFormat() {
+        RestAssuredTemplates.testGet_validateResponseFormat(GET_CURRENT_SUCCESS);
+    }
+
+    @Test
+    void testGet_currentUser_success_validateSchema() {
+        RestAssuredTemplates.testGet_validateResponseFormat(GET_CURRENT_SUCCESS);
+    }
+
+    @Test
+    void testGet_currentUser_failure_notFound() {
+        RestAssuredTemplates.testGet(GET_CURRENT_NOT_FOUND);
+    }
+
+    @Test
+    void testGet_currentUser_failure_notFound_validateResponseFormat() {
+        RestAssuredTemplates.testGet_validateResponseFormat(GET_CURRENT_NOT_FOUND);
+    }
+
+    @Test
+    void testGet_currentUser_failure_notFound_validateSchema() {
+        RestAssuredTemplates.testGet_validateResponseFormat(GET_CURRENT_NOT_FOUND);
+    }
+
+    @Test
+    void testGet_currentUser_failure_badCreds() {
+        RestAssuredTemplates.testGet(BAD_CREDS);
+    }
+
+    /*
+     * POST CURRENT USER
+     */
+    @Test
+    void testPost_currentUser_success() {
+        RestAssuredTemplates.testPost(GET_CURRENT_SUCCESS, generateSigningSample("fakeuser"));
+    }
+
+    @Test
+    void testPost_currentUser_success_validateResponseFormat() {
+        RestAssuredTemplates.testPost_validateResponseFormat(GET_CURRENT_SUCCESS, generateSigningSample("fakeuser"));
+    }
+
+    @Test
+    void testPost_currentUser_success_validateSchema() {
+        RestAssuredTemplates.testPost_validateResponseFormat(GET_CURRENT_SUCCESS, generateSigningSample("fakeuser"));
+    }
+
+    @Test
+    void testPost_currentUser_failure_invalidRequestFormat() {
+        RestAssuredTemplates.testPost(
+                TestCaseHelper.prepareTestCase(BASE_URL, new String[] {}, null).setHeaderParams(userCreds)
+                        .setRequestContentType(ContentType.TEXT).setStatusCode(500).build(),
+                generateSigningSample("fakeuser"));
+    }
+
+    @Test
+    void testPost_currentUser_failure_invalidHandle() {
+        RestAssuredTemplates.testPost(POST_CURRENT_INVALID_HANDLE, generateSigningSample("otheruser"));
+    }
+
+    @Test
+    void testPost_currentUser_failure_invalidHandle_validateFormat() {
+        RestAssuredTemplates.testPost_validateResponseFormat(POST_CURRENT_INVALID_HANDLE,
+                generateSigningSample("otheruser"));
+    }
+
+    @Test
+    void testPost_currentUser_failure_badCreds() {
+        RestAssuredTemplates.testPost(BAD_CREDS, generateSigningSample("fakeuser"));
+    }
+
+    /*
+     * GET FOR USER
+     */
+    @Test
+    void testGet_getForUser_success() {
+        RestAssuredTemplates.testGet(GET_USER_SUCCESS);
+    }
+
+    @Test
+    void testGet_geFortUser_success_validateResponseFormat() {
+        RestAssuredTemplates.testGet_validateResponseFormat(GET_USER_SUCCESS);
+    }
+
+    @Test
+    void testGet_getForUser_success_validateSchema() {
+        RestAssuredTemplates.testGet_validateResponseFormat(GET_USER_SUCCESS);
+    }
+
+    @Test
+    void testGet_getForUser_failure_notFound() {
+        RestAssuredTemplates.testGet(GET_USER_NOT_FOUND);
+    }
+
+    @Test
+    void testGet_getForUser_failure_notFound_validateResponseFormat() {
+        RestAssuredTemplates.testGet_validateResponseFormat(GET_USER_NOT_FOUND);
+    }
+
+    @Test
+    void testGet_getForUser_failure_notFound_validateSchema() {
+        RestAssuredTemplates.testGet_validateResponseFormat(GET_USER_NOT_FOUND);
+    }
+
+    @Test
+    void testGet_getForUser_failure_badCreds() {
+        RestAssuredTemplates.testGet(FOR_USER_BAD_CREDS);
+    }
+
+    /*
+     * DELETE FOR USER
+     */
+    @Test
+    void testDelete_deleteForUser_success() {
+        RestAssuredTemplates.testDelete(GET_USER_SUCCESS, null);
+    }
+
+    @Test
+    void testDelete_deleteForUser_failure_invalidUser() {
+        RestAssuredTemplates.testDelete(DELETE_FOR_USER_INVALID_USER, null);
+    }
+
+    @Test
+    void testDelete_deleteForUser_failure_invalidUser_validateResponseFormat() {
+        RestAssuredTemplates.testDelete_validateResponseFormat(DELETE_FOR_USER_INVALID_USER, null);
+    }
+
+    @Test
+    void testDelete_deleteForUser_failure_invalidUser_validateSchema() {
+        RestAssuredTemplates.testDelete_validateSchema(DELETE_FOR_USER_INVALID_USER, null);
+    }
+
+    @Test
+    void testDelete_deleteForUser_failure_badCreds() {
+        RestAssuredTemplates.testGet(FOR_USER_BAD_CREDS);
+    }
+
+    private String generateSigningSample(String ghHandle) {
+        try {
+            return mapper.writeValueAsString(
+                    AgreementSigningRequest.builder().setVersion("1").setGithubHandle(ghHandle).build());
+        } catch (JsonProcessingException e) {
+            throw new RuntimeException(e);
+        }
+    }
+}
diff --git a/src/test/java/org/eclipsefoundation/openvsx/services/DrupalOAuthServiceTest.java b/src/test/java/org/eclipsefoundation/openvsx/services/DrupalOAuthServiceTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..b5fd0ab457dd8fb452953e76f697f97a9b53ed60
--- /dev/null
+++ b/src/test/java/org/eclipsefoundation/openvsx/services/DrupalOAuthServiceTest.java
@@ -0,0 +1,112 @@
+/*********************************************************************
+* Copyright (c) 2023 Eclipse Foundation.
+*
+* This program and the accompanying materials are made
+* available under the terms of the Eclipse Public License 2.0
+* which is available at https://www.eclipse.org/legal/epl-2.0/
+*
+* Author: Zachary Sabourin <zachary.sabourin@eclipse-foundation.org>
+*
+* SPDX-License-Identifier: EPL-2.0
+**********************************************************************/
+package org.eclipsefoundation.openvsx.services;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.inject.Inject;
+
+import org.eclipsefoundation.core.exception.FinalForbiddenException;
+import org.eclipsefoundation.openvsx.api.models.DrupalOAuthData;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import io.quarkus.test.junit.QuarkusTest;
+
+@QuarkusTest
+class DrupalOAuthServiceTest {
+
+    @Inject
+    DrupalOAuthService oauthService;
+
+    @Test
+    void testToken_success() {
+        List<String> validScopes = new ArrayList<>();
+        validScopes.add("read");
+        validScopes.add("write");
+        validScopes.add("admin");
+
+        List<String> validIds = new ArrayList<>();
+        validIds.add("client-id");
+        validIds.add("test-id");
+        validIds.add("valid-id");
+
+        DrupalOAuthData tokenData = oauthService.validateTokenStatus("token2", validScopes, validIds);
+        Assertions.assertEquals("token2", tokenData.getAccessToken());
+        Assertions.assertEquals("test-id", tokenData.getClientId());
+
+        validScopes = new ArrayList<>();
+        validScopes.add("read");
+        validScopes.add("write");
+
+        tokenData = oauthService.validateTokenStatus("token4", validScopes, validIds);
+        Assertions.assertEquals("token4", tokenData.getAccessToken());
+        Assertions.assertEquals("client-id", tokenData.getClientId());
+    }
+
+    @Test
+    void testToken_failure_expired() {
+        List<String> validScopes = new ArrayList<>();
+        validScopes.add("read");
+        validScopes.add("write");
+
+        List<String> validIds = new ArrayList<>();
+        validIds.add("client-id");
+        validIds.add("test-id");
+        validIds.add("valid-id");
+
+        Assertions.assertThrows(FinalForbiddenException.class,
+                () -> oauthService.validateTokenStatus("token3", validScopes, validIds));
+
+        Assertions.assertThrows(FinalForbiddenException.class,
+                () -> oauthService.validateTokenStatus("token1", validScopes, validIds));
+    }
+
+    @Test
+    void testToken_failure_invalidScope() {
+        List<String> validScopes = new ArrayList<>();
+        validScopes.add("admin");
+
+        List<String> validIds = new ArrayList<>();
+        validIds.add("client-id");
+        validIds.add("test-id");
+        validIds.add("valid-id");
+
+        Assertions.assertThrows(FinalForbiddenException.class,
+                () -> oauthService.validateTokenStatus("token1", validScopes, validIds));
+
+        Assertions.assertThrows(FinalForbiddenException.class,
+                () -> oauthService.validateTokenStatus("token4", validScopes, validIds));
+    }
+
+    @Test
+    void testToken_failure_invalidClientId() {
+        List<String> scopes1 = new ArrayList<>();
+        scopes1.add("read");
+        scopes1.add("write");
+        scopes1.add("admin");
+
+        List<String> validIds = new ArrayList<>();
+        validIds.add("valid-id");
+
+        Assertions.assertThrows(FinalForbiddenException.class,
+                () -> oauthService.validateTokenStatus("token2", scopes1, validIds));
+
+        List<String> scopes2 = new ArrayList<>();
+        scopes2.add("read");
+        scopes2.add("write");
+
+        Assertions.assertThrows(FinalForbiddenException.class,
+                () -> oauthService.validateTokenStatus("token4", scopes2, validIds));
+    }
+}
\ No newline at end of file
diff --git a/src/test/java/org/eclipsefoundation/openvsx/test/api/MockDrupalOAuthAPI.java b/src/test/java/org/eclipsefoundation/openvsx/test/api/MockDrupalOAuthAPI.java
new file mode 100644
index 0000000000000000000000000000000000000000..deb49a3994ea012506327aa944f5269d174f9bfa
--- /dev/null
+++ b/src/test/java/org/eclipsefoundation/openvsx/test/api/MockDrupalOAuthAPI.java
@@ -0,0 +1,75 @@
+/*********************************************************************
+* Copyright (c) 2023 Eclipse Foundation.
+*
+* This program and the accompanying materials are made
+* available under the terms of the Eclipse Public License 2.0
+* which is available at https://www.eclipse.org/legal/epl-2.0/
+*
+* Author: Zachary Sabourin <zachary.sabourin@eclipse-foundation.org>
+*
+* SPDX-License-Identifier: EPL-2.0
+**********************************************************************/
+package org.eclipsefoundation.openvsx.test.api;
+
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import javax.enterprise.context.ApplicationScoped;
+
+import org.eclipse.microprofile.rest.client.inject.RestClient;
+import org.eclipsefoundation.openvsx.api.DrupalOAuthAPI;
+import org.eclipsefoundation.openvsx.api.models.DrupalOAuthData;
+
+import io.quarkus.test.Mock;
+
+@Mock
+@RestClient
+@ApplicationScoped
+public class MockDrupalOAuthAPI implements DrupalOAuthAPI {
+
+    List<DrupalOAuthData> internal;
+
+    public MockDrupalOAuthAPI() {
+        internal = new ArrayList<>();
+        internal.addAll(Arrays.asList(
+                DrupalOAuthData.builder()
+                        .setAccessToken("token1")
+                        .setClientId("client-id")
+                        .setExpires(1674111182)
+                        .setScope("read write")
+                        .build(),
+                DrupalOAuthData.builder()
+                        .setAccessToken("token2")
+                        .setClientId("test-id")
+                        .setUserId("fakeuser")
+                        .setExpires(Instant.now().getEpochSecond() + 20000)
+                        .setScope("read write admin")
+                        .build(),
+                DrupalOAuthData.builder()
+                        .setAccessToken("token3")
+                        .setClientId("test-id")
+                        .setExpires(1234567890)
+                        .setScope("read admin")
+                        .build(),
+                DrupalOAuthData.builder()
+                        .setAccessToken("token4")
+                        .setClientId("client-id")
+                        .setExpires(Instant.now().getEpochSecond() + 20000)
+                        .setScope("read write")
+                        .build(),
+                DrupalOAuthData.builder()
+                        .setAccessToken("token5")
+                        .setClientId("test-id")
+                        .setUserId("name")
+                        .setExpires(Instant.now().getEpochSecond() + 20000)
+                        .setScope("read write admin")
+                        .build()));
+    }
+
+    @Override
+    public DrupalOAuthData getTokenInfo(String token) {
+        return internal.stream().filter(t -> t.getAccessToken().equalsIgnoreCase(token)).findFirst().orElse(null);
+    }
+}
\ No newline at end of file
diff --git a/src/test/java/org/eclipsefoundation/openvsx/test/api/MockEclipseAPI.java b/src/test/java/org/eclipsefoundation/openvsx/test/api/MockEclipseAPI.java
new file mode 100644
index 0000000000000000000000000000000000000000..9ae448399e542ba61e115fdadabe3cab35e7827f
--- /dev/null
+++ b/src/test/java/org/eclipsefoundation/openvsx/test/api/MockEclipseAPI.java
@@ -0,0 +1,58 @@
+/*********************************************************************
+* Copyright (c) 2023 Eclipse Foundation.
+*
+* This program and the accompanying materials are made
+* available under the terms of the Eclipse Public License 2.0
+* which is available at https://www.eclipse.org/legal/epl-2.0/
+*
+* Author: Zachary Sabourin <zachary.sabourin@eclipse-foundation.org>
+*
+* SPDX-License-Identifier: EPL-2.0
+**********************************************************************/
+package org.eclipsefoundation.openvsx.test.api;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import javax.enterprise.context.ApplicationScoped;
+
+import org.eclipse.microprofile.rest.client.inject.RestClient;
+import org.eclipsefoundation.openvsx.api.EclipseAPI;
+import org.eclipsefoundation.openvsx.api.models.EfUser;
+
+import io.quarkus.test.Mock;
+
+@Mock
+@RestClient
+@ApplicationScoped
+public class MockEclipseAPI implements EclipseAPI {
+
+    private List<EfUser> users;
+
+    public MockEclipseAPI() {
+        this.users = new ArrayList<>();
+        this.users.addAll(Arrays.asList(
+                EfUser.builder()
+                        .setName("firstlast")
+                        .setGithubHandle("handle")
+                        .build(),
+                EfUser.builder()
+                        .setName("fakeuser")
+                        .setGithubHandle("fakeuser")
+                        .build(),
+                EfUser.builder()
+                        .setName("name")
+                        .setGithubHandle("name")
+                        .build(),
+                EfUser.builder()
+                        .setName("testtesterson")
+                        .setGithubHandle("mctesty")
+                        .build()));
+    }
+
+    @Override
+    public EfUser getUserByGithubName(String username) {
+        return users.stream().filter(u -> u.getGithubHandle().equalsIgnoreCase(username)).findFirst().orElse(null);
+    }
+}
diff --git a/src/test/java/org/eclipsefoundation/openvsx/test/api/MockPeopleAPI.java b/src/test/java/org/eclipsefoundation/openvsx/test/api/MockPeopleAPI.java
new file mode 100644
index 0000000000000000000000000000000000000000..a0e24fb445c67f6e4673244c68af3a5f62880f75
--- /dev/null
+++ b/src/test/java/org/eclipsefoundation/openvsx/test/api/MockPeopleAPI.java
@@ -0,0 +1,94 @@
+/*********************************************************************
+* Copyright (c) 2023 Eclipse Foundation.
+*
+* This program and the accompanying materials are made
+* available under the terms of the Eclipse Public License 2.0
+* which is available at https://www.eclipse.org/legal/epl-2.0/
+*
+* Author: Zachary Sabourin <zachary.sabourin@eclipse-foundation.org>
+*
+* SPDX-License-Identifier: EPL-2.0
+**********************************************************************/
+package org.eclipsefoundation.openvsx.test.api;
+
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import javax.enterprise.context.ApplicationScoped;
+import javax.ws.rs.core.Response;
+
+import org.eclipse.microprofile.rest.client.inject.RestClient;
+import org.eclipsefoundation.foundationdb.client.model.PeopleDocumentData;
+import org.eclipsefoundation.openvsx.api.PeopleAPI;
+
+import io.quarkus.test.Mock;
+
+@Mock
+@RestClient
+@ApplicationScoped
+public class MockPeopleAPI implements PeopleAPI {
+
+    private List<PeopleDocumentData> peopleDocs;
+
+    public MockPeopleAPI() {
+        this.peopleDocs = new ArrayList<>();
+        this.peopleDocs.addAll(Arrays.asList(
+                PeopleDocumentData.builder()
+                        .setPersonID("fakeuser")
+                        .setDocumentID("sampleId")
+                        .setVersion(1)
+                        .setEffectiveDate(new Date())
+                        .build(),
+                PeopleDocumentData.builder()
+                        .setPersonID("firstlast")
+                        .setDocumentID("sampleId")
+                        .setVersion(1)
+                        .setEffectiveDate(new Date())
+                        .build(),
+                PeopleDocumentData.builder()
+                        .setPersonID("name")
+                        .setDocumentID("otherId")
+                        .setVersion(1)
+                        .setEffectiveDate(new Date())
+                        .build(),
+                PeopleDocumentData.builder()
+                        .setPersonID("username")
+                        .setDocumentID("sampleId")
+                        .setVersion(1)
+                        .setEffectiveDate(new Date())
+                        .build()));
+    }
+
+    @Override
+    public List<PeopleDocumentData> getPeopleDocuments(String personId, String documentId) {
+        return peopleDocs.stream().filter(
+                d -> d.getPersonID().equalsIgnoreCase(personId) && d.getDocumentID().equalsIgnoreCase(documentId))
+                .collect(Collectors.toList());
+    }
+
+    @Override
+    public List<PeopleDocumentData> createPeopleDocument(String personId, PeopleDocumentData src) {
+        peopleDocs.add(src);
+        return Arrays.asList(src);
+    }
+
+    @Override
+    public Response deletePeopleDocument(String personId, String documentId, String effectiveDate) {
+        PeopleDocumentData result = peopleDocs.stream()
+                .filter(d -> d.getDocumentID().equalsIgnoreCase(documentId)
+                        && d.getPersonID().equalsIgnoreCase(personId)
+                        && d.getEffectiveDate() == Date.from(Instant.parse(effectiveDate)))
+                .findFirst().orElse(null);
+
+        if (result == null) {
+            return Response.status(404).build();
+        }
+
+        peopleDocs.remove(result);
+        return Response.status(204).build();
+    }
+}
diff --git a/src/test/java/org/eclipsefoundation/openvsx/test/helpers/SchemaNamespaceHelper.java b/src/test/java/org/eclipsefoundation/openvsx/test/helpers/SchemaNamespaceHelper.java
new file mode 100644
index 0000000000000000000000000000000000000000..53e6d9b44c33631bfcf8dab86a55a9b7ccc4dc5d
--- /dev/null
+++ b/src/test/java/org/eclipsefoundation/openvsx/test/helpers/SchemaNamespaceHelper.java
@@ -0,0 +1,23 @@
+/*********************************************************************
+* Copyright (c) 2023 Eclipse Foundation.
+*
+* This program and the accompanying materials are made
+* available under the terms of the Eclipse Public License 2.0
+* which is available at https://www.eclipse.org/legal/epl-2.0/
+*
+* Author: Zachary Sabourin <zachary.sabourin@eclipse-foundation.org>
+*
+* SPDX-License-Identifier: EPL-2.0
+**********************************************************************/
+package org.eclipsefoundation.openvsx.test.helpers;
+
+public class SchemaNamespaceHelper {
+    public static final String BASE_SCHEMA_PATH = "schemas/";
+    public static final String BASE_SCHEMA_PATH_SUFFIX = "-schema.json";
+
+    public static final String SIGNING_REQUEST_SCHEMA_PATH = BASE_SCHEMA_PATH + "agreement-signing-request"
+            + BASE_SCHEMA_PATH_SUFFIX;
+    public static final String PUBLISHER_AGREEMENT_SCHEMA_PATH = BASE_SCHEMA_PATH + "publisher-agreement"
+            + BASE_SCHEMA_PATH_SUFFIX;
+    public static final String ERROR_SCHEMA_PATH = BASE_SCHEMA_PATH + "error" + BASE_SCHEMA_PATH_SUFFIX;
+}
diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties
index 8ca4d923a957ca9a5536a671cca483f16fcc2e63..f2c254ba6d8531653609487f74aba13069c390f0 100644
--- a/src/test/resources/application.properties
+++ b/src/test/resources/application.properties
@@ -2,4 +2,10 @@
 quarkus.oauth2.enabled=false
 quarkus.oidc.enabled=false
 quarkus.keycloak.devservices.enabled=false
-quarkus.oidc-client.enabled=false
\ No newline at end of file
+quarkus.oidc-client.enabled=false
+
+eclipse.openvsx.oauth-filter.enabled=true
+
+eclipse.openvsx.clients=test-id
+eclipse.openvsx.scopes=read,write,admin
+eclipse.openvsx.doc-id=sampleId
\ No newline at end of file