Reduce duplicated code to build OAuth20Service

Change-Id: I0e70cbe305ca5bc48c77c1c17aeb58026cd902b9
diff --git a/src/main/java/com/googlesource/gerrit/plugins/oauth/Module.java b/src/main/java/com/googlesource/gerrit/plugins/oauth/Module.java
index dc41e8b..c54b7bf 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/oauth/Module.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/oauth/Module.java
@@ -51,6 +51,7 @@
   @Override
   protected void configure() {
     bind(OAuthPluginConfigFactory.class);
+    bind(OAuth20ServiceFactory.class);
     for (String provider : configuredProviders) {
       bind(AccountExternalIdCreator.class)
           .annotatedWith(Exports.named(provider))
diff --git a/src/main/java/com/googlesource/gerrit/plugins/oauth/OAuth20ServiceFactory.java b/src/main/java/com/googlesource/gerrit/plugins/oauth/OAuth20ServiceFactory.java
new file mode 100644
index 0000000..1e36385
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/oauth/OAuth20ServiceFactory.java
@@ -0,0 +1,54 @@
+// Copyright (C) 2026 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.oauth;
+
+import com.github.scribejava.core.builder.ServiceBuilder;
+import com.github.scribejava.core.builder.api.DefaultApi20;
+import com.github.scribejava.core.oauth.OAuth20Service;
+import com.google.common.base.Strings;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.gerrit.server.config.PluginConfig;
+import com.google.inject.Inject;
+
+public class OAuth20ServiceFactory {
+  private final OAuthPluginConfigFactory cfgFactory;
+  private final String canonicalWebUrl;
+
+  @Inject
+  public OAuth20ServiceFactory(
+      OAuthPluginConfigFactory cfgFactory, @CanonicalWebUrl String canonicalWebUrl) {
+    this.cfgFactory = cfgFactory;
+    this.canonicalWebUrl = canonicalWebUrl;
+  }
+
+  public OAuth20Service create(String providerName, DefaultApi20 api) {
+    return create(providerName, api, null);
+  }
+
+  public OAuth20Service create(String providerName, DefaultApi20 api, @Nullable String scope) {
+    PluginConfig cfg = cfgFactory.create(providerName);
+    ServiceBuilder builder =
+        new ServiceBuilder(cfg.getString(InitOAuth.CLIENT_ID))
+            .apiSecret(cfg.getString(InitOAuth.CLIENT_SECRET))
+            .callback(canonicalWebUrl + "oauth");
+
+    if (!Strings.isNullOrEmpty(scope)) {
+      builder.defaultScope(scope);
+    }
+
+    return builder.build(api);
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/oauth/airvantage/AirVantageOAuthService.java b/src/main/java/com/googlesource/gerrit/plugins/oauth/airvantage/AirVantageOAuthService.java
index 07fefde..07821ce 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/oauth/airvantage/AirVantageOAuthService.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/oauth/airvantage/AirVantageOAuthService.java
@@ -19,26 +19,20 @@
 import static javax.servlet.http.HttpServletResponse.SC_OK;
 import static org.slf4j.LoggerFactory.getLogger;
 
-import com.github.scribejava.core.builder.ServiceBuilder;
 import com.github.scribejava.core.model.OAuth2AccessToken;
 import com.github.scribejava.core.model.OAuthRequest;
 import com.github.scribejava.core.model.Response;
 import com.github.scribejava.core.model.Verb;
 import com.github.scribejava.core.oauth.OAuth20Service;
-import com.google.common.base.CharMatcher;
 import com.google.gerrit.extensions.auth.oauth.OAuthServiceProvider;
 import com.google.gerrit.extensions.auth.oauth.OAuthToken;
 import com.google.gerrit.extensions.auth.oauth.OAuthUserInfo;
 import com.google.gerrit.extensions.auth.oauth.OAuthVerifier;
-import com.google.gerrit.server.config.CanonicalWebUrl;
-import com.google.gerrit.server.config.PluginConfig;
 import com.google.gson.JsonElement;
 import com.google.gson.JsonObject;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
-import com.googlesource.gerrit.plugins.oauth.InitOAuth;
-import com.googlesource.gerrit.plugins.oauth.OAuthPluginConfigFactory;
+import com.googlesource.gerrit.plugins.oauth.OAuth20ServiceFactory;
 import com.googlesource.gerrit.plugins.oauth.OAuthServiceProviderConfig;
 import com.googlesource.gerrit.plugins.oauth.OAuthServiceProviderExternalIdScheme;
 import java.io.IOException;
@@ -56,16 +50,8 @@
   private final String extIdScheme;
 
   @Inject
-  AirVantageOAuthService(
-      OAuthPluginConfigFactory cfgFactory, @CanonicalWebUrl Provider<String> urlProvider) {
-    PluginConfig cfg = cfgFactory.create(PROVIDER_NAME);
-    String canonicalWebUrl = CharMatcher.is('/').trimTrailingFrom(urlProvider.get()) + "/";
-
-    service =
-        new ServiceBuilder(cfg.getString(InitOAuth.CLIENT_ID))
-            .apiSecret(cfg.getString(InitOAuth.CLIENT_SECRET))
-            .callback(canonicalWebUrl + "oauth")
-            .build(new AirVantageApi());
+  AirVantageOAuthService(OAuth20ServiceFactory oauth20ServiceFactory) {
+    service = oauth20ServiceFactory.create(PROVIDER_NAME, new AirVantageApi());
     extIdScheme = OAuthServiceProviderExternalIdScheme.create(PROVIDER_NAME);
   }
 
diff --git a/src/main/java/com/googlesource/gerrit/plugins/oauth/auth0/Auth0OAuthService.java b/src/main/java/com/googlesource/gerrit/plugins/oauth/auth0/Auth0OAuthService.java
index ac1934f..79e9afc 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/oauth/auth0/Auth0OAuthService.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/oauth/auth0/Auth0OAuthService.java
@@ -18,26 +18,23 @@
 import static com.googlesource.gerrit.plugins.oauth.JsonUtil.asString;
 import static com.googlesource.gerrit.plugins.oauth.JsonUtil.isNull;
 
-import com.github.scribejava.core.builder.ServiceBuilder;
 import com.github.scribejava.core.model.OAuth2AccessToken;
 import com.github.scribejava.core.model.OAuthRequest;
 import com.github.scribejava.core.model.Response;
 import com.github.scribejava.core.model.Verb;
 import com.github.scribejava.core.oauth.OAuth20Service;
-import com.google.common.base.CharMatcher;
 import com.google.gerrit.extensions.auth.oauth.OAuthServiceProvider;
 import com.google.gerrit.extensions.auth.oauth.OAuthToken;
 import com.google.gerrit.extensions.auth.oauth.OAuthUserInfo;
 import com.google.gerrit.extensions.auth.oauth.OAuthVerifier;
-import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.config.PluginConfig;
 import com.google.gson.JsonElement;
 import com.google.gson.JsonObject;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.ProvisionException;
 import com.google.inject.Singleton;
 import com.googlesource.gerrit.plugins.oauth.InitOAuth;
+import com.googlesource.gerrit.plugins.oauth.OAuth20ServiceFactory;
 import com.googlesource.gerrit.plugins.oauth.OAuthPluginConfigFactory;
 import com.googlesource.gerrit.plugins.oauth.OAuthServiceProviderConfig;
 import com.googlesource.gerrit.plugins.oauth.OAuthServiceProviderExternalIdScheme;
@@ -61,10 +58,8 @@
 
   @Inject
   Auth0OAuthService(
-      OAuthPluginConfigFactory cfgFactory, @CanonicalWebUrl Provider<String> urlProvider) {
+      OAuthPluginConfigFactory cfgFactory, OAuth20ServiceFactory oauth20ServiceFactory) {
     PluginConfig cfg = cfgFactory.create(PROVIDER_NAME);
-    String canonicalWebUrl = CharMatcher.is('/').trimTrailingFrom(urlProvider.get()) + "/";
-
     rootUrl = cfg.getString(InitOAuth.ROOT_URL);
     if (!URI.create(rootUrl).isAbsolute()) {
       throw new ProvisionException("Root URL must be absolute URL");
@@ -72,11 +67,8 @@
     serviceName = cfg.getString(InitOAuth.SERVICE_NAME, "Auth0");
 
     service =
-        new ServiceBuilder(cfg.getString(InitOAuth.CLIENT_ID))
-            .apiSecret(cfg.getString(InitOAuth.CLIENT_SECRET))
-            .callback(canonicalWebUrl + "oauth")
-            .defaultScope("openid profile email")
-            .build(new Auth0Api(rootUrl));
+        oauth20ServiceFactory.create(PROVIDER_NAME, new Auth0Api(rootUrl), "openid profile email");
+
     extIdScheme = OAuthServiceProviderExternalIdScheme.create(PROVIDER_NAME);
   }
 
diff --git a/src/main/java/com/googlesource/gerrit/plugins/oauth/authentik/AuthentikOAuthService.java b/src/main/java/com/googlesource/gerrit/plugins/oauth/authentik/AuthentikOAuthService.java
index fd1be49..809d8e8 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/oauth/authentik/AuthentikOAuthService.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/oauth/authentik/AuthentikOAuthService.java
@@ -18,26 +18,23 @@
 import static com.googlesource.gerrit.plugins.oauth.JsonUtil.asString;
 import static com.googlesource.gerrit.plugins.oauth.JsonUtil.isNull;
 
-import com.github.scribejava.core.builder.ServiceBuilder;
 import com.github.scribejava.core.model.OAuth2AccessToken;
 import com.github.scribejava.core.model.OAuthRequest;
 import com.github.scribejava.core.model.Response;
 import com.github.scribejava.core.model.Verb;
 import com.github.scribejava.core.oauth.OAuth20Service;
-import com.google.common.base.CharMatcher;
 import com.google.gerrit.extensions.auth.oauth.OAuthServiceProvider;
 import com.google.gerrit.extensions.auth.oauth.OAuthToken;
 import com.google.gerrit.extensions.auth.oauth.OAuthUserInfo;
 import com.google.gerrit.extensions.auth.oauth.OAuthVerifier;
-import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.config.PluginConfig;
 import com.google.gson.JsonElement;
 import com.google.gson.JsonObject;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.ProvisionException;
 import com.google.inject.Singleton;
 import com.googlesource.gerrit.plugins.oauth.InitOAuth;
+import com.googlesource.gerrit.plugins.oauth.OAuth20ServiceFactory;
 import com.googlesource.gerrit.plugins.oauth.OAuthPluginConfigFactory;
 import com.googlesource.gerrit.plugins.oauth.OAuthServiceProviderConfig;
 import com.googlesource.gerrit.plugins.oauth.OAuthServiceProviderExternalIdScheme;
@@ -62,9 +59,8 @@
 
   @Inject
   AuthentikOAuthService(
-      OAuthPluginConfigFactory cfgFactory, @CanonicalWebUrl Provider<String> urlProvider) {
+      OAuthPluginConfigFactory cfgFactory, OAuth20ServiceFactory oauth20ServiceFactory) {
     PluginConfig cfg = cfgFactory.create(PROVIDER_NAME);
-    String canonicalWebUrl = CharMatcher.is('/').trimTrailingFrom(urlProvider.get()) + "/";
 
     rootUrl = cfg.getString(InitOAuth.ROOT_URL);
     if (!URI.create(rootUrl).isAbsolute()) {
@@ -74,11 +70,9 @@
     linkExistingGerrit = cfg.getBoolean(InitOAuth.LINK_TO_EXISTING_GERRIT_ACCOUNT, false);
 
     service =
-        new ServiceBuilder(cfg.getString(InitOAuth.CLIENT_ID))
-            .apiSecret(cfg.getString(InitOAuth.CLIENT_SECRET))
-            .callback(canonicalWebUrl + "oauth")
-            .defaultScope("openid profile email")
-            .build(new AuthentikApi(rootUrl));
+        oauth20ServiceFactory.create(
+            PROVIDER_NAME, new AuthentikApi(rootUrl), "openid profile email");
+
     extIdScheme = OAuthServiceProviderExternalIdScheme.create(PROVIDER_NAME);
   }
 
diff --git a/src/main/java/com/googlesource/gerrit/plugins/oauth/azure/AzureActiveDirectoryService.java b/src/main/java/com/googlesource/gerrit/plugins/oauth/azure/AzureActiveDirectoryService.java
index 79edfda..2e141b0 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/oauth/azure/AzureActiveDirectoryService.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/oauth/azure/AzureActiveDirectoryService.java
@@ -20,28 +20,25 @@
 import static com.googlesource.gerrit.plugins.oauth.JsonUtil.jwtPayloadJson;
 
 import com.github.scribejava.apis.MicrosoftAzureActiveDirectory20Api;
-import com.github.scribejava.core.builder.ServiceBuilder;
 import com.github.scribejava.core.exceptions.OAuthException;
 import com.github.scribejava.core.model.OAuth2AccessToken;
 import com.github.scribejava.core.model.OAuthRequest;
 import com.github.scribejava.core.model.Response;
 import com.github.scribejava.core.model.Verb;
 import com.github.scribejava.core.oauth.OAuth20Service;
-import com.google.common.base.CharMatcher;
 import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.extensions.auth.oauth.OAuthServiceProvider;
 import com.google.gerrit.extensions.auth.oauth.OAuthToken;
 import com.google.gerrit.extensions.auth.oauth.OAuthUserInfo;
 import com.google.gerrit.extensions.auth.oauth.OAuthVerifier;
-import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.config.PluginConfig;
 import com.google.gson.Gson;
 import com.google.gson.JsonElement;
 import com.google.gson.JsonObject;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import com.googlesource.gerrit.plugins.oauth.InitOAuth;
+import com.googlesource.gerrit.plugins.oauth.OAuth20ServiceFactory;
 import com.googlesource.gerrit.plugins.oauth.OAuthPluginConfigFactory;
 import com.googlesource.gerrit.plugins.oauth.OAuthServiceProviderConfig;
 import com.googlesource.gerrit.plugins.oauth.OAuthServiceProviderExternalIdScheme;
@@ -67,7 +64,6 @@
       ImmutableSet.<String>builder().add(DEFAULT_TENANT).add("common").add("consumers").build();
   private final OAuth20Service service;
   private final Gson gson;
-  private final String canonicalWebUrl;
   private final boolean useEmailAsUsername;
   private final String tenant;
   private final String clientId;
@@ -79,24 +75,19 @@
 
   @Inject
   AzureActiveDirectoryService(
-      OAuthPluginConfigFactory cfgFactory, @CanonicalWebUrl Provider<String> urlProvider) {
+      OAuthPluginConfigFactory cfgFactory, OAuth20ServiceFactory oauth20ServiceFactory) {
     PluginConfig cfg = cfgFactory.create(PROVIDER_NAME);
     this.extIdScheme = OAuthServiceProviderExternalIdScheme.create(PROVIDER_NAME);
     this.extIdDeprecatedScheme =
         OAuthServiceProviderExternalIdScheme.create(PROVIDER_DEPRECATED_NAME);
-    this.canonicalWebUrl = CharMatcher.is('/').trimTrailingFrom(urlProvider.get()) + "/";
     this.useEmailAsUsername = cfg.getBoolean(InitOAuth.USE_EMAIL_AS_USERNAME, false);
     this.tenant = cfg.getString(InitOAuth.TENANT, DEFAULT_TENANT);
     this.clientId = cfg.getString(InitOAuth.CLIENT_ID);
     this.service =
-        new ServiceBuilder(cfg.getString(InitOAuth.CLIENT_ID))
-            .apiSecret(cfg.getString(InitOAuth.CLIENT_SECRET))
-            .callback(canonicalWebUrl + "oauth")
-            .defaultScope(SCOPE)
-            .build(MicrosoftAzureActiveDirectory20Api.custom(tenant));
+        oauth20ServiceFactory.create(
+            PROVIDER_NAME, MicrosoftAzureActiveDirectory20Api.custom(tenant), SCOPE);
     this.gson = JSON.newGson();
     if (log.isDebugEnabled()) {
-      log.debug("OAuth2: canonicalWebUrl={}", canonicalWebUrl);
       log.debug("OAuth2: scope={}", SCOPE);
       log.debug("OAuth2: useEmailAsUsername={}", useEmailAsUsername);
     }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/oauth/bitbucket/BitbucketOAuthService.java b/src/main/java/com/googlesource/gerrit/plugins/oauth/bitbucket/BitbucketOAuthService.java
index 6ec4577..3c0452e 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/oauth/bitbucket/BitbucketOAuthService.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/oauth/bitbucket/BitbucketOAuthService.java
@@ -19,25 +19,22 @@
 import static javax.servlet.http.HttpServletResponse.SC_OK;
 import static org.slf4j.LoggerFactory.getLogger;
 
-import com.github.scribejava.core.builder.ServiceBuilder;
 import com.github.scribejava.core.model.OAuth2AccessToken;
 import com.github.scribejava.core.model.OAuthRequest;
 import com.github.scribejava.core.model.Response;
 import com.github.scribejava.core.model.Verb;
 import com.github.scribejava.core.oauth.OAuth20Service;
-import com.google.common.base.CharMatcher;
 import com.google.gerrit.extensions.auth.oauth.OAuthServiceProvider;
 import com.google.gerrit.extensions.auth.oauth.OAuthToken;
 import com.google.gerrit.extensions.auth.oauth.OAuthUserInfo;
 import com.google.gerrit.extensions.auth.oauth.OAuthVerifier;
-import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.config.PluginConfig;
 import com.google.gson.JsonElement;
 import com.google.gson.JsonObject;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import com.googlesource.gerrit.plugins.oauth.InitOAuth;
+import com.googlesource.gerrit.plugins.oauth.OAuth20ServiceFactory;
 import com.googlesource.gerrit.plugins.oauth.OAuthPluginConfigFactory;
 import com.googlesource.gerrit.plugins.oauth.OAuthServiceProviderConfig;
 import com.googlesource.gerrit.plugins.oauth.OAuthServiceProviderExternalIdScheme;
@@ -57,15 +54,10 @@
 
   @Inject
   BitbucketOAuthService(
-      OAuthPluginConfigFactory cfgFactory, @CanonicalWebUrl Provider<String> urlProvider) {
+      OAuthPluginConfigFactory cfgFactory, OAuth20ServiceFactory oauth20ServiceFactory) {
     PluginConfig cfg = cfgFactory.create(PROVIDER_NAME);
-    String canonicalWebUrl = CharMatcher.is('/').trimTrailingFrom(urlProvider.get()) + "/";
     fixLegacyUserId = cfg.getBoolean(InitOAuth.FIX_LEGACY_USER_ID, false);
-    service =
-        new ServiceBuilder(cfg.getString(InitOAuth.CLIENT_ID))
-            .apiSecret(cfg.getString(InitOAuth.CLIENT_SECRET))
-            .callback(canonicalWebUrl + "oauth")
-            .build(new BitbucketApi());
+    service = oauth20ServiceFactory.create(PROVIDER_NAME, new BitbucketApi());
     extIdScheme = OAuthServiceProviderExternalIdScheme.create(PROVIDER_NAME);
   }
 
diff --git a/src/main/java/com/googlesource/gerrit/plugins/oauth/cas/CasOAuthService.java b/src/main/java/com/googlesource/gerrit/plugins/oauth/cas/CasOAuthService.java
index 25f5e90..dc75fd2 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/oauth/cas/CasOAuthService.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/oauth/cas/CasOAuthService.java
@@ -18,27 +18,24 @@
 import static com.google.gerrit.json.OutputFormat.JSON;
 import static com.googlesource.gerrit.plugins.oauth.JsonUtil.isNull;
 
-import com.github.scribejava.core.builder.ServiceBuilder;
 import com.github.scribejava.core.model.OAuth2AccessToken;
 import com.github.scribejava.core.model.OAuthRequest;
 import com.github.scribejava.core.model.Response;
 import com.github.scribejava.core.model.Verb;
 import com.github.scribejava.core.oauth.OAuth20Service;
-import com.google.common.base.CharMatcher;
 import com.google.gerrit.extensions.auth.oauth.OAuthServiceProvider;
 import com.google.gerrit.extensions.auth.oauth.OAuthToken;
 import com.google.gerrit.extensions.auth.oauth.OAuthUserInfo;
 import com.google.gerrit.extensions.auth.oauth.OAuthVerifier;
-import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.config.PluginConfig;
 import com.google.gson.JsonArray;
 import com.google.gson.JsonElement;
 import com.google.gson.JsonObject;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.ProvisionException;
 import com.google.inject.Singleton;
 import com.googlesource.gerrit.plugins.oauth.InitOAuth;
+import com.googlesource.gerrit.plugins.oauth.OAuth20ServiceFactory;
 import com.googlesource.gerrit.plugins.oauth.OAuthPluginConfigFactory;
 import com.googlesource.gerrit.plugins.oauth.OAuthServiceProviderConfig;
 import com.googlesource.gerrit.plugins.oauth.OAuthServiceProviderExternalIdScheme;
@@ -64,20 +61,16 @@
 
   @Inject
   CasOAuthService(
-      OAuthPluginConfigFactory cfgFactory, @CanonicalWebUrl Provider<String> urlProvider) {
+      OAuthPluginConfigFactory cfgFactory, OAuth20ServiceFactory oauth20ServiceFactory) {
     PluginConfig cfg = cfgFactory.create(PROVIDER_NAME);
     rootUrl = cfg.getString(InitOAuth.ROOT_URL);
     if (!URI.create(rootUrl).isAbsolute()) {
       throw new ProvisionException("Root URL must be absolute URL");
     }
-    String canonicalWebUrl = CharMatcher.is('/').trimTrailingFrom(urlProvider.get()) + "/";
     fixLegacyUserId = cfg.getBoolean(InitOAuth.FIX_LEGACY_USER_ID, false);
     boolean useJsonExtractor = cfg.getBoolean(USE_JSON_EXTRACTOR, false);
-    service =
-        new ServiceBuilder(cfg.getString(InitOAuth.CLIENT_ID))
-            .apiSecret(cfg.getString(InitOAuth.CLIENT_SECRET))
-            .callback(canonicalWebUrl + "oauth")
-            .build(new CasApi(rootUrl, useJsonExtractor));
+    service = oauth20ServiceFactory.create(PROVIDER_NAME, new CasApi(rootUrl, useJsonExtractor));
+
     extIdScheme = OAuthServiceProviderExternalIdScheme.create(PROVIDER_NAME);
   }
 
diff --git a/src/main/java/com/googlesource/gerrit/plugins/oauth/cognito/CognitoOAuthService.java b/src/main/java/com/googlesource/gerrit/plugins/oauth/cognito/CognitoOAuthService.java
index 612a822..d589162 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/oauth/cognito/CognitoOAuthService.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/oauth/cognito/CognitoOAuthService.java
@@ -18,26 +18,23 @@
 import static com.googlesource.gerrit.plugins.oauth.JsonUtil.asString;
 import static com.googlesource.gerrit.plugins.oauth.JsonUtil.isNull;
 
-import com.github.scribejava.core.builder.ServiceBuilder;
 import com.github.scribejava.core.model.OAuth2AccessToken;
 import com.github.scribejava.core.model.OAuthRequest;
 import com.github.scribejava.core.model.Response;
 import com.github.scribejava.core.model.Verb;
 import com.github.scribejava.core.oauth.OAuth20Service;
-import com.google.common.base.CharMatcher;
 import com.google.gerrit.extensions.auth.oauth.OAuthServiceProvider;
 import com.google.gerrit.extensions.auth.oauth.OAuthToken;
 import com.google.gerrit.extensions.auth.oauth.OAuthUserInfo;
 import com.google.gerrit.extensions.auth.oauth.OAuthVerifier;
-import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.config.PluginConfig;
 import com.google.gson.JsonElement;
 import com.google.gson.JsonObject;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.ProvisionException;
 import com.google.inject.Singleton;
 import com.googlesource.gerrit.plugins.oauth.InitOAuth;
+import com.googlesource.gerrit.plugins.oauth.OAuth20ServiceFactory;
 import com.googlesource.gerrit.plugins.oauth.OAuthPluginConfigFactory;
 import com.googlesource.gerrit.plugins.oauth.OAuthServiceProviderConfig;
 import com.googlesource.gerrit.plugins.oauth.OAuthServiceProviderExternalIdScheme;
@@ -63,9 +60,8 @@
 
   @Inject
   CognitoOAuthService(
-      OAuthPluginConfigFactory cfgFactory, @CanonicalWebUrl Provider<String> urlProvider) {
+      OAuthPluginConfigFactory cfgFactory, OAuth20ServiceFactory oauth20ServiceFactory) {
     PluginConfig cfg = cfgFactory.create(PROVIDER_NAME);
-    String canonicalWebUrl = CharMatcher.is('/').trimTrailingFrom(urlProvider.get()) + "/";
 
     rootUrl = cfg.getString(InitOAuth.ROOT_URL);
     if (!URI.create(rootUrl).isAbsolute()) {
@@ -76,11 +72,8 @@
     linkExistingGerrit = cfg.getBoolean(InitOAuth.LINK_TO_EXISTING_GERRIT_ACCOUNT, false);
 
     service =
-        new ServiceBuilder(cfg.getString(InitOAuth.CLIENT_ID))
-            .apiSecret(cfg.getString(InitOAuth.CLIENT_SECRET))
-            .callback(canonicalWebUrl + "oauth")
-            .defaultScope("openid profile email")
-            .build(new CognitoApi(rootUrl));
+        oauth20ServiceFactory.create(
+            PROVIDER_NAME, new CognitoApi(rootUrl), "openid profile email");
     extIdScheme = OAuthServiceProviderExternalIdScheme.create(PROVIDER_NAME);
   }
 
diff --git a/src/main/java/com/googlesource/gerrit/plugins/oauth/dex/DexOAuthService.java b/src/main/java/com/googlesource/gerrit/plugins/oauth/dex/DexOAuthService.java
index 2248d17..7282345 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/oauth/dex/DexOAuthService.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/oauth/dex/DexOAuthService.java
@@ -18,23 +18,20 @@
 import static com.googlesource.gerrit.plugins.oauth.JsonUtil.isNull;
 import static com.googlesource.gerrit.plugins.oauth.JsonUtil.jwtPayloadJson;
 
-import com.github.scribejava.core.builder.ServiceBuilder;
 import com.github.scribejava.core.model.OAuth2AccessToken;
 import com.github.scribejava.core.oauth.OAuth20Service;
-import com.google.common.base.CharMatcher;
 import com.google.gerrit.extensions.auth.oauth.OAuthServiceProvider;
 import com.google.gerrit.extensions.auth.oauth.OAuthToken;
 import com.google.gerrit.extensions.auth.oauth.OAuthUserInfo;
 import com.google.gerrit.extensions.auth.oauth.OAuthVerifier;
-import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.config.PluginConfig;
 import com.google.gson.JsonElement;
 import com.google.gson.JsonObject;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.ProvisionException;
 import com.google.inject.Singleton;
 import com.googlesource.gerrit.plugins.oauth.InitOAuth;
+import com.googlesource.gerrit.plugins.oauth.OAuth20ServiceFactory;
 import com.googlesource.gerrit.plugins.oauth.OAuthPluginConfigFactory;
 import com.googlesource.gerrit.plugins.oauth.OAuthServiceProviderConfig;
 import com.googlesource.gerrit.plugins.oauth.OAuthServiceProviderExternalIdScheme;
@@ -58,9 +55,8 @@
 
   @Inject
   DexOAuthService(
-      OAuthPluginConfigFactory cfgFactory, @CanonicalWebUrl Provider<String> urlProvider) {
+      OAuthPluginConfigFactory cfgFactory, OAuth20ServiceFactory oauth20ServiceFactory) {
     PluginConfig cfg = cfgFactory.create(PROVIDER_NAME);
-    String canonicalWebUrl = CharMatcher.is('/').trimTrailingFrom(urlProvider.get()) + "/";
 
     rootUrl = cfg.getString(InitOAuth.ROOT_URL);
     if (!URI.create(rootUrl).isAbsolute()) {
@@ -70,11 +66,9 @@
     serviceName = cfg.getString(InitOAuth.SERVICE_NAME, "Dex OAuth2");
 
     service =
-        new ServiceBuilder(cfg.getString(InitOAuth.CLIENT_ID))
-            .apiSecret(cfg.getString(InitOAuth.CLIENT_SECRET))
-            .defaultScope("openid profile email offline_access")
-            .callback(canonicalWebUrl + "oauth")
-            .build(new DexApi(rootUrl));
+        oauth20ServiceFactory.create(
+            PROVIDER_NAME, new DexApi(rootUrl), "openid profile email offline_access");
+
     extIdScheme = OAuthServiceProviderExternalIdScheme.create(PROVIDER_NAME);
   }
 
diff --git a/src/main/java/com/googlesource/gerrit/plugins/oauth/facebook/FacebookOAuthService.java b/src/main/java/com/googlesource/gerrit/plugins/oauth/facebook/FacebookOAuthService.java
index 5888369..b746e04 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/oauth/facebook/FacebookOAuthService.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/oauth/facebook/FacebookOAuthService.java
@@ -18,26 +18,20 @@
 import static com.googlesource.gerrit.plugins.oauth.JsonUtil.asString;
 import static com.googlesource.gerrit.plugins.oauth.JsonUtil.isNull;
 
-import com.github.scribejava.core.builder.ServiceBuilder;
 import com.github.scribejava.core.model.OAuth2AccessToken;
 import com.github.scribejava.core.model.OAuthRequest;
 import com.github.scribejava.core.model.Response;
 import com.github.scribejava.core.model.Verb;
 import com.github.scribejava.core.oauth.OAuth20Service;
-import com.google.common.base.CharMatcher;
 import com.google.gerrit.extensions.auth.oauth.OAuthServiceProvider;
 import com.google.gerrit.extensions.auth.oauth.OAuthToken;
 import com.google.gerrit.extensions.auth.oauth.OAuthUserInfo;
 import com.google.gerrit.extensions.auth.oauth.OAuthVerifier;
-import com.google.gerrit.server.config.CanonicalWebUrl;
-import com.google.gerrit.server.config.PluginConfig;
 import com.google.gson.JsonElement;
 import com.google.gson.JsonObject;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
-import com.googlesource.gerrit.plugins.oauth.InitOAuth;
-import com.googlesource.gerrit.plugins.oauth.OAuthPluginConfigFactory;
+import com.googlesource.gerrit.plugins.oauth.OAuth20ServiceFactory;
 import com.googlesource.gerrit.plugins.oauth.OAuthServiceProviderConfig;
 import com.googlesource.gerrit.plugins.oauth.OAuthServiceProviderExternalIdScheme;
 import java.io.IOException;
@@ -59,17 +53,9 @@
   private final String extIdScheme;
 
   @Inject
-  FacebookOAuthService(
-      OAuthPluginConfigFactory cfgFactory, @CanonicalWebUrl Provider<String> urlProvider) {
-    PluginConfig cfg = cfgFactory.create(PROVIDER_NAME);
-    String canonicalWebUrl = CharMatcher.is('/').trimTrailingFrom(urlProvider.get()) + "/";
+  FacebookOAuthService(OAuth20ServiceFactory oauth20ServiceFactory) {
+    service = oauth20ServiceFactory.create(PROVIDER_NAME, new Facebook2Api(), SCOPE);
 
-    service =
-        new ServiceBuilder(cfg.getString(InitOAuth.CLIENT_ID))
-            .apiSecret(cfg.getString(InitOAuth.CLIENT_SECRET))
-            .callback(canonicalWebUrl + "oauth")
-            .defaultScope(SCOPE)
-            .build(new Facebook2Api());
     extIdScheme = OAuthServiceProviderExternalIdScheme.create(PROVIDER_NAME);
   }
 
diff --git a/src/main/java/com/googlesource/gerrit/plugins/oauth/github/GitHubOAuthService.java b/src/main/java/com/googlesource/gerrit/plugins/oauth/github/GitHubOAuthService.java
index b72a93f..fc8e70e 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/oauth/github/GitHubOAuthService.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/oauth/github/GitHubOAuthService.java
@@ -18,7 +18,6 @@
 import static com.googlesource.gerrit.plugins.oauth.JsonUtil.asString;
 import static com.googlesource.gerrit.plugins.oauth.JsonUtil.isNull;
 
-import com.github.scribejava.core.builder.ServiceBuilder;
 import com.github.scribejava.core.model.OAuth2AccessToken;
 import com.github.scribejava.core.model.OAuthRequest;
 import com.github.scribejava.core.model.Response;
@@ -29,14 +28,13 @@
 import com.google.gerrit.extensions.auth.oauth.OAuthToken;
 import com.google.gerrit.extensions.auth.oauth.OAuthUserInfo;
 import com.google.gerrit.extensions.auth.oauth.OAuthVerifier;
-import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.config.PluginConfig;
 import com.google.gson.JsonElement;
 import com.google.gson.JsonObject;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import com.googlesource.gerrit.plugins.oauth.InitOAuth;
+import com.googlesource.gerrit.plugins.oauth.OAuth20ServiceFactory;
 import com.googlesource.gerrit.plugins.oauth.OAuthPluginConfigFactory;
 import com.googlesource.gerrit.plugins.oauth.OAuthServiceProviderConfig;
 import com.googlesource.gerrit.plugins.oauth.OAuthServiceProviderExternalIdScheme;
@@ -63,20 +61,15 @@
 
   @Inject
   GitHubOAuthService(
-      OAuthPluginConfigFactory cfgFactory, @CanonicalWebUrl Provider<String> urlProvider) {
+      OAuthPluginConfigFactory cfgFactory, OAuth20ServiceFactory oauth20ServiceFactory) {
     PluginConfig cfg = cfgFactory.create(PROVIDER_NAME);
-    String canonicalWebUrl = CharMatcher.is('/').trimTrailingFrom(urlProvider.get()) + "/";
     fixLegacyUserId = cfg.getBoolean(InitOAuth.FIX_LEGACY_USER_ID, false);
     rootUrl =
         CharMatcher.is('/').trimTrailingFrom(cfg.getString(InitOAuth.ROOT_URL, GITHUB_ROOT_URL))
             + "/";
 
-    service =
-        new ServiceBuilder(cfg.getString(InitOAuth.CLIENT_ID))
-            .apiSecret(cfg.getString(InitOAuth.CLIENT_SECRET))
-            .callback(canonicalWebUrl + "oauth")
-            .defaultScope(SCOPE)
-            .build(new GitHub2Api(rootUrl));
+    service = oauth20ServiceFactory.create(PROVIDER_NAME, new GitHub2Api(rootUrl), SCOPE);
+
     extIdScheme = OAuthServiceProviderExternalIdScheme.create(PROVIDER_NAME);
   }
 
diff --git a/src/main/java/com/googlesource/gerrit/plugins/oauth/gitlab/GitLabOAuthService.java b/src/main/java/com/googlesource/gerrit/plugins/oauth/gitlab/GitLabOAuthService.java
index 426b06f..0af8eff 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/oauth/gitlab/GitLabOAuthService.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/oauth/gitlab/GitLabOAuthService.java
@@ -20,26 +20,23 @@
 import static javax.servlet.http.HttpServletResponse.SC_OK;
 import static org.slf4j.LoggerFactory.getLogger;
 
-import com.github.scribejava.core.builder.ServiceBuilder;
 import com.github.scribejava.core.model.OAuth2AccessToken;
 import com.github.scribejava.core.model.OAuthRequest;
 import com.github.scribejava.core.model.Response;
 import com.github.scribejava.core.model.Verb;
 import com.github.scribejava.core.oauth.OAuth20Service;
-import com.google.common.base.CharMatcher;
 import com.google.gerrit.extensions.auth.oauth.OAuthServiceProvider;
 import com.google.gerrit.extensions.auth.oauth.OAuthToken;
 import com.google.gerrit.extensions.auth.oauth.OAuthUserInfo;
 import com.google.gerrit.extensions.auth.oauth.OAuthVerifier;
-import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.config.PluginConfig;
 import com.google.gson.JsonElement;
 import com.google.gson.JsonObject;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.ProvisionException;
 import com.google.inject.Singleton;
 import com.googlesource.gerrit.plugins.oauth.InitOAuth;
+import com.googlesource.gerrit.plugins.oauth.OAuth20ServiceFactory;
 import com.googlesource.gerrit.plugins.oauth.OAuthPluginConfigFactory;
 import com.googlesource.gerrit.plugins.oauth.OAuthServiceProviderConfig;
 import com.googlesource.gerrit.plugins.oauth.OAuthServiceProviderExternalIdScheme;
@@ -60,18 +57,14 @@
 
   @Inject
   GitLabOAuthService(
-      OAuthPluginConfigFactory cfgFactory, @CanonicalWebUrl Provider<String> urlProvider) {
+      OAuthPluginConfigFactory cfgFactory, OAuth20ServiceFactory oauth20ServiceFactory) {
     PluginConfig cfg = cfgFactory.create(PROVIDER_NAME);
-    String canonicalWebUrl = CharMatcher.is('/').trimTrailingFrom(urlProvider.get()) + "/";
     rootUrl = cfg.getString(InitOAuth.ROOT_URL);
     if (!URI.create(rootUrl).isAbsolute()) {
       throw new ProvisionException("Root URL must be absolute URL");
     }
-    service =
-        new ServiceBuilder(cfg.getString(InitOAuth.CLIENT_ID))
-            .apiSecret(cfg.getString(InitOAuth.CLIENT_SECRET))
-            .callback(canonicalWebUrl + "oauth")
-            .build(new GitLabApi(rootUrl));
+    service = oauth20ServiceFactory.create(PROVIDER_NAME, new GitLabApi(rootUrl));
+
     extIdScheme = OAuthServiceProviderExternalIdScheme.create(PROVIDER_NAME);
   }
 
diff --git a/src/main/java/com/googlesource/gerrit/plugins/oauth/google/GoogleOAuthService.java b/src/main/java/com/googlesource/gerrit/plugins/oauth/google/GoogleOAuthService.java
index 49a78d6..dd64ffc 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/oauth/google/GoogleOAuthService.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/oauth/google/GoogleOAuthService.java
@@ -19,26 +19,23 @@
 import static com.googlesource.gerrit.plugins.oauth.JsonUtil.isNull;
 import static com.googlesource.gerrit.plugins.oauth.JsonUtil.jwtPayloadJson;
 
-import com.github.scribejava.core.builder.ServiceBuilder;
 import com.github.scribejava.core.model.OAuth2AccessToken;
 import com.github.scribejava.core.model.OAuthRequest;
 import com.github.scribejava.core.model.Response;
 import com.github.scribejava.core.model.Verb;
 import com.github.scribejava.core.oauth.OAuth20Service;
-import com.google.common.base.CharMatcher;
 import com.google.common.base.Strings;
 import com.google.gerrit.extensions.auth.oauth.OAuthServiceProvider;
 import com.google.gerrit.extensions.auth.oauth.OAuthToken;
 import com.google.gerrit.extensions.auth.oauth.OAuthUserInfo;
 import com.google.gerrit.extensions.auth.oauth.OAuthVerifier;
-import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.config.PluginConfig;
 import com.google.gson.JsonElement;
 import com.google.gson.JsonObject;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import com.googlesource.gerrit.plugins.oauth.InitOAuth;
+import com.googlesource.gerrit.plugins.oauth.OAuth20ServiceFactory;
 import com.googlesource.gerrit.plugins.oauth.OAuthPluginConfigFactory;
 import com.googlesource.gerrit.plugins.oauth.OAuthServiceProviderConfig;
 import com.googlesource.gerrit.plugins.oauth.OAuthServiceProviderExternalIdScheme;
@@ -62,7 +59,6 @@
       "https://www.googleapis.com/oauth2/v2/userinfo";
   private static final String SCOPE = "email profile";
   private final OAuth20Service service;
-  private final String canonicalWebUrl;
   private final List<String> domains;
   private final boolean useEmailAsUsername;
   private final boolean fixLegacyUserId;
@@ -70,9 +66,8 @@
 
   @Inject
   GoogleOAuthService(
-      OAuthPluginConfigFactory cfgFactory, @CanonicalWebUrl Provider<String> urlProvider) {
+      OAuthPluginConfigFactory cfgFactory, OAuth20ServiceFactory oauth20ServiceFactory) {
     PluginConfig cfg = cfgFactory.create(PROVIDER_NAME);
-    this.canonicalWebUrl = CharMatcher.is('/').trimTrailingFrom(urlProvider.get()) + "/";
     if (cfg.getBoolean(InitOAuth.LINK_TO_EXISTING_OPENID_ACCOUNT, false)) {
       log.warn(
           String.format(
@@ -81,14 +76,9 @@
     fixLegacyUserId = cfg.getBoolean(InitOAuth.FIX_LEGACY_USER_ID, false);
     this.domains = Arrays.asList(cfg.getStringList(InitOAuth.DOMAIN));
     this.useEmailAsUsername = cfg.getBoolean(InitOAuth.USE_EMAIL_AS_USERNAME, false);
-    this.service =
-        new ServiceBuilder(cfg.getString(InitOAuth.CLIENT_ID))
-            .apiSecret(cfg.getString(InitOAuth.CLIENT_SECRET))
-            .callback(canonicalWebUrl + "oauth")
-            .defaultScope(SCOPE)
-            .build(new Google2Api());
+    this.service = oauth20ServiceFactory.create(PROVIDER_NAME, new Google2Api(), SCOPE);
+
     if (log.isDebugEnabled()) {
-      log.debug("OAuth2: canonicalWebUrl={}", canonicalWebUrl);
       log.debug("OAuth2: scope={}", SCOPE);
       log.debug("OAuth2: domains={}", domains);
       log.debug("OAuth2: useEmailAsUsername={}", useEmailAsUsername);
diff --git a/src/main/java/com/googlesource/gerrit/plugins/oauth/keycloak/KeycloakOAuthService.java b/src/main/java/com/googlesource/gerrit/plugins/oauth/keycloak/KeycloakOAuthService.java
index 4bdf501..90a2797 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/oauth/keycloak/KeycloakOAuthService.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/oauth/keycloak/KeycloakOAuthService.java
@@ -18,23 +18,20 @@
 import static com.googlesource.gerrit.plugins.oauth.JsonUtil.isNull;
 import static com.googlesource.gerrit.plugins.oauth.JsonUtil.jwtPayloadJson;
 
-import com.github.scribejava.core.builder.ServiceBuilder;
 import com.github.scribejava.core.model.OAuth2AccessToken;
 import com.github.scribejava.core.oauth.OAuth20Service;
-import com.google.common.base.CharMatcher;
 import com.google.gerrit.extensions.auth.oauth.OAuthServiceProvider;
 import com.google.gerrit.extensions.auth.oauth.OAuthToken;
 import com.google.gerrit.extensions.auth.oauth.OAuthUserInfo;
 import com.google.gerrit.extensions.auth.oauth.OAuthVerifier;
-import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.config.PluginConfig;
 import com.google.gson.JsonElement;
 import com.google.gson.JsonObject;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.ProvisionException;
 import com.google.inject.Singleton;
 import com.googlesource.gerrit.plugins.oauth.InitOAuth;
+import com.googlesource.gerrit.plugins.oauth.OAuth20ServiceFactory;
 import com.googlesource.gerrit.plugins.oauth.OAuthPluginConfigFactory;
 import com.googlesource.gerrit.plugins.oauth.OAuthServiceProviderConfig;
 import com.googlesource.gerrit.plugins.oauth.OAuthServiceProviderExternalIdScheme;
@@ -58,9 +55,8 @@
 
   @Inject
   KeycloakOAuthService(
-      OAuthPluginConfigFactory cfgFactory, @CanonicalWebUrl Provider<String> urlProvider) {
+      OAuthPluginConfigFactory cfgFactory, OAuth20ServiceFactory oauth20ServiceFactory) {
     PluginConfig cfg = cfgFactory.create(PROVIDER_NAME);
-    String canonicalWebUrl = CharMatcher.is('/').trimTrailingFrom(urlProvider.get()) + "/";
 
     String rootUrl = cfg.getString(InitOAuth.ROOT_URL);
     if (!URI.create(rootUrl).isAbsolute()) {
@@ -71,11 +67,8 @@
     usePreferredUsername = cfg.getBoolean(InitOAuth.USE_PREFERRED_USERNAME, true);
 
     service =
-        new ServiceBuilder(cfg.getString(InitOAuth.CLIENT_ID))
-            .apiSecret(cfg.getString(InitOAuth.CLIENT_SECRET))
-            .callback(canonicalWebUrl + "oauth")
-            .defaultScope("openid")
-            .build(new KeycloakApi(rootUrl, realm));
+        oauth20ServiceFactory.create(PROVIDER_NAME, new KeycloakApi(rootUrl, realm), "openid");
+
     extIdScheme = OAuthServiceProviderExternalIdScheme.create(PROVIDER_NAME);
   }
 
diff --git a/src/main/java/com/googlesource/gerrit/plugins/oauth/lemon/LemonLDAPOAuthService.java b/src/main/java/com/googlesource/gerrit/plugins/oauth/lemon/LemonLDAPOAuthService.java
index ea3fbd8..7e53550 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/oauth/lemon/LemonLDAPOAuthService.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/oauth/lemon/LemonLDAPOAuthService.java
@@ -19,25 +19,22 @@
 import static com.googlesource.gerrit.plugins.oauth.JsonUtil.isNull;
 import static org.slf4j.LoggerFactory.getLogger;
 
-import com.github.scribejava.core.builder.ServiceBuilder;
 import com.github.scribejava.core.model.OAuth2AccessToken;
 import com.github.scribejava.core.model.OAuthRequest;
 import com.github.scribejava.core.model.Response;
 import com.github.scribejava.core.model.Verb;
 import com.github.scribejava.core.oauth.OAuth20Service;
-import com.google.common.base.CharMatcher;
 import com.google.gerrit.extensions.auth.oauth.OAuthServiceProvider;
 import com.google.gerrit.extensions.auth.oauth.OAuthToken;
 import com.google.gerrit.extensions.auth.oauth.OAuthUserInfo;
 import com.google.gerrit.extensions.auth.oauth.OAuthVerifier;
-import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.config.PluginConfig;
 import com.google.gson.JsonElement;
 import com.google.gson.JsonObject;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import com.googlesource.gerrit.plugins.oauth.InitOAuth;
+import com.googlesource.gerrit.plugins.oauth.OAuth20ServiceFactory;
 import com.googlesource.gerrit.plugins.oauth.OAuthPluginConfigFactory;
 import com.googlesource.gerrit.plugins.oauth.OAuthServiceProviderConfig;
 import java.io.IOException;
@@ -57,16 +54,12 @@
 
   @Inject
   LemonLDAPOAuthService(
-      OAuthPluginConfigFactory cfgFactory, @CanonicalWebUrl Provider<String> urlProvider) {
+      OAuthPluginConfigFactory cfgFactory, OAuth20ServiceFactory oauth20ServiceFactory) {
     PluginConfig cfg = cfgFactory.create(PROVIDER_NAME);
-    String canonicalWebUrl = CharMatcher.is('/').trimTrailingFrom(urlProvider.get()) + "/";
     rootUrl = cfg.getString(InitOAuth.ROOT_URL);
     service =
-        new ServiceBuilder(cfg.getString(InitOAuth.CLIENT_ID))
-            .apiSecret(cfg.getString(InitOAuth.CLIENT_SECRET))
-            .defaultScope("openid profile email")
-            .callback(canonicalWebUrl + "oauth")
-            .build(new LemonLDAPApi(rootUrl));
+        oauth20ServiceFactory.create(
+            PROVIDER_NAME, new LemonLDAPApi(rootUrl), "openid profile email");
   }
 
   @Override
diff --git a/src/main/java/com/googlesource/gerrit/plugins/oauth/phabricator/PhabricatorOAuthService.java b/src/main/java/com/googlesource/gerrit/plugins/oauth/phabricator/PhabricatorOAuthService.java
index 7a67fc7..1efd6a8 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/oauth/phabricator/PhabricatorOAuthService.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/oauth/phabricator/PhabricatorOAuthService.java
@@ -18,26 +18,23 @@
 import static com.googlesource.gerrit.plugins.oauth.JsonUtil.asString;
 import static com.googlesource.gerrit.plugins.oauth.JsonUtil.isNull;
 
-import com.github.scribejava.core.builder.ServiceBuilder;
 import com.github.scribejava.core.model.OAuth2AccessToken;
 import com.github.scribejava.core.model.OAuthRequest;
 import com.github.scribejava.core.model.Response;
 import com.github.scribejava.core.model.Verb;
 import com.github.scribejava.core.oauth.OAuth20Service;
-import com.google.common.base.CharMatcher;
 import com.google.gerrit.extensions.auth.oauth.OAuthServiceProvider;
 import com.google.gerrit.extensions.auth.oauth.OAuthToken;
 import com.google.gerrit.extensions.auth.oauth.OAuthUserInfo;
 import com.google.gerrit.extensions.auth.oauth.OAuthVerifier;
-import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.config.PluginConfig;
 import com.google.gson.JsonElement;
 import com.google.gson.JsonObject;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.ProvisionException;
 import com.google.inject.Singleton;
 import com.googlesource.gerrit.plugins.oauth.InitOAuth;
+import com.googlesource.gerrit.plugins.oauth.OAuth20ServiceFactory;
 import com.googlesource.gerrit.plugins.oauth.OAuthPluginConfigFactory;
 import com.googlesource.gerrit.plugins.oauth.OAuthServiceProviderConfig;
 import com.googlesource.gerrit.plugins.oauth.OAuthServiceProviderExternalIdScheme;
@@ -60,21 +57,13 @@
 
   @Inject
   PhabricatorOAuthService(
-      OAuthPluginConfigFactory cfgFactory, @CanonicalWebUrl Provider<String> urlProvider) {
+      OAuthPluginConfigFactory cfgFactory, OAuth20ServiceFactory oauth20ServiceFactory) {
     PluginConfig cfg = cfgFactory.create(PROVIDER_NAME);
-    String canonicalWebUrl = CharMatcher.is('/').trimTrailingFrom(urlProvider.get()) + "/";
     rootUrl = cfg.getString(InitOAuth.ROOT_URL);
     if (!URI.create(rootUrl).isAbsolute()) {
       throw new ProvisionException("Root URL must be absolute URL");
     }
-    this.service =
-        new ServiceBuilder(cfg.getString(InitOAuth.CLIENT_ID))
-            .apiSecret(cfg.getString(InitOAuth.CLIENT_SECRET))
-            .callback(canonicalWebUrl + "oauth")
-            .build(new PhabricatorApi(rootUrl));
-    if (log.isDebugEnabled()) {
-      log.debug("OAuth2: canonicalWebUrl={}", canonicalWebUrl);
-    }
+    this.service = oauth20ServiceFactory.create(PROVIDER_NAME, new PhabricatorApi(rootUrl));
     extIdScheme = OAuthServiceProviderExternalIdScheme.create(PROVIDER_NAME);
   }
 
diff --git a/src/main/java/com/googlesource/gerrit/plugins/oauth/sap/SAPIasOAuthService.java b/src/main/java/com/googlesource/gerrit/plugins/oauth/sap/SAPIasOAuthService.java
index 4aa2b53..7925e6b 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/oauth/sap/SAPIasOAuthService.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/oauth/sap/SAPIasOAuthService.java
@@ -16,7 +16,6 @@
 
 import static org.slf4j.LoggerFactory.getLogger;
 
-import com.github.scribejava.core.builder.ServiceBuilder;
 import com.github.scribejava.core.model.OAuth2AccessToken;
 import com.github.scribejava.core.oauth.AccessTokenRequestParams;
 import com.github.scribejava.core.oauth.AuthorizationUrlBuilder;
@@ -26,13 +25,12 @@
 import com.google.gerrit.extensions.auth.oauth.OAuthToken;
 import com.google.gerrit.extensions.auth.oauth.OAuthUserInfo;
 import com.google.gerrit.extensions.auth.oauth.OAuthVerifier;
-import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.config.PluginConfig;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.ProvisionException;
 import com.google.inject.Singleton;
 import com.googlesource.gerrit.plugins.oauth.InitOAuth;
+import com.googlesource.gerrit.plugins.oauth.OAuth20ServiceFactory;
 import com.googlesource.gerrit.plugins.oauth.OAuthPluginConfigFactory;
 import com.googlesource.gerrit.plugins.oauth.OAuthServiceProviderConfig;
 import com.googlesource.gerrit.plugins.oauth.OAuthServiceProviderExternalIdScheme;
@@ -65,10 +63,9 @@
   @Inject
   SAPIasOAuthService(
       OAuthPluginConfigFactory cfgFactory,
-      @CanonicalWebUrl Provider<String> urlProvider,
+      OAuth20ServiceFactory oauth20ServiceFactory,
       CombiningValidator<Token> tokenValidator) {
     PluginConfig cfg = cfgFactory.create(PROVIDER_NAME);
-    String canonicalWebUrl = urlProvider.get();
     rootUrl = cfg.getString(InitOAuth.ROOT_URL);
     if (!URI.create(rootUrl).isAbsolute()) {
       throw new ProvisionException("Root URL must be absolute URL");
@@ -77,11 +74,8 @@
     linkExistingGerrit = cfg.getBoolean(InitOAuth.LINK_TO_EXISTING_GERRIT_ACCOUNT, false);
     enablePKCE = cfg.getBoolean(InitOAuth.ENABLE_PKCE, false);
     service =
-        new ServiceBuilder(cfg.getString(InitOAuth.CLIENT_ID))
-            .apiSecret(cfg.getString(InitOAuth.CLIENT_SECRET))
-            .callback(canonicalWebUrl + "oauth")
-            .defaultScope("openid profile email")
-            .build(new SAPIasApi(rootUrl));
+        oauth20ServiceFactory.create(PROVIDER_NAME, new SAPIasApi(rootUrl), "openid profile email");
+
     authorizationUrlBuilder = service.createAuthorizationUrlBuilder();
     extIdScheme = OAuthServiceProviderExternalIdScheme.create(PROVIDER_NAME);
     this.tokenValidator = tokenValidator;
diff --git a/src/main/java/com/googlesource/gerrit/plugins/oauth/tuleap/TuleapOAuthService.java b/src/main/java/com/googlesource/gerrit/plugins/oauth/tuleap/TuleapOAuthService.java
index b24aac8..6bbb7ea 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/oauth/tuleap/TuleapOAuthService.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/oauth/tuleap/TuleapOAuthService.java
@@ -18,26 +18,23 @@
 import static com.googlesource.gerrit.plugins.oauth.JsonUtil.asString;
 import static com.googlesource.gerrit.plugins.oauth.JsonUtil.isNull;
 
-import com.github.scribejava.core.builder.ServiceBuilder;
 import com.github.scribejava.core.model.OAuth2AccessToken;
 import com.github.scribejava.core.model.OAuthRequest;
 import com.github.scribejava.core.model.Response;
 import com.github.scribejava.core.model.Verb;
 import com.github.scribejava.core.oauth.OAuth20Service;
-import com.google.common.base.CharMatcher;
 import com.google.gerrit.extensions.auth.oauth.OAuthServiceProvider;
 import com.google.gerrit.extensions.auth.oauth.OAuthToken;
 import com.google.gerrit.extensions.auth.oauth.OAuthUserInfo;
 import com.google.gerrit.extensions.auth.oauth.OAuthVerifier;
-import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.config.PluginConfig;
 import com.google.gson.JsonElement;
 import com.google.gson.JsonObject;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.ProvisionException;
 import com.google.inject.Singleton;
 import com.googlesource.gerrit.plugins.oauth.InitOAuth;
+import com.googlesource.gerrit.plugins.oauth.OAuth20ServiceFactory;
 import com.googlesource.gerrit.plugins.oauth.OAuthPluginConfigFactory;
 import com.googlesource.gerrit.plugins.oauth.OAuthServiceProviderConfig;
 import com.googlesource.gerrit.plugins.oauth.OAuthServiceProviderExternalIdScheme;
@@ -61,9 +58,8 @@
 
   @Inject
   TuleapOAuthService(
-      OAuthPluginConfigFactory cfgFactory, @CanonicalWebUrl Provider<String> urlProvider) {
+      OAuthPluginConfigFactory cfgFactory, OAuth20ServiceFactory oauth20ServiceFactory) {
     PluginConfig cfg = cfgFactory.create(PROVIDER_NAME);
-    String canonicalWebUrl = CharMatcher.is('/').trimTrailingFrom(urlProvider.get()) + "/";
 
     rootUrl = cfg.getString(InitOAuth.ROOT_URL);
     if (!URI.create(rootUrl).isAbsolute()) {
@@ -72,11 +68,8 @@
     serviceName = cfg.getString(InitOAuth.SERVICE_NAME, "Tuleap");
 
     service =
-        new ServiceBuilder(cfg.getString(InitOAuth.CLIENT_ID))
-            .apiSecret(cfg.getString(InitOAuth.CLIENT_SECRET))
-            .callback(canonicalWebUrl + "oauth")
-            .defaultScope("openid profile email")
-            .build(new TuleapApi(rootUrl));
+        oauth20ServiceFactory.create(PROVIDER_NAME, new TuleapApi(rootUrl), "openid profile email");
+
     extIdScheme = OAuthServiceProviderExternalIdScheme.create(PROVIDER_NAME);
   }
 
diff --git a/src/test/java/com/googlesource/gerrit/plugins/oauth/cognito/CognitoOAuthServiceTest.java b/src/test/java/com/googlesource/gerrit/plugins/oauth/cognito/CognitoOAuthServiceTest.java
index 60ee64b..4ad0ac2 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/oauth/cognito/CognitoOAuthServiceTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/oauth/cognito/CognitoOAuthServiceTest.java
@@ -25,8 +25,8 @@
 import com.google.gerrit.extensions.auth.oauth.OAuthToken;
 import com.google.gerrit.extensions.auth.oauth.OAuthUserInfo;
 import com.google.gerrit.server.config.PluginConfig;
-import com.google.inject.Provider;
 import com.googlesource.gerrit.plugins.oauth.InitOAuth;
+import com.googlesource.gerrit.plugins.oauth.OAuth20ServiceFactory;
 import com.googlesource.gerrit.plugins.oauth.OAuthPluginConfigFactory;
 import java.lang.reflect.Field;
 import javax.servlet.http.HttpServletResponse;
@@ -42,7 +42,6 @@
   // Mocks for constructor dependencies of CognitoOAuthService
   @Mock private OAuthPluginConfigFactory mockConfigFactory;
   @Mock private PluginConfig mockPluginConfig;
-  @Mock private Provider<String> mockUrlProvider;
 
   // This is the ScribeJava service we want to mock
   @Mock private OAuth20Service mockScribeOAuthService;
@@ -69,9 +68,6 @@
     // Mock the PluginConfigFactory to return our mockPluginConfig
     when(mockConfigFactory.create(CognitoOAuthService.PROVIDER_NAME)).thenReturn(mockPluginConfig);
 
-    // Mock the CanonicalWebUrl provider
-    when(mockUrlProvider.get()).thenReturn(TEST_CANONICAL_WEB_URL);
-
     // Configure the mockPluginConfig with necessary values for CognitoOAuthService
     // constructor
     when(mockPluginConfig.getString(InitOAuth.ROOT_URL)).thenReturn(TEST_COGNITO_ROOT_URL);
@@ -95,8 +91,10 @@
     when(mockPluginConfig.getBoolean(InitOAuth.LINK_TO_EXISTING_GERRIT_ACCOUNT, false))
         .thenReturn(linkExistingGerritAccounts);
 
+    OAuth20ServiceFactory serviceFactory =
+        new OAuth20ServiceFactory(mockConfigFactory, TEST_CANONICAL_WEB_URL);
     CognitoOAuthService serviceInstance =
-        new CognitoOAuthService(mockConfigFactory, mockUrlProvider);
+        new CognitoOAuthService(mockConfigFactory, serviceFactory);
 
     // Replace the internal OAuth20Service with our mock using reflection.
     Field serviceField = CognitoOAuthService.class.getDeclaredField("service");
diff --git a/src/test/java/com/googlesource/gerrit/plugins/oauth/github/GithubApiUrlTest.java b/src/test/java/com/googlesource/gerrit/plugins/oauth/github/GithubApiUrlTest.java
index 73cbce1..94b2594 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/oauth/github/GithubApiUrlTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/oauth/github/GithubApiUrlTest.java
@@ -21,8 +21,8 @@
 import com.google.common.base.Strings;
 import com.google.gerrit.extensions.auth.oauth.OAuthServiceProvider;
 import com.google.gerrit.server.config.PluginConfig;
-import com.google.inject.Provider;
 import com.googlesource.gerrit.plugins.oauth.InitOAuth;
+import com.googlesource.gerrit.plugins.oauth.OAuth20ServiceFactory;
 import com.googlesource.gerrit.plugins.oauth.OAuthPluginConfigFactory;
 import java.net.URLEncoder;
 import java.nio.charset.StandardCharsets;
@@ -35,10 +35,9 @@
 @RunWith(MockitoJUnitRunner.class)
 public class GithubApiUrlTest {
   private static final String PLUGIN_NAME = "gerrit-oauth-provider";
-  private static final String CANONICAL_URL = "https://localhost";
+  private static final String CANONICAL_URL = "https://localhost/";
   private static final String TEST_CLIENT_ID = "test_client_id";
 
-  @Mock private Provider<String> urlProviderMock;
   @Mock OAuthPluginConfigFactory oauthPluginConfigFactoryMock;
 
   private OAuthServiceProvider getGithubOAuthProvider(String rootUrl) {
@@ -49,10 +48,12 @@
     }
     pluginConfig.setString(InitOAuth.CLIENT_ID, TEST_CLIENT_ID);
     pluginConfig.setString(InitOAuth.CLIENT_SECRET, "secret");
-    when(urlProviderMock.get()).thenReturn(CANONICAL_URL);
     when(oauthPluginConfigFactoryMock.create(GitHubOAuthService.PROVIDER_NAME))
         .thenReturn(pluginConfig.asPluginConfig());
-    return new GitHubOAuthService(oauthPluginConfigFactoryMock, urlProviderMock);
+
+    OAuth20ServiceFactory serviceFactory =
+        new OAuth20ServiceFactory(oauthPluginConfigFactoryMock, CANONICAL_URL);
+    return new GitHubOAuthService(oauthPluginConfigFactoryMock, serviceFactory);
   }
 
   private String getExpectedUrl(String rootUrl) throws Exception {
@@ -65,7 +66,7 @@
         rootUrl,
         TEST_CLIENT_ID,
         URLEncoder.encode(CANONICAL_URL, StandardCharsets.UTF_8.name()),
-        URLEncoder.encode("/oauth", StandardCharsets.UTF_8.name()),
+        URLEncoder.encode("oauth", StandardCharsets.UTF_8.name()),
         URLEncoder.encode(GitHubOAuthService.SCOPE, StandardCharsets.UTF_8.name()));
   }