Merge branch 'stable-3.4'

* stable-3.4:
  Update documentation with the configuration of the bearer token
  Add bearer token to the git fetch.
  Associate GitRepositoryManager indirectly through a provider
  Introduce pull-replication user and internal group

Change-Id: I433a8a97eb1d2f06a591135d90af6cfb987375b7
diff --git a/BUILD b/BUILD
index 82112d1..5c5f240 100644
--- a/BUILD
+++ b/BUILD
@@ -46,6 +46,6 @@
     ),
     deps = PLUGIN_TEST_DEPS + PLUGIN_DEPS + [
         ":pull-replication__plugin",
-         "//plugins/replication:replication",
+        "//plugins/replication:replication",
     ],
 )
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/LocalGitRepositoryManagerProvider.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/LocalGitRepositoryManagerProvider.java
new file mode 100644
index 0000000..ea8aa00
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/LocalGitRepositoryManagerProvider.java
@@ -0,0 +1,47 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.replication.pull;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.MoreObjects;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.LocalDiskRepositoryManager;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import com.google.inject.name.Named;
+
+@Singleton
+public class LocalGitRepositoryManagerProvider implements Provider<GitRepositoryManager> {
+
+  @Inject(optional = true)
+  @Named("LocalDiskRepositoryManager")
+  @Nullable
+  private GitRepositoryManager gitRepositoryManager;
+
+  private final LocalDiskRepositoryManager localDiskRepositoryManager;
+
+  @VisibleForTesting
+  @Inject
+  public LocalGitRepositoryManagerProvider(LocalDiskRepositoryManager localDiskRepositoryManager) {
+    this.localDiskRepositoryManager = localDiskRepositoryManager;
+  }
+
+  @Override
+  public GitRepositoryManager get() {
+    return MoreObjects.firstNonNull(gitRepositoryManager, localDiskRepositoryManager);
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationModule.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationModule.java
index 93bbde0..1671410 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationModule.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationModule.java
@@ -44,6 +44,7 @@
 import com.googlesource.gerrit.plugins.replication.StartReplicationCapability;
 import com.googlesource.gerrit.plugins.replication.pull.api.FetchJob;
 import com.googlesource.gerrit.plugins.replication.pull.api.PullReplicationApiModule;
+import com.googlesource.gerrit.plugins.replication.pull.auth.PullReplicationGroupModule;
 import com.googlesource.gerrit.plugins.replication.pull.client.FetchApiClient;
 import com.googlesource.gerrit.plugins.replication.pull.client.FetchRestApiClient;
 import com.googlesource.gerrit.plugins.replication.pull.client.HttpClient;
@@ -73,6 +74,7 @@
   @Override
   protected void configure() {
 
+    install(new PullReplicationGroupModule());
     bind(BearerTokenProvider.class).in(Scopes.SINGLETON);
     bind(RevisionReader.class).in(Scopes.SINGLETON);
     bind(ApplyObject.class);
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/BearerAuthenticationFilter.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/BearerAuthenticationFilter.java
index cbe1ab8..8147149 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/BearerAuthenticationFilter.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/BearerAuthenticationFilter.java
@@ -19,12 +19,12 @@
 import com.google.gerrit.httpd.AllRequestFilter;
 import com.google.gerrit.httpd.WebSession;
 import com.google.gerrit.server.AccessPath;
-import com.google.gerrit.server.PluginUser;
 import com.google.gerrit.server.util.ManualRequestContext;
 import com.google.gerrit.server.util.ThreadLocalRequestContext;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.name.Named;
+import com.googlesource.gerrit.plugins.replication.pull.auth.PullReplicationInternalUser;
 import java.io.IOException;
 import java.util.Optional;
 import java.util.regex.Matcher;
@@ -46,7 +46,7 @@
   private static final String BEARER_TOKEN = "BearerToken";
   private final DynamicItem<WebSession> session;
   private final String pluginName;
-  private final Provider<PluginUser> pluginUserProvider;
+  private final PullReplicationInternalUser pluginUser;
   private final Provider<ThreadLocalRequestContext> threadLocalRequestContext;
   private final String bearerToken;
   private final Pattern bearerTokenRegex = Pattern.compile("^Bearer\\s(.+)$");
@@ -55,12 +55,12 @@
   BearerAuthenticationFilter(
       DynamicItem<WebSession> session,
       @PluginName String pluginName,
-      Provider<PluginUser> pluginUserProvider,
+      PullReplicationInternalUser pluginUser,
       Provider<ThreadLocalRequestContext> threadLocalRequestContext,
       @Named(BEARER_TOKEN) String bearerToken) {
     this.session = session;
     this.pluginName = pluginName;
-    this.pluginUserProvider = pluginUserProvider;
+    this.pluginUser = pluginUser;
     this.threadLocalRequestContext = threadLocalRequestContext;
     this.bearerToken = bearerToken;
   }
@@ -82,13 +82,13 @@
 
     if (isBasicAuthenticationRequest(requestURI)) {
       filterChain.doFilter(servletRequest, servletResponse);
-    } else if (isPullReplicationApiRequest(requestURI)) {
+    } else if (isPullReplicationApiRequest(requestURI) || isGitUploadPackRequest(httpRequest)) {
       Optional<String> authorizationHeader =
           Optional.ofNullable(httpRequest.getHeader("Authorization"));
 
       if (isBearerTokenAuthenticated(authorizationHeader, bearerToken))
         try (ManualRequestContext ctx =
-            new ManualRequestContext(pluginUserProvider.get(), threadLocalRequestContext.get())) {
+            new ManualRequestContext(pluginUser, threadLocalRequestContext.get())) {
           WebSession ws = session.get();
           ws.setAccessPathOk(AccessPath.REST_API, true);
           filterChain.doFilter(servletRequest, servletResponse);
@@ -100,6 +100,13 @@
     }
   }
 
+  private boolean isGitUploadPackRequest(HttpServletRequest requestURI) {
+    return requestURI.getRequestURI().contains("git-upload-pack")
+        || Optional.ofNullable(requestURI.getQueryString())
+            .map(q -> q.contains("git-upload-pack"))
+            .orElse(false);
+  }
+
   private boolean isBearerTokenAuthenticated(
       Optional<String> authorizationHeader, String bearerToken) {
     return authorizationHeader
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/DeleteRefCommand.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/DeleteRefCommand.java
index 14f2545..d86287a 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/DeleteRefCommand.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/DeleteRefCommand.java
@@ -22,7 +22,7 @@
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.server.events.EventDispatcher;
-import com.google.gerrit.server.git.LocalDiskRepositoryManager;
+import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.permissions.RefPermission;
@@ -31,6 +31,7 @@
 import com.google.inject.Inject;
 import com.googlesource.gerrit.plugins.replication.pull.Context;
 import com.googlesource.gerrit.plugins.replication.pull.FetchRefReplicatedEvent;
+import com.googlesource.gerrit.plugins.replication.pull.LocalGitRepositoryManagerProvider;
 import com.googlesource.gerrit.plugins.replication.pull.PullReplicationStateLogger;
 import com.googlesource.gerrit.plugins.replication.pull.ReplicationState;
 import com.googlesource.gerrit.plugins.replication.pull.Source;
@@ -53,24 +54,24 @@
   private final ProjectCache projectCache;
   private final SourcesCollection sourcesCollection;
   private final PermissionBackend permissionBackend;
-  private final LocalDiskRepositoryManager gitManager;
+  private final GitRepositoryManager gitManager;
 
   @Inject
   public DeleteRefCommand(
       PullReplicationStateLogger fetchStateLog,
       ProjectCache projectCache,
-      DynamicItem<EventDispatcher> eventDispatcher,
       SourcesCollection sourcesCollection,
       ApplyObject applyObject,
       PermissionBackend permissionBackend,
-      LocalDiskRepositoryManager gitManager) {
+      DynamicItem<EventDispatcher> eventDispatcher,
+      LocalGitRepositoryManagerProvider gitManagerProvider) {
     this.fetchStateLog = fetchStateLog;
     this.projectCache = projectCache;
     this.applyObject = applyObject;
     this.eventDispatcher = eventDispatcher;
     this.sourcesCollection = sourcesCollection;
     this.permissionBackend = permissionBackend;
-    this.gitManager = gitManager;
+    this.gitManager = gitManagerProvider.get();
   }
 
   public void deleteRef(Project.NameKey name, String refName, String sourceLabel)
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/auth/PullReplicationGroupBackend.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/auth/PullReplicationGroupBackend.java
new file mode 100644
index 0000000..9521004
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/auth/PullReplicationGroupBackend.java
@@ -0,0 +1,98 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.replication.pull.auth;
+
+import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.GroupDescription;
+import com.google.gerrit.entities.GroupReference;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.account.AbstractGroupBackend;
+import com.google.gerrit.server.account.GroupMembership;
+import com.google.gerrit.server.account.ListGroupMembership;
+import com.google.gerrit.server.group.SystemGroupBackend;
+import com.google.gerrit.server.project.ProjectState;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.util.Arrays;
+import java.util.Collection;
+
+/** Backend to expose the pull-replication internal user group membership. */
+@Singleton
+class PullReplicationGroupBackend extends AbstractGroupBackend {
+  public static final AccountGroup.UUID INTERNAL_GROUP_UUID =
+      AccountGroup.uuid("pullreplication:internal-user");
+  public static final String INTERNAL_GROUP_NAME = "Pull-replication Internal User";
+  public static final String NAME_PREFIX = "pullreplication/";
+  public static final GroupDescription.Basic INTERNAL_GROUP_DESCRIPTION =
+      new GroupDescription.Basic() {
+
+        @Override
+        public String getUrl() {
+          return null;
+        }
+
+        @Override
+        public String getName() {
+          return INTERNAL_GROUP_NAME;
+        }
+
+        @Override
+        public AccountGroup.UUID getGroupUUID() {
+          return INTERNAL_GROUP_UUID;
+        }
+
+        @Override
+        public String getEmailAddress() {
+          return null;
+        }
+      };
+  private final PullReplicationInternalUser internalUser;
+
+  static final ListGroupMembership INTERNAL_GROUP_MEMBERSHIP =
+      new ListGroupMembership(
+          Arrays.asList(INTERNAL_GROUP_UUID, SystemGroupBackend.ANONYMOUS_USERS));
+
+  @Inject
+  public PullReplicationGroupBackend(PullReplicationInternalUser internalUser) {
+    this.internalUser = internalUser;
+  }
+
+  @Override
+  public boolean handles(AccountGroup.UUID uuid) {
+    return INTERNAL_GROUP_UUID.equals(uuid);
+  }
+
+  @Override
+  public GroupDescription.Basic get(AccountGroup.UUID uuid) {
+    return handles(uuid) ? INTERNAL_GROUP_DESCRIPTION : null;
+  }
+
+  @Override
+  public Collection<GroupReference> suggest(String name, ProjectState project) {
+    return Arrays.asList(
+        NAME_PREFIX.contains(name.toLowerCase())
+            ? GroupReference.create(INTERNAL_GROUP_UUID, INTERNAL_GROUP_NAME)
+            : GroupReference.create(name));
+  }
+
+  @Override
+  public GroupMembership membershipsOf(CurrentUser user) {
+    if (user.equals(internalUser)) {
+      return INTERNAL_GROUP_MEMBERSHIP;
+    }
+
+    return ListGroupMembership.EMPTY;
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/auth/PullReplicationGroupModule.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/auth/PullReplicationGroupModule.java
new file mode 100644
index 0000000..583ba8e
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/auth/PullReplicationGroupModule.java
@@ -0,0 +1,26 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.replication.pull.auth;
+
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.server.account.GroupBackend;
+import com.google.inject.AbstractModule;
+
+public class PullReplicationGroupModule extends AbstractModule {
+  @Override
+  protected void configure() {
+    DynamicSet.bind(binder(), GroupBackend.class).to(PullReplicationGroupBackend.class);
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/auth/PullReplicationInternalUser.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/auth/PullReplicationInternalUser.java
new file mode 100644
index 0000000..e8b7666
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/auth/PullReplicationInternalUser.java
@@ -0,0 +1,35 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.replication.pull.auth;
+
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.server.PluginUser;
+import com.google.gerrit.server.account.GroupMembership;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+@Singleton
+public class PullReplicationInternalUser extends PluginUser {
+
+  @Inject
+  protected PullReplicationInternalUser(@PluginName String pluginName) {
+    super(pluginName);
+  }
+
+  @Override
+  public GroupMembership getEffectiveGroups() {
+    return PullReplicationGroupBackend.INTERNAL_GROUP_MEMBERSHIP;
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/fetch/ApplyObject.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/fetch/ApplyObject.java
index 2bb1caf..36356e9 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/fetch/ApplyObject.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/fetch/ApplyObject.java
@@ -15,8 +15,9 @@
 package com.googlesource.gerrit.plugins.replication.pull.fetch;
 
 import com.google.gerrit.entities.Project;
-import com.google.gerrit.server.git.LocalDiskRepositoryManager;
+import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.inject.Inject;
+import com.googlesource.gerrit.plugins.replication.pull.LocalGitRepositoryManagerProvider;
 import com.googlesource.gerrit.plugins.replication.pull.api.data.RevisionData;
 import com.googlesource.gerrit.plugins.replication.pull.api.data.RevisionObjectData;
 import com.googlesource.gerrit.plugins.replication.pull.api.exception.MissingParentObjectException;
@@ -30,15 +31,15 @@
 
 public class ApplyObject {
 
-  private final LocalDiskRepositoryManager gitManager;
+  private final GitRepositoryManager gitManager;
 
-  // NOTE: We do need specifically the LocalDiskRepositoryManager to make sure
+  // NOTE: We do need specifically the local GitRepositoryManager to make sure
   // to be able to write onto the directly physical repository without any wrapper.
   // Using for instance the multi-site wrapper injected by Guice would result
   // in a split-brain because of the misalignment of local vs. global refs values.
   @Inject
-  public ApplyObject(LocalDiskRepositoryManager gitManager) {
-    this.gitManager = gitManager;
+  public ApplyObject(LocalGitRepositoryManagerProvider gitManagerProvider) {
+    this.gitManager = gitManagerProvider.get();
   }
 
   public RefUpdateState apply(Project.NameKey name, RefSpec refSpec, RevisionData[] revisionsData)
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/fetch/JGitFetch.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/fetch/JGitFetch.java
index 5de82f1..89972cf 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/fetch/JGitFetch.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/fetch/JGitFetch.java
@@ -18,34 +18,22 @@
 
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
-import com.googlesource.gerrit.plugins.replication.CredentialsFactory;
-import com.googlesource.gerrit.plugins.replication.pull.SourceConfiguration;
 import java.io.IOException;
 import java.util.List;
 import java.util.stream.Collectors;
 import org.eclipse.jgit.lib.NullProgressMonitor;
 import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.transport.CredentialsProvider;
-import org.eclipse.jgit.transport.FetchResult;
-import org.eclipse.jgit.transport.RefSpec;
-import org.eclipse.jgit.transport.RemoteConfig;
-import org.eclipse.jgit.transport.Transport;
-import org.eclipse.jgit.transport.URIish;
+import org.eclipse.jgit.transport.*;
 
 public class JGitFetch implements Fetch {
-  private final RemoteConfig config;
-  private final CredentialsProvider credentialsProvider;
   URIish uri;
   Repository git;
+  private final TransportProvider transportProvider;
 
   @Inject
   public JGitFetch(
-      SourceConfiguration sourceConfig,
-      CredentialsFactory cpFactory,
-      @Assisted URIish uri,
-      @Assisted Repository git) {
-    this.config = sourceConfig.getRemoteConfig();
-    this.credentialsProvider = cpFactory.create(config.getName());
+      TransportProvider transportProvider, @Assisted URIish uri, @Assisted Repository git) {
+    this.transportProvider = transportProvider;
     this.uri = uri;
     this.git = git;
   }
@@ -53,7 +41,7 @@
   @Override
   public List<RefUpdateState> fetch(List<RefSpec> refs) throws IOException {
     FetchResult res;
-    try (Transport tn = Transport.open(git, uri)) {
+    try (Transport tn = transportProvider.open(git, uri)) {
       res = fetchVia(tn, refs);
     }
     return res.getTrackingRefUpdates().stream()
@@ -62,9 +50,6 @@
   }
 
   private FetchResult fetchVia(Transport tn, List<RefSpec> fetchRefSpecs) throws IOException {
-    tn.applyConfig(config);
-    tn.setCredentialsProvider(credentialsProvider);
-
     repLog.info("Fetch references {} from {}", fetchRefSpecs, uri);
     return tn.fetch(NullProgressMonitor.INSTANCE, fetchRefSpecs);
   }
diff --git a/src/main/java/org/eclipse/jgit/transport/TransportHttpWithBearerToken.java b/src/main/java/org/eclipse/jgit/transport/TransportHttpWithBearerToken.java
new file mode 100644
index 0000000..68ff6f8
--- /dev/null
+++ b/src/main/java/org/eclipse/jgit/transport/TransportHttpWithBearerToken.java
@@ -0,0 +1,74 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package org.eclipse.jgit.transport;
+
+import static org.eclipse.jgit.util.HttpSupport.HDR_AUTHORIZATION;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableSet;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.URL;
+import java.util.Collection;
+import java.util.Set;
+import org.eclipse.jgit.errors.NotSupportedException;
+import org.eclipse.jgit.lib.ProgressMonitor;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.transport.http.HttpConnection;
+
+/**
+ * This is a hack in order to allow git over http/https with Bearer Token Authentication.
+ *
+ * <p>Currently {@link org.eclipse.jgit.transport.TransportHttp} does NOT provide Bearer Token
+ * Authentication and unfortunately it is not possible to extend the functionality because some
+ * classes, methods and instance variables are private or protected. This package in the pull
+ * replication plugin provides the visibility needed and allows to extend the functionality.
+ *
+ * <p>It is important to mention that in the case of git push operation, this class cannot be used
+ * because the push operation needs to initialise a push hook: {@link
+ * org.eclipse.jgit.transport.Transport#push(ProgressMonitor, Collection, OutputStream)} and it is
+ * defined as a private in the code.
+ */
+public class TransportHttpWithBearerToken extends TransportHttp {
+
+  private static final String SCHEME_HTTP = "http";
+  private static final String SCHEME_HTTPS = "https";
+  private static final Set<String> SCHEMES_ALLOWED = ImmutableSet.of(SCHEME_HTTP, SCHEME_HTTPS);
+  private final String bearerToken;
+
+  public TransportHttpWithBearerToken(Repository local, URIish uri, String bearerToken)
+      throws NotSupportedException {
+    super(local, uri);
+    this.bearerToken = bearerToken;
+  }
+
+  protected HttpConnection httpOpen(String method, URL u, AcceptEncoding acceptEncoding)
+      throws IOException {
+    HttpConnection conn = super.httpOpen(method, u, acceptEncoding);
+    conn.setRequestProperty(HDR_AUTHORIZATION, "Bearer " + bearerToken); // $NON-NLS-1$
+    return conn;
+  }
+
+  /**
+   * This method copies the behaviour of {@link
+   * org.eclipse.jgit.transport.TransportProtocol#canHandle(URIish, Repository, String)} in the case
+   * of {@link org.eclipse.jgit.transport.TransportHttp} where scheme, host and path are compulsory.
+   */
+  public static boolean canHandle(URIish uri) {
+    return SCHEMES_ALLOWED.contains(uri.getScheme())
+        && !Strings.isNullOrEmpty(uri.getHost())
+        && !Strings.isNullOrEmpty(uri.getPath());
+  }
+}
diff --git a/src/main/java/org/eclipse/jgit/transport/TransportProvider.java b/src/main/java/org/eclipse/jgit/transport/TransportProvider.java
new file mode 100644
index 0000000..ca15b6c
--- /dev/null
+++ b/src/main/java/org/eclipse/jgit/transport/TransportProvider.java
@@ -0,0 +1,68 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package org.eclipse.jgit.transport;
+
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import com.googlesource.gerrit.plugins.replication.CredentialsFactory;
+import com.googlesource.gerrit.plugins.replication.pull.BearerTokenProvider;
+import com.googlesource.gerrit.plugins.replication.pull.SourceConfiguration;
+import java.util.Optional;
+import org.eclipse.jgit.errors.NotSupportedException;
+import org.eclipse.jgit.errors.TransportException;
+import org.eclipse.jgit.lib.Repository;
+
+/**
+ * This class is responsible for providing a Custom Git HTTP Transport with Bearer Token
+ * Authentication or a concrete implementation of {@link org.eclipse.jgit.transport.Transport}.
+ */
+@Singleton
+public class TransportProvider {
+  private final RemoteConfig remoteConfig;
+  private final CredentialsProvider credentialsProvider;
+  private final Optional<String> bearerToken;
+
+  @Inject
+  public TransportProvider(
+      SourceConfiguration sourceConfig,
+      CredentialsFactory cpFactory,
+      BearerTokenProvider bearerTokenProvider) {
+    this.remoteConfig = sourceConfig.getRemoteConfig();
+    this.credentialsProvider = cpFactory.create(remoteConfig.getName());
+    this.bearerToken = bearerTokenProvider.get();
+  }
+
+  public Transport open(Repository local, URIish uri)
+      throws NotSupportedException, TransportException {
+    return (bearerToken.isPresent() && TransportHttpWithBearerToken.canHandle(uri))
+        ? provideTransportHttpWithBearerToken(local, uri)
+        : provideNativeTransport(local, uri);
+  }
+
+  private Transport provideTransportHttpWithBearerToken(Repository local, URIish uri)
+      throws NotSupportedException {
+    Transport tn = new TransportHttpWithBearerToken(local, uri, bearerToken.get());
+    tn.applyConfig(remoteConfig);
+    return tn;
+  }
+
+  private Transport provideNativeTransport(Repository local, URIish uri)
+      throws NotSupportedException, TransportException {
+    Transport tn = Transport.open(local, uri);
+    tn.applyConfig(remoteConfig);
+    tn.setCredentialsProvider(credentialsProvider);
+    return tn;
+  }
+}
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index f29e572..d3a2859 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -518,8 +518,15 @@
 
 The optional file `$site_path/secure.config` is a Git-style config
 file that provides secure values that should not be world-readable,
-such as passwords. Passwords for HTTP remotes can be obtained from
-this file.
+such as passwords. The HTTP authentication can be configured in 2 
+different flavours:
+
+*HTTP Bearer Token Authentication*
+
+auth.bearerToken
+:	shared secret configured on all replication endpoints. 
+
+*HTTP Basic Authentication*
 
 remote.NAME.username
 :	Username to use for HTTP authentication on this remote, if not
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationIT.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationIT.java
index 187af3d..f3c62c5 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationIT.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationIT.java
@@ -19,61 +19,37 @@
 import static com.google.gerrit.acceptance.GitUtil.pushOne;
 import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
-import static java.util.stream.Collectors.toList;
 
-import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.acceptance.LightweightPluginDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit.Result;
 import com.google.gerrit.acceptance.SkipProjectClone;
 import com.google.gerrit.acceptance.TestPlugin;
 import com.google.gerrit.acceptance.UseLocalDisk;
 import com.google.gerrit.acceptance.config.GerritConfig;
-import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.entities.Permission;
-import com.google.gerrit.entities.Project;
 import com.google.gerrit.entities.Project.NameKey;
 import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.api.projects.BranchInput;
-import com.google.gerrit.extensions.common.Input;
 import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
 import com.google.gerrit.extensions.events.HeadUpdatedListener;
 import com.google.gerrit.extensions.events.ProjectDeletedListener;
-import com.google.gerrit.extensions.registration.DynamicSet;
-import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.extensions.restapi.BadRequestException;
-import com.google.gerrit.extensions.restapi.ResourceConflictException;
-import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.extensions.restapi.RestModifyView;
-import com.google.gerrit.server.config.SitePaths;
-import com.google.gerrit.server.project.ProjectResource;
-import com.google.inject.Inject;
-import com.google.inject.Singleton;
 import com.googlesource.gerrit.plugins.replication.AutoReloadConfigDecorator;
-import java.io.File;
 import java.io.IOException;
-import java.nio.file.Path;
-import java.time.Duration;
-import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
 import java.util.Optional;
-import java.util.function.Supplier;
-import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.storage.file.FileBasedConfig;
 import org.eclipse.jgit.transport.PushResult;
 import org.eclipse.jgit.transport.ReceiveCommand;
 import org.eclipse.jgit.transport.RemoteRefUpdate;
 import org.eclipse.jgit.transport.RemoteRefUpdate.Status;
-import org.eclipse.jgit.util.FS;
 import org.junit.Test;
 
 @SkipProjectClone
@@ -82,48 +58,29 @@
     name = "pull-replication",
     sysModule = "com.googlesource.gerrit.plugins.replication.pull.PullReplicationModule",
     httpModule = "com.googlesource.gerrit.plugins.replication.pull.api.HttpModule")
-public class PullReplicationIT extends LightweightPluginDaemonTest {
-  private static final Optional<String> ALL_PROJECTS = Optional.empty();
-  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
-  private static final int TEST_REPLICATION_DELAY = 1;
-  private static final Duration TEST_TIMEOUT = Duration.ofSeconds(TEST_REPLICATION_DELAY * 2000);
-  private static final String TEST_REPLICATION_SUFFIX = "suffix1";
-  private static final String TEST_REPLICATION_REMOTE = "remote1";
+public class PullReplicationIT extends PullReplicationSetupBase {
 
-  @Inject private SitePaths sitePaths;
-  @Inject private ProjectOperations projectOperations;
-  @Inject private DynamicSet<ProjectDeletedListener> deletedListeners;
-  private Path gitPath;
-  private FileBasedConfig config;
-  private FileBasedConfig secureConfig;
+  @Override
+  protected void setReplicationSource(
+      String remoteName, List<String> replicaSuffixes, Optional<String> project)
+      throws IOException {
+    List<String> fetchUrls =
+        buildReplicaURLs(replicaSuffixes, s -> gitPath.resolve("${name}" + s + ".git").toString());
+    config.setStringList("remote", remoteName, "url", fetchUrls);
+    config.setString("remote", remoteName, "apiUrl", adminRestSession.url());
+    config.setString("remote", remoteName, "fetch", "+refs/*:refs/*");
+    config.setInt("remote", remoteName, "timeout", 600);
+    config.setInt("remote", remoteName, "replicationDelay", TEST_REPLICATION_DELAY);
+    project.ifPresent(prj -> config.setString("remote", remoteName, "projects", prj));
+    config.setBoolean("gerrit", null, "autoReload", true);
+    config.save();
+  }
 
   @Override
   public void setUpTestPlugin() throws Exception {
     setUpTestPlugin(false);
   }
 
-  protected void setUpTestPlugin(boolean loadExisting) throws Exception {
-    gitPath = sitePaths.site_path.resolve("git");
-
-    File configFile = sitePaths.etc_dir.resolve("replication.config").toFile();
-    config = new FileBasedConfig(configFile, FS.DETECTED);
-    if (loadExisting && configFile.exists()) {
-      config.load();
-    }
-    setReplicationSource(
-        TEST_REPLICATION_REMOTE,
-        TEST_REPLICATION_SUFFIX,
-        ALL_PROJECTS); // Simulates a full replication.config initialization
-    config.save();
-
-    secureConfig =
-        new FileBasedConfig(sitePaths.etc_dir.resolve("secure.config").toFile(), FS.DETECTED);
-    setReplicationCredentials(TEST_REPLICATION_REMOTE, admin.username(), admin.httpPassword());
-    secureConfig.save();
-
-    super.setUpTestPlugin();
-  }
-
   @Test
   @GerritConfig(name = "gerrit.instanceId", value = TEST_REPLICATION_REMOTE)
   public void shouldReplicateNewChangeRef() throws Exception {
@@ -425,80 +382,4 @@
       assertThat(targetBranchRef.getObjectId()).isEqualTo(sourceCommit.getId());
     }
   }
-
-  private Ref getRef(Repository repo, String branchName) throws IOException {
-    return repo.getRefDatabase().exactRef(branchName);
-  }
-
-  private Ref checkedGetRef(Repository repo, String branchName) {
-    try {
-      return repo.getRefDatabase().exactRef(branchName);
-    } catch (Exception e) {
-      logger.atSevere().withCause(e).log("failed to get ref %s in repo %s", branchName, repo);
-      return null;
-    }
-  }
-
-  private void setReplicationSource(
-      String remoteName, String replicaSuffix, Optional<String> project)
-      throws IOException, ConfigInvalidException {
-    setReplicationSource(remoteName, Arrays.asList(replicaSuffix), project);
-  }
-
-  private void setReplicationSource(
-      String remoteName, List<String> replicaSuffixes, Optional<String> project)
-      throws IOException, ConfigInvalidException {
-
-    List<String> replicaUrls =
-        replicaSuffixes.stream()
-            .map(suffix -> gitPath.resolve("${name}" + suffix + ".git").toString())
-            .collect(toList());
-    config.setStringList("remote", remoteName, "url", replicaUrls);
-    config.setString("remote", remoteName, "apiUrl", adminRestSession.url());
-    config.setString("remote", remoteName, "fetch", "+refs/*:refs/*");
-    config.setInt("remote", remoteName, "timeout", 600);
-    config.setInt("remote", remoteName, "replicationDelay", TEST_REPLICATION_DELAY);
-    project.ifPresent(prj -> config.setString("remote", remoteName, "projects", prj));
-    config.setBoolean("gerrit", null, "autoReload", true);
-    config.save();
-  }
-
-  private void setReplicationCredentials(String remoteName, String username, String password)
-      throws IOException {
-    secureConfig.setString("remote", remoteName, "username", username);
-    secureConfig.setString("remote", remoteName, "password", password);
-    secureConfig.save();
-  }
-
-  private void waitUntil(Supplier<Boolean> waitCondition) throws InterruptedException {
-    WaitUtil.waitUntil(waitCondition, TEST_TIMEOUT);
-  }
-
-  private <T> T getInstance(Class<T> classObj) {
-    return plugin.getSysInjector().getInstance(classObj);
-  }
-
-  private Project.NameKey createTestProject(String name) throws Exception {
-    return projectOperations.newProject().name(name).create();
-  }
-
-  @Singleton
-  public static class FakeDeleteProjectPlugin implements RestModifyView<ProjectResource, Input> {
-    private int deleteEndpointCalls;
-
-    FakeDeleteProjectPlugin() {
-      this.deleteEndpointCalls = 0;
-    }
-
-    @Override
-    public Response<?> apply(ProjectResource resource, Input input)
-        throws AuthException, BadRequestException, ResourceConflictException, Exception {
-      deleteEndpointCalls += 1;
-      return Response.ok();
-    }
-
-    int getDeleteEndpointCalls() {
-      return deleteEndpointCalls;
-    }
-  }
 }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationSetupBase.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationSetupBase.java
new file mode 100644
index 0000000..e07d481
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationSetupBase.java
@@ -0,0 +1,124 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.replication.pull;
+
+import static java.util.stream.Collectors.toList;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.acceptance.LightweightPluginDaemonTest;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.entities.Project.NameKey;
+import com.google.gerrit.extensions.events.ProjectDeletedListener;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.inject.Inject;
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.time.Duration;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.function.Supplier;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.FS;
+
+public abstract class PullReplicationSetupBase extends LightweightPluginDaemonTest {
+
+  protected static final Optional<String> ALL_PROJECTS = Optional.empty();
+  protected static final FluentLogger logger = FluentLogger.forEnclosingClass();
+  protected static final int TEST_REPLICATION_DELAY = 1;
+  protected static final Duration TEST_TIMEOUT = Duration.ofSeconds(TEST_REPLICATION_DELAY * 2000);
+  protected static final String TEST_REPLICATION_SUFFIX = "suffix1";
+  protected static final String TEST_REPLICATION_REMOTE = "remote1";
+  @Inject protected SitePaths sitePaths;
+  @Inject protected ProjectOperations projectOperations;
+  @Inject protected DynamicSet<ProjectDeletedListener> deletedListeners;
+  protected Path gitPath;
+  protected FileBasedConfig config;
+  protected FileBasedConfig secureConfig;
+
+  protected void setUpTestPlugin(boolean loadExisting) throws Exception {
+    gitPath = sitePaths.site_path.resolve("git");
+
+    File configFile = sitePaths.etc_dir.resolve("replication.config").toFile();
+    config = new FileBasedConfig(configFile, FS.DETECTED);
+    if (loadExisting && configFile.exists()) {
+      config.load();
+    }
+    setReplicationSource(
+        TEST_REPLICATION_REMOTE,
+        TEST_REPLICATION_SUFFIX,
+        ALL_PROJECTS); // Simulates a full replication.config initialization
+    config.save();
+
+    secureConfig =
+        new FileBasedConfig(sitePaths.etc_dir.resolve("secure.config").toFile(), FS.DETECTED);
+    setReplicationCredentials(TEST_REPLICATION_REMOTE, admin.username(), admin.httpPassword());
+    secureConfig.save();
+
+    super.setUpTestPlugin();
+  }
+
+  protected Ref getRef(Repository repo, String branchName) throws IOException {
+    return repo.getRefDatabase().exactRef(branchName);
+  }
+
+  protected Ref checkedGetRef(Repository repo, String branchName) {
+    try {
+      return repo.getRefDatabase().exactRef(branchName);
+    } catch (Exception e) {
+      logger.atSevere().withCause(e).log("failed to get ref %s in repo %s", branchName, repo);
+      return null;
+    }
+  }
+
+  protected void setReplicationSource(
+      String remoteName, String replicaSuffix, Optional<String> project)
+      throws IOException, ConfigInvalidException {
+    setReplicationSource(remoteName, Arrays.asList(replicaSuffix), project);
+  }
+
+  protected abstract void setReplicationSource(
+      String remoteName, List<String> replicaSuffixes, Optional<String> project) throws IOException;
+
+  protected void setReplicationCredentials(String remoteName, String username, String password)
+      throws IOException {
+    secureConfig.setString("remote", remoteName, "username", username);
+    secureConfig.setString("remote", remoteName, "password", password);
+    secureConfig.save();
+  }
+
+  protected void waitUntil(Supplier<Boolean> waitCondition) throws InterruptedException {
+    WaitUtil.waitUntil(waitCondition, TEST_TIMEOUT);
+  }
+
+  protected <T> T getInstance(Class<T> classObj) {
+    return plugin.getSysInjector().getInstance(classObj);
+  }
+
+  protected NameKey createTestProject(String name) throws Exception {
+    return projectOperations.newProject().name(name).create();
+  }
+
+  protected List<String> buildReplicaURLs(
+      List<String> replicaSuffixes, Function<String, String> toURL) {
+    return replicaSuffixes.stream().map(suffix -> toURL.apply(suffix)).collect(toList());
+  }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationWithGitHttpTransportProtocolIT.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationWithGitHttpTransportProtocolIT.java
new file mode 100644
index 0000000..d8e5947
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationWithGitHttpTransportProtocolIT.java
@@ -0,0 +1,102 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.replication.pull;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.acceptance.PushOneCommit.Result;
+import com.google.gerrit.acceptance.SkipProjectClone;
+import com.google.gerrit.acceptance.TestPlugin;
+import com.google.gerrit.acceptance.UseLocalDisk;
+import com.google.gerrit.acceptance.config.GerritConfig;
+import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
+import java.io.IOException;
+import java.util.List;
+import java.util.Optional;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.transport.ReceiveCommand;
+import org.junit.Test;
+
+@SkipProjectClone
+@UseLocalDisk
+@TestPlugin(
+    name = "pull-replication",
+    sysModule = "com.googlesource.gerrit.plugins.replication.pull.PullReplicationModule",
+    httpModule = "com.googlesource.gerrit.plugins.replication.pull.api.HttpModule")
+public class PullReplicationWithGitHttpTransportProtocolIT extends PullReplicationSetupBase {
+
+  @Override
+  protected void setReplicationSource(
+      String remoteName, List<String> replicaSuffixes, Optional<String> project)
+      throws IOException {
+    List<String> fetchUrls =
+        buildReplicaURLs(replicaSuffixes, s -> adminRestSession.url() + "/${name}" + s + ".git");
+    config.setStringList("remote", remoteName, "url", fetchUrls);
+    config.setString("remote", remoteName, "apiUrl", adminRestSession.url());
+    config.setString("remote", remoteName, "fetch", "+refs/*:refs/*");
+    config.setInt("remote", remoteName, "timeout", 600);
+    config.setInt("remote", remoteName, "replicationDelay", TEST_REPLICATION_DELAY);
+    project.ifPresent(prj -> config.setString("remote", remoteName, "projects", prj));
+    config.setBoolean("gerrit", null, "autoReload", true);
+    config.save();
+  }
+
+  @Override
+  public void setUpTestPlugin() throws Exception {
+    setUpTestPlugin(false);
+  }
+
+  @Test
+  @GerritConfig(name = "gerrit.instanceId", value = TEST_REPLICATION_REMOTE)
+  public void shouldReplicateWithBasicAuthentication() throws Exception {
+    runTest();
+  }
+
+  @Test
+  @GerritConfig(name = "gerrit.instanceId", value = TEST_REPLICATION_REMOTE)
+  @GerritConfig(name = "auth.bearerToken", value = "some-bearer-token")
+  public void shouldReplicateWithBearerTokenAuthentication() throws Exception {
+    runTest();
+  }
+
+  private void runTest() throws Exception {
+    testRepo = cloneProject(createTestProject(project + TEST_REPLICATION_SUFFIX));
+
+    Result pushResult = createChange();
+    RevCommit sourceCommit = pushResult.getCommit();
+    String sourceRef = pushResult.getPatchSet().refName();
+
+    ReplicationQueue pullReplicationQueue = getInstance(ReplicationQueue.class);
+    GitReferenceUpdatedListener.Event event =
+        new FakeGitReferenceUpdatedEvent(
+            project,
+            sourceRef,
+            ObjectId.zeroId().getName(),
+            sourceCommit.getId().getName(),
+            ReceiveCommand.Type.CREATE);
+    pullReplicationQueue.onGitReferenceUpdated(event);
+
+    try (Repository repo = repoManager.openRepository(project)) {
+      waitUntil(() -> checkedGetRef(repo, sourceRef) != null);
+
+      Ref targetBranchRef = getRef(repo, sourceRef);
+      assertThat(targetBranchRef).isNotNull();
+      assertThat(targetBranchRef.getObjectId()).isEqualTo(sourceCommit.getId());
+    }
+  }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/BearerAuthenticationFilterTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/BearerAuthenticationFilterTest.java
index bbbe66f..824496a 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/BearerAuthenticationFilterTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/BearerAuthenticationFilterTest.java
@@ -15,16 +15,19 @@
 package com.googlesource.gerrit.plugins.replication.pull.api;
 
 import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
+import static org.mockito.Mockito.atMost;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.httpd.WebSession;
 import com.google.gerrit.server.AccessPath;
-import com.google.gerrit.server.PluginUser;
 import com.google.gerrit.server.util.ThreadLocalRequestContext;
 import com.google.inject.Provider;
+import com.googlesource.gerrit.plugins.replication.pull.auth.PullReplicationInternalUser;
 import java.io.IOException;
+import java.util.Optional;
 import javax.servlet.FilterChain;
 import javax.servlet.ServletException;
 import javax.servlet.http.HttpServletRequest;
@@ -37,37 +40,36 @@
 @RunWith(MockitoJUnitRunner.class)
 public class BearerAuthenticationFilterTest {
 
+  private final Optional<String> NO_QUERY_PARAMETERS = Optional.empty();
+  private final Optional<String> GIT_UPLOAD_PACK_QUERY_PARAMETER =
+      Optional.of("service=git-upload-pack");
   @Mock private DynamicItem<WebSession> session;
   @Mock private WebSession webSession;
-  @Mock private Provider<PluginUser> pluginUserProvider;
   @Mock private Provider<ThreadLocalRequestContext> threadLocalRequestContextProvider;
-  @Mock private PluginUser pluginUser;
+  @Mock private PullReplicationInternalUser pluginUser;
   @Mock private ThreadLocalRequestContext threadLocalRequestContext;
   @Mock private HttpServletRequest httpServletRequest;
   @Mock private HttpServletResponse httpServletResponse;
   @Mock private FilterChain filterChain;
   private final String pluginName = "pull-replication";
 
-  private void authenticateWithURI(String uri) throws ServletException, IOException {
+  private void authenticateAndFilter(String uri, Optional<String> queryStringMaybe)
+      throws ServletException, IOException {
     final String bearerToken = "some-bearer-token";
     when(httpServletRequest.getRequestURI()).thenReturn(uri);
+    queryStringMaybe.ifPresent(qs -> when(httpServletRequest.getQueryString()).thenReturn(qs));
     when(httpServletRequest.getHeader("Authorization"))
         .thenReturn(String.format("Bearer %s", bearerToken));
-    when(pluginUserProvider.get()).thenReturn(pluginUser);
     when(threadLocalRequestContextProvider.get()).thenReturn(threadLocalRequestContext);
     when(session.get()).thenReturn(webSession);
     final BearerAuthenticationFilter filter =
         new BearerAuthenticationFilter(
-            session,
-            pluginName,
-            pluginUserProvider,
-            threadLocalRequestContextProvider,
-            bearerToken);
+            session, pluginName, pluginUser, threadLocalRequestContextProvider, bearerToken);
     filter.doFilter(httpServletRequest, httpServletResponse, filterChain);
 
-    verify(httpServletRequest).getRequestURI();
+    verify(httpServletRequest, atMost(2)).getRequestURI();
+    verify(httpServletRequest, atMost(1)).getQueryString();
     verify(httpServletRequest).getHeader("Authorization");
-    verify(pluginUserProvider).get();
     verify(threadLocalRequestContextProvider).get();
     verify(session).get();
     verify(webSession).setAccessPathOk(AccessPath.REST_API, true);
@@ -75,38 +77,45 @@
   }
 
   @Test
-  public void shouldAuthenticateWithBearerTokenWhenFetch() throws ServletException, IOException {
-    authenticateWithURI("any-prefix/pull-replication~fetch");
+  public void shouldAuthenticateWhenFetch() throws ServletException, IOException {
+    authenticateAndFilter("any-prefix/pull-replication~fetch", NO_QUERY_PARAMETERS);
   }
 
   @Test
-  public void shouldAuthenticateWithBearerTokenWhenApplyObject()
-      throws ServletException, IOException {
-    authenticateWithURI("any-prefix/pull-replication~apply-object");
+  public void shouldAuthenticateWhenApplyObject() throws ServletException, IOException {
+    authenticateAndFilter("any-prefix/pull-replication~apply-object", NO_QUERY_PARAMETERS);
   }
 
   @Test
-  public void shouldAuthenticateWithBearerTokenWhenApplyObjects()
-      throws ServletException, IOException {
-    authenticateWithURI("any-prefix/pull-replication~apply-objects");
+  public void shouldAuthenticateWhenApplyObjects() throws ServletException, IOException {
+    authenticateAndFilter("any-prefix/pull-replication~apply-objects", NO_QUERY_PARAMETERS);
   }
 
   @Test
-  public void shouldAuthenticateWithBearerTokenWhenDeleteProject()
-      throws ServletException, IOException {
-    authenticateWithURI("any-prefix/pull-replication~delete-project");
+  public void shouldAuthenticateWhenDeleteProject() throws ServletException, IOException {
+    authenticateAndFilter("any-prefix/pull-replication~delete-project", NO_QUERY_PARAMETERS);
   }
 
   @Test
-  public void shouldAuthenticateWithBearerTokenWhenUpdateHead()
-      throws ServletException, IOException {
-    authenticateWithURI("any-prefix/projects/my-project/HEAD");
+  public void shouldAuthenticateWhenUpdateHead() throws ServletException, IOException {
+    authenticateAndFilter("any-prefix/projects/my-project/HEAD", NO_QUERY_PARAMETERS);
   }
 
   @Test
-  public void shouldAuthenticateWithBearerTokenWhenInitProject()
+  public void shouldAuthenticateWhenInitProject() throws ServletException, IOException {
+    authenticateAndFilter(
+        "any-prefix/pull-replication/init-project/my-project.git", NO_QUERY_PARAMETERS);
+  }
+
+  @Test
+  public void shouldAuthenticateWhenGitUploadPacket() throws ServletException, IOException {
+    authenticateAndFilter("any-prefix/git-upload-pack", NO_QUERY_PARAMETERS);
+  }
+
+  @Test
+  public void shouldAuthenticateWhenGitUploadPacketInQueryParameter()
       throws ServletException, IOException {
-    authenticateWithURI("any-prefix/pull-replication/init-project/my-project.git");
+    authenticateAndFilter("any-prefix", GIT_UPLOAD_PACK_QUERY_PARAMETER);
   }
 
   @Test
@@ -119,7 +128,7 @@
         new BearerAuthenticationFilter(
             session,
             pluginName,
-            pluginUserProvider,
+            pluginUser,
             threadLocalRequestContextProvider,
             "some-bearer-token");
     filter.doFilter(httpServletRequest, httpServletResponse, filterChain);
@@ -138,7 +147,7 @@
         new BearerAuthenticationFilter(
             session,
             pluginName,
-            pluginUserProvider,
+            pluginUser,
             threadLocalRequestContextProvider,
             "some-bearer-token");
     filter.doFilter(httpServletRequest, httpServletResponse, filterChain);
@@ -156,7 +165,7 @@
         new BearerAuthenticationFilter(
             session,
             pluginName,
-            pluginUserProvider,
+            pluginUser,
             threadLocalRequestContextProvider,
             "some-bearer-token");
     filter.doFilter(httpServletRequest, httpServletResponse, filterChain);
@@ -173,12 +182,12 @@
         new BearerAuthenticationFilter(
             session,
             pluginName,
-            pluginUserProvider,
+            pluginUser,
             threadLocalRequestContextProvider,
             "some-bearer-token");
     filter.doFilter(httpServletRequest, httpServletResponse, filterChain);
 
-    verify(httpServletRequest).getRequestURI();
+    verify(httpServletRequest, times(2)).getRequestURI();
     verify(filterChain).doFilter(httpServletRequest, httpServletResponse);
   }
 
@@ -192,7 +201,7 @@
         new BearerAuthenticationFilter(
             session,
             pluginName,
-            pluginUserProvider,
+            pluginUser,
             threadLocalRequestContextProvider,
             "some-bearer-token");
     filter.doFilter(httpServletRequest, httpServletResponse, filterChain);
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/DeleteRefCommandTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/DeleteRefCommandTest.java
index 1574e0c..2adf72f 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/DeleteRefCommandTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/DeleteRefCommandTest.java
@@ -33,6 +33,7 @@
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
 import com.googlesource.gerrit.plugins.replication.pull.FetchRefReplicatedEvent;
+import com.googlesource.gerrit.plugins.replication.pull.LocalGitRepositoryManagerProvider;
 import com.googlesource.gerrit.plugins.replication.pull.PullReplicationStateLogger;
 import com.googlesource.gerrit.plugins.replication.pull.Source;
 import com.googlesource.gerrit.plugins.replication.pull.SourcesCollection;
@@ -100,11 +101,11 @@
         new DeleteRefCommand(
             fetchStateLog,
             projectCache,
-            eventDispatcherDataItem,
             sourceCollection,
             applyObject,
             permissionBackend,
-            gitManager);
+            eventDispatcherDataItem,
+            new LocalGitRepositoryManagerProvider(gitManager));
   }
 
   @Test
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/auth/PullReplicationGroupBackendIT.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/auth/PullReplicationGroupBackendIT.java
new file mode 100644
index 0000000..87738e3
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/auth/PullReplicationGroupBackendIT.java
@@ -0,0 +1,80 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.replication.pull.auth;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.googlesource.gerrit.plugins.replication.pull.auth.PullReplicationGroupBackend.INTERNAL_GROUP_DESCRIPTION;
+import static com.googlesource.gerrit.plugins.replication.pull.auth.PullReplicationGroupBackend.INTERNAL_GROUP_NAME;
+import static com.googlesource.gerrit.plugins.replication.pull.auth.PullReplicationGroupBackend.INTERNAL_GROUP_UUID;
+import static com.googlesource.gerrit.plugins.replication.pull.auth.PullReplicationGroupBackend.NAME_PREFIX;
+
+import com.google.gerrit.acceptance.LightweightPluginDaemonTest;
+import com.google.gerrit.acceptance.SkipProjectClone;
+import com.google.gerrit.acceptance.TestPlugin;
+import com.google.gerrit.entities.GroupDescription;
+import com.google.gerrit.entities.GroupReference;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.account.GroupMembership;
+import com.google.gerrit.server.group.SystemGroupBackend;
+import java.util.Collection;
+import org.junit.Test;
+
+@SkipProjectClone
+@TestPlugin(
+    name = "pull-replication",
+    sysModule = "com.googlesource.gerrit.plugins.replication.pull.auth.PullReplicationGroupModule")
+public class PullReplicationGroupBackendIT extends LightweightPluginDaemonTest {
+
+  @Test
+  public void shouldResolvePullReplicationInternalGroup() {
+    GroupDescription.Basic group = groupBackend.get(INTERNAL_GROUP_UUID);
+
+    assertThat(group).isNotNull();
+    assertThat(group).isEqualTo(INTERNAL_GROUP_DESCRIPTION);
+  }
+
+  @Test
+  public void shouldSuggestPullReplicationInternalGroup() {
+    Collection<GroupReference> groups = groupBackend.suggest(NAME_PREFIX, null);
+
+    assertThat(groups).isNotNull();
+    assertThat(groups).hasSize(1);
+
+    GroupReference groupReference = groups.iterator().next();
+    assertThat(groupReference.getName()).isEqualTo(INTERNAL_GROUP_NAME);
+    assertThat(groupReference.getUUID()).isEqualTo(INTERNAL_GROUP_UUID);
+  }
+
+  @Test
+  public void pullReplicationInternalUserShouldHaveMembershipOfInternalGroupAndAnonymousUsers() {
+    assertMemberOfInternalAndAnonymousUsers(
+        groupBackend.membershipsOf(getPullReplicationInternalUser()));
+  }
+
+  @Test
+  public void pullReplicationInternalUserShouldHaveEffectiveGroups() {
+    assertMemberOfInternalAndAnonymousUsers(getPullReplicationInternalUser().getEffectiveGroups());
+  }
+
+  private CurrentUser getPullReplicationInternalUser() {
+    CurrentUser user = plugin.getSysInjector().getInstance(PullReplicationInternalUser.class);
+    return user;
+  }
+
+  private void assertMemberOfInternalAndAnonymousUsers(GroupMembership userMembership) {
+    assertThat(userMembership.contains(INTERNAL_GROUP_UUID)).isTrue();
+    assertThat(userMembership.contains(SystemGroupBackend.ANONYMOUS_USERS)).isTrue();
+  }
+}
diff --git a/src/test/java/org/eclipse/jgit/transport/TransportHttpWithBearerTokenTest.java b/src/test/java/org/eclipse/jgit/transport/TransportHttpWithBearerTokenTest.java
new file mode 100644
index 0000000..5bf1047
--- /dev/null
+++ b/src/test/java/org/eclipse/jgit/transport/TransportHttpWithBearerTokenTest.java
@@ -0,0 +1,52 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package org.eclipse.jgit.transport;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import java.net.URISyntaxException;
+import org.junit.Test;
+
+public class TransportHttpWithBearerTokenTest {
+
+  @Test
+  public void cannotHandleURIWhenSchemaIsNeitherHttpNorHttps() throws URISyntaxException {
+    URIish uriUnderTest = new URIish("some-uri").setScheme(null);
+    boolean result = TransportHttpWithBearerToken.canHandle(uriUnderTest);
+    assertThat(result).isFalse();
+  }
+
+  @Test
+  public void cannotHandleURIWhenHostIsNotPresent() throws URISyntaxException {
+    URIish uriUnderTest = new URIish("some-uri").setScheme("http").setHost(null);
+    boolean result = TransportHttpWithBearerToken.canHandle(uriUnderTest);
+    assertThat(result).isFalse();
+  }
+
+  @Test
+  public void cannotHandleURIWhenPathIsNotPresent() throws URISyntaxException {
+    URIish uriUnderTest =
+        new URIish("some-uri").setScheme("http").setHost("some-host").setPath(null);
+    boolean result = TransportHttpWithBearerToken.canHandle(uriUnderTest);
+    assertThat(result).isFalse();
+  }
+
+  @Test
+  public void canHandleURIWhenIsWellFormed() throws URISyntaxException {
+    URIish uriUnderTest = new URIish("http://some-host/some-path");
+    boolean result = TransportHttpWithBearerToken.canHandle(uriUnderTest);
+    assertThat(result).isTrue();
+  }
+}
diff --git a/src/test/java/org/eclipse/jgit/transport/TransportProviderTest.java b/src/test/java/org/eclipse/jgit/transport/TransportProviderTest.java
new file mode 100644
index 0000000..ea70c40
--- /dev/null
+++ b/src/test/java/org/eclipse/jgit/transport/TransportProviderTest.java
@@ -0,0 +1,103 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package org.eclipse.jgit.transport;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.eclipse.jgit.transport.HttpConfig.EXTRA_HEADER;
+import static org.eclipse.jgit.transport.HttpConfig.HTTP;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.googlesource.gerrit.plugins.replication.CredentialsFactory;
+import com.googlesource.gerrit.plugins.replication.pull.BearerTokenProvider;
+import com.googlesource.gerrit.plugins.replication.pull.SourceConfiguration;
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.util.Optional;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.StoredConfig;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class TransportProviderTest {
+  @Mock private SourceConfiguration sourceConfig;
+  @Mock private CredentialsFactory cpFactory;
+  @Mock private RemoteConfig remoteConfig;
+  @Mock private BearerTokenProvider bearerTokenProvider;
+  @Mock private Repository repository;
+  @Mock private StoredConfig storedConfig;
+  @Mock private org.eclipse.jgit.transport.TransferConfig transferConfig;
+
+  @Before
+  public void setup() {
+    when(sourceConfig.getRemoteConfig()).thenReturn(remoteConfig);
+    when(repository.getConfig()).thenReturn(storedConfig);
+    String[] emptyHeaders = {};
+    when(storedConfig.getStringList(HTTP, null, EXTRA_HEADER)).thenReturn(emptyHeaders);
+    when(storedConfig.get(TransferConfig.KEY)).thenReturn(transferConfig);
+  }
+
+  private void verifyConstructor() {
+    verify(sourceConfig).getRemoteConfig();
+    verify(remoteConfig).getName();
+    verify(bearerTokenProvider).get();
+  }
+
+  @Test
+  public void shouldProvideTransportHttpWithBearerToken() throws URISyntaxException, IOException {
+    when(bearerTokenProvider.get()).thenReturn(Optional.of("some-bearer-token"));
+
+    TransportProvider transportProvider =
+        new TransportProvider(sourceConfig, cpFactory, bearerTokenProvider);
+    verifyConstructor();
+
+    URIish urIish = new URIish("http://some-host/some-path");
+    Transport transport = transportProvider.open(repository, urIish);
+    assertThat(transport).isInstanceOf(TransportHttpWithBearerToken.class);
+  }
+
+  @Test
+  public void shouldProvideNativeTransportWhenNoBearerTokenProvided()
+      throws URISyntaxException, IOException {
+
+    when(bearerTokenProvider.get()).thenReturn(Optional.empty());
+
+    TransportProvider transportProvider =
+        new TransportProvider(sourceConfig, cpFactory, bearerTokenProvider);
+    verifyConstructor();
+
+    URIish urIish = new URIish("ssh://some-host/some-path");
+    Transport transport = transportProvider.open(repository, urIish);
+    assertThat(transport).isNotInstanceOf(TransportHttpWithBearerToken.class);
+  }
+
+  @Test
+  public void shouldProvideNativeTransportWhenNoHttpSchemeProvided()
+      throws URISyntaxException, IOException {
+    when(bearerTokenProvider.get()).thenReturn(Optional.of("some-bearer-token"));
+
+    TransportProvider transportProvider =
+        new TransportProvider(sourceConfig, cpFactory, bearerTokenProvider);
+    verifyConstructor();
+
+    URIish urIish = new URIish("ssh://some-host/some-path");
+    Transport transport = transportProvider.open(repository, urIish);
+    assertThat(transport).isNotInstanceOf(TransportHttpWithBearerToken.class);
+  }
+}