Merge branch 'stable-2.13' into stable-2.14

* stable-2.13:
  GoogleOAuthService: Stop using deprecated Google+ userinfo endpoint

Change-Id: If45593d6b43d1760cf5e921a49db302bd7ae2408
diff --git a/.bazelignore b/.bazelignore
new file mode 100644
index 0000000..30f1613
--- /dev/null
+++ b/.bazelignore
@@ -0,0 +1 @@
+eclipse-out
diff --git a/.bazelrc b/.bazelrc
new file mode 100644
index 0000000..4ed16cf
--- /dev/null
+++ b/.bazelrc
@@ -0,0 +1,2 @@
+build --workspace_status_command=./tools/workspace-status.sh
+test --build_tests_only
diff --git a/.buckconfig b/.buckconfig
deleted file mode 100644
index 74f7ad9..0000000
--- a/.buckconfig
+++ /dev/null
@@ -1,13 +0,0 @@
-[alias]
-  oauth = //:oauth
-  plugin = //:oauth
-
-[java]
-  src_roots = java, resources
-
-[project]
-  ignore = .git
-
-[cache]
-  mode = dir
-  dir = buck-out/cache
diff --git a/.buckversion b/.buckversion
deleted file mode 120000
index 6203e53..0000000
--- a/.buckversion
+++ /dev/null
@@ -1 +0,0 @@
-bucklets/buckversion
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 038506e..b6af1fb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,10 @@
-/.buckd
-/buck-out
 /.classpath
 /.project
-/.settings/org.maven.ide.eclipse.prefs
-/.settings/org.eclipse.m2e.core.prefs
+/.settings
+/bazel-bin
+/bazel-genfiles
+/bazel-oauth
+/bazel-out
+/bazel-testlogs
+/eclipse-out
+/.idea
diff --git a/.gitmodules b/.gitmodules
deleted file mode 100644
index f088c56..0000000
--- a/.gitmodules
+++ /dev/null
@@ -1,3 +0,0 @@
-[submodule "bucklets"]
-	path = bucklets
-	url = https://github.com/davido/bucklets
diff --git a/.settings/org.eclipse.core.runtime.prefs b/.settings/org.eclipse.core.runtime.prefs
new file mode 100644
index 0000000..8667cfd
--- /dev/null
+++ b/.settings/org.eclipse.core.runtime.prefs
@@ -0,0 +1,3 @@
+#Tue Sep 02 16:59:24 PDT 2008
+eclipse.preferences.version=1
+line.separator=\n
diff --git a/.travis.yml b/.travis.yml
index 5405db8..3209482 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -13,26 +13,37 @@
 # limitations under the License.
 #
 
+dist: trusty
+sudo: required
 language: java
-sudo: false
-addons:
-  apt:
-    packages:
-      - oracle-java8-installer
-jdk: oraclejdk8
 
-install:
-  - cd ..
-  - mkdir -p build
-  - cd build
-  - git clone https://github.com/facebook/buck
-  - cd buck
-  - ant
-  - export PATH=$PATH:$TRAVIS_BUILD_DIR/../build/buck/bin
-  - cd $TRAVIS_BUILD_DIR
+matrix:
+  include:
+    - os: linux
+      jdk: oraclejdk8
+    - os: osx
+      osx_image: xcode8
+
+install: true
+
+before_install:
+  - OS=linux
+  - ARCH=x86_64
+  - V=0.11.0
+  - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then OS=darwin; fi
+  - GH_BASE="https://github.com/bazelbuild/bazel/releases/download/$V"
+  - GH_ARTIFACT="bazel-$V-installer-$OS-$ARCH.sh"
+  - URL="$GH_BASE/$GH_ARTIFACT"
+  - echo $URL
+  - wget -O install.sh $URL
+  - chmod +x install.sh
+  - ./install.sh --user
+  - rm -f install.sh
 
 script:
-  - buck build plugin > buck.log
+  - |
+    bazel build --verbose_failures :all
+    bazel test --test_output=errors :oauth_tests
 
-after_failure:
-  - cat buck.log
+notifications:
+  email: false
diff --git a/.watchmanconfig b/.watchmanconfig
deleted file mode 120000
index 65be7a7..0000000
--- a/.watchmanconfig
+++ /dev/null
@@ -1 +0,0 @@
-bucklets/watchmanconfig
\ No newline at end of file
diff --git a/BUCK b/BUCK
deleted file mode 100644
index ac55bc6..0000000
--- a/BUCK
+++ /dev/null
@@ -1,37 +0,0 @@
-include_defs('//bucklets/gerrit_plugin.bucklet')
-include_defs('//bucklets/maven_jar.bucklet')
-define_license('scribe')
-
-gerrit_plugin(
-  name = 'oauth',
-  srcs = glob(['src/main/java/**/*.java']),
-  resources = glob(['src/main/resources/**/*']),
-  manifest_entries = [
-    'Gerrit-PluginName: oauth',
-    'Gerrit-HttpModule: com.googlesource.gerrit.plugins.oauth.HttpModule',
-    'Gerrit-InitStep: com.googlesource.gerrit.plugins.oauth.InitOAuth',
-    'Implementation-Title: Gerrit OAuth authentication provider',
-    'Implementation-URL: https://github.com/davido/gerrit-oauth-provider',
-  ],
-  deps = [
-    ':scribe'
-  ],
-  provided_deps = [
-    '//lib:guava',
-    '//lib:gson',
-    '//lib/commons:codec',
-  ],
-)
-
-java_library(
-  name = 'classpath',
-  deps = [':oauth__plugin'],
-)
-
-maven_jar(
-  name = 'scribe',
-  id = 'org.scribe:scribe:1.3.7',
-  sha1 = '583921bed46635d9f529ef5f14f7c9e83367bc6e',
-  license = 'scribe',
-  local_license = True,
-)
diff --git a/BUILD b/BUILD
new file mode 100644
index 0000000..ea1fc3a
--- /dev/null
+++ b/BUILD
@@ -0,0 +1,44 @@
+load("//tools/bzl:junit.bzl", "junit_tests")
+load(
+    "//tools/bzl:plugin.bzl",
+    "PLUGIN_DEPS",
+    "PLUGIN_TEST_DEPS",
+    "gerrit_plugin",
+)
+
+gerrit_plugin(
+    name = "oauth",
+    srcs = glob(["src/main/java/**/*.java"]),
+    manifest_entries = [
+        "Gerrit-PluginName: gerrit-oauth-provider",
+        "Gerrit-Module: com.googlesource.gerrit.plugins.oauth.Module",
+        "Gerrit-HttpModule: com.googlesource.gerrit.plugins.oauth.HttpModule",
+        "Gerrit-InitStep: com.googlesource.gerrit.plugins.oauth.InitOAuth",
+        "Implementation-Title: Gerrit OAuth authentication provider",
+        "Implementation-URL: https://github.com/davido/gerrit-oauth-provider",
+    ],
+    resources = glob(["src/main/resources/**/*"]),
+    deps = [
+        "@commons-codec//jar:neverlink",
+        "@scribe//jar",
+    ],
+)
+
+junit_tests(
+    name = "oauth_tests",
+    srcs = glob(["src/test/java/**/*.java"]),
+    tags = ["oauth"],
+    deps = [
+        ":oauth__plugin_test_deps",
+    ],
+)
+
+java_library(
+    name = "oauth__plugin_test_deps",
+    testonly = 1,
+    visibility = ["//visibility:public"],
+    exports = PLUGIN_DEPS + PLUGIN_TEST_DEPS + [
+        ":oauth__plugin",
+        "@scribe//jar",
+    ],
+)
diff --git a/README.md b/README.md
index de690ff..9056d47 100644
--- a/README.md
+++ b/README.md
@@ -9,8 +9,13 @@
 
 * [Bitbucket](https://confluence.atlassian.com/bitbucket/oauth-on-bitbucket-cloud-238027431.html)
 * [CAS](https://www.apereo.org/projects/cas)
+* [CoreOS Dex](https://github.com/coreos/dex)
+* [Facebook](https://developers.facebook.com/docs/facebook-login)
 * [GitHub](https://developer.github.com/v3/oauth/)
+* [GitLab](https://about.gitlab.com/)
 * [Google](https://developers.google.com/identity/protocols/OAuth2)
+* [Keycloak](http://www.keycloak.org/)
+* [Office365](https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-v2-protocols)
 
 See the [Wiki](https://github.com/davido/gerrit-oauth-provider/wiki) what it can do for you.
 
@@ -22,19 +27,20 @@
 Build
 -----
 
-To build the plugin, install [Buck](http://facebook.github.io/buck/setup/install.html)
-and run the following:
+To build the plugin with Bazel, install
+[Bazel](https://bazel.build/versions/master/docs/install.html) and run the
+following:
 
 ```
-  git clone --recursive https://github.com/davido/gerrit-oauth-provider.git
-  cd gerrit-oauth-provider && buck build plugin
+  git clone https://gerrit.googlesource.com/plugins/oauth
+  cd oauth && bazel build oauth
 ```
 
 Install
 -------
 
-Copy the `buck-out/gen/gerrit-oauth-provider.jar` to
-`$gerit_site/plugins` and re-run init to configure it:
+Copy the `bazel-genfiles/oauth.jar` to
+`$gerrit_site/plugins` and re-run init to configure it:
 
 ```
   java -jar gerrit.war init -d <site>
diff --git a/VERSION b/VERSION
deleted file mode 100644
index 52ac3af..0000000
--- a/VERSION
+++ /dev/null
@@ -1,4 +0,0 @@
-# Used by BUCK to include "Implementation-Version" in plugin Manifest.
-# If this file doesn't exist the output of 'git describe' is used
-# instead.
-PLUGIN_VERSION = '2.13.2'
diff --git a/WORKSPACE b/WORKSPACE
new file mode 100644
index 0000000..c03579b
--- /dev/null
+++ b/WORKSPACE
@@ -0,0 +1,29 @@
+workspace(name = "com_github_davido_gerrit_oauth_provider")
+
+load("//:bazlets.bzl", "load_bazlets")
+
+load_bazlets(
+    commit = "2c39029a585bd1d5b785150948f162730f7b7e42",
+    #local_path = "/home/<user>/projects/bazlets",
+)
+
+# Snapshot Plugin API
+#load(
+#    "@com_googlesource_gerrit_bazlets//:gerrit_api_maven_local.bzl",
+#    "gerrit_api_maven_local",
+#)
+
+# Load snapshot Plugin API
+#gerrit_api_maven_local()
+
+# Release Plugin API
+load(
+    "@com_googlesource_gerrit_bazlets//:gerrit_api.bzl",
+    "gerrit_api",
+)
+
+gerrit_api()
+
+load(":external_plugin_deps.bzl", "external_plugin_deps")
+
+external_plugin_deps(omit_commons_codec = False)
diff --git a/bazlets.bzl b/bazlets.bzl
new file mode 100644
index 0000000..f089af4
--- /dev/null
+++ b/bazlets.bzl
@@ -0,0 +1,18 @@
+load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository")
+
+NAME = "com_googlesource_gerrit_bazlets"
+
+def load_bazlets(
+        commit,
+        local_path = None):
+    if not local_path:
+        git_repository(
+            name = NAME,
+            remote = "https://gerrit.googlesource.com/bazlets",
+            commit = commit,
+        )
+    else:
+        native.local_repository(
+            name = NAME,
+            path = local_path,
+        )
diff --git a/bucklets b/bucklets
deleted file mode 160000
index d2936a4..0000000
--- a/bucklets
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit d2936a48fc559e90b66e83de7ff163202e75486b
diff --git a/external_plugin_deps.bzl b/external_plugin_deps.bzl
new file mode 100644
index 0000000..e560344
--- /dev/null
+++ b/external_plugin_deps.bzl
@@ -0,0 +1,14 @@
+load("//tools/bzl:maven_jar.bzl", "maven_jar")
+
+def external_plugin_deps(omit_commons_codec = True):
+    maven_jar(
+        name = "scribe",
+        artifact = "org.scribe:scribe:1.3.7",
+        sha1 = "583921bed46635d9f529ef5f14f7c9e83367bc6e",
+    )
+    if not omit_commons_codec:
+        maven_jar(
+            name = "commons-codec",
+            artifact = "commons-codec:commons-codec:1.4",
+            sha1 = "4216af16d38465bbab0f3dff8efa14204f7a399a",
+        )
diff --git a/lib/BUCK b/lib/BUCK
deleted file mode 100644
index ab803a7..0000000
--- a/lib/BUCK
+++ /dev/null
@@ -1,15 +0,0 @@
-include_defs('//bucklets/maven_jar.bucklet')
-
-maven_jar(
-  name = 'gson',
-  id = 'com.google.code.gson:gson:2.3.1',
-  sha1 = 'ecb6e1f8e4b0e84c4b886c2f14a1500caf309757',
-  license = 'Apache2.0',
-)
-
-maven_jar(
-  name = 'guava',
-  id = 'com.google.guava:guava:18.0',
-  sha1 = 'cce0823396aa693798f8882e64213b1772032b09',
-  license = 'Apache2.0',
-)
diff --git a/lib/commons/BUCK b/lib/commons/BUCK
deleted file mode 100644
index 7aafa82..0000000
--- a/lib/commons/BUCK
+++ /dev/null
@@ -1,9 +0,0 @@
-include_defs('//bucklets/maven_jar.bucklet')
-
-maven_jar(
-  name = 'codec',
-  id = 'commons-codec:commons-codec:1.4',
-  sha1 = '4216af16d38465bbab0f3dff8efa14204f7a399a',
-  license = 'Apache2.0',
-  exclude = ['META-INF/LICENSE.txt', 'META-INF/NOTICE.txt'],
-)
diff --git a/lib/gerrit/BUCK b/lib/gerrit/BUCK
deleted file mode 100644
index 1ae48ff..0000000
--- a/lib/gerrit/BUCK
+++ /dev/null
@@ -1,13 +0,0 @@
-include_defs('//bucklets/maven_jar.bucklet')
-
-VER = '2.13.2'
-REPO = MAVEN_CENTRAL
-
-maven_jar(
-  name = 'plugin-api',
-  id = 'com.google.gerrit:gerrit-plugin-api:' + VER,
-  sha1 = '3cdeb17c2b0f945e71135ef6abe5a1db59b9d313',
-  license = 'Apache2.0',
-  attach_source = False,
-  repository = REPO,
-)
diff --git a/src/main/java/com/googlesource/gerrit/plugins/oauth/BitbucketApi.java b/src/main/java/com/googlesource/gerrit/plugins/oauth/BitbucketApi.java
index 52a9280..a3fff4a 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/oauth/BitbucketApi.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/oauth/BitbucketApi.java
@@ -14,6 +14,12 @@
 
 package com.googlesource.gerrit.plugins.oauth;
 
+import static com.google.gerrit.server.OutputFormat.JSON;
+import static java.lang.String.format;
+import static javax.servlet.http.HttpServletResponse.SC_OK;
+import static org.scribe.model.OAuthConstants.ACCESS_TOKEN;
+import static org.scribe.model.OAuthConstants.CODE;
+
 import com.google.common.io.BaseEncoding;
 import com.google.gson.JsonElement;
 import com.google.gson.JsonObject;
@@ -28,12 +34,6 @@
 import org.scribe.model.Verifier;
 import org.scribe.oauth.OAuthService;
 
-import static com.google.gerrit.server.OutputFormat.JSON;
-import static java.lang.String.format;
-import static javax.servlet.http.HttpServletResponse.SC_OK;
-import static org.scribe.model.OAuthConstants.ACCESS_TOKEN;
-import static org.scribe.model.OAuthConstants.CODE;
-
 public class BitbucketApi extends DefaultApi20 {
 
   private static final String AUTHORIZE_URL =
@@ -41,8 +41,7 @@
   private static final String ACCESS_TOKEN_ENDPOINT =
       "https://bitbucket.org/site/oauth2/access_token";
 
-  public BitbucketApi() {
-  }
+  public BitbucketApi() {}
 
   @Override
   public String getAuthorizationUrl(OAuthConfig config) {
@@ -85,8 +84,8 @@
 
     @Override
     public Token getAccessToken(Token token, Verifier verifier) {
-      OAuthRequest request = new OAuthRequest(api.getAccessTokenVerb(),
-          api.getAccessTokenEndpoint());
+      OAuthRequest request =
+          new OAuthRequest(api.getAccessTokenVerb(), api.getAccessTokenEndpoint());
       request.addHeader("Authorization", prepareAuthorizationHeaderValue());
       request.addBodyParameter(GRANT_TYPE, GRANT_TYPE_VALUE);
       request.addBodyParameter(CODE, verifier.getValue());
@@ -94,11 +93,12 @@
       if (response.getCode() == SC_OK) {
         Token t = api.getAccessTokenExtractor().extract(response.getBody());
         return new Token(t.getToken(), config.getApiSecret());
-      } else {
-        throw new OAuthException(
-            String.format("Error response received: %s, HTTP status: %s",
-                response.getBody(), response.getCode()));
       }
+
+      throw new OAuthException(
+          String.format(
+              "Error response received: %s, HTTP status: %s",
+              response.getBody(), response.getCode()));
     }
 
     private String prepareAuthorizationHeaderValue() {
@@ -129,8 +129,7 @@
     }
   }
 
-  private static final class BitbucketTokenExtractor
-      implements AccessTokenExtractor {
+  private static final class BitbucketTokenExtractor implements AccessTokenExtractor {
 
     @Override
     public Token extract(String response) {
@@ -139,15 +138,13 @@
         JsonObject jsonObject = json.getAsJsonObject();
         JsonElement id = jsonObject.get(ACCESS_TOKEN);
         if (id == null || id.isJsonNull()) {
-          throw new OAuthException(
-              "Response doesn't contain 'access_token' field");
+          throw new OAuthException("Response doesn't contain 'access_token' field");
         }
         JsonElement accessToken = jsonObject.get(ACCESS_TOKEN);
         return new Token(accessToken.getAsString(), "");
-      } else {
-        throw new OAuthException(
-            String.format("Invalid JSON '%s': not a JSON Object", json));
       }
+
+      throw new OAuthException(String.format("Invalid JSON '%s': not a JSON Object", json));
     }
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/oauth/BitbucketOAuthService.java b/src/main/java/com/googlesource/gerrit/plugins/oauth/BitbucketOAuthService.java
index 80bbda4..e600067 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/oauth/BitbucketOAuthService.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/oauth/BitbucketOAuthService.java
@@ -14,6 +14,10 @@
 
 package com.googlesource.gerrit.plugins.oauth;
 
+import static com.google.gerrit.server.OutputFormat.JSON;
+import static javax.servlet.http.HttpServletResponse.SC_OK;
+import static org.slf4j.LoggerFactory.getLogger;
+
 import com.google.common.base.CharMatcher;
 import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.extensions.auth.oauth.OAuthServiceProvider;
@@ -28,6 +32,7 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
+import java.io.IOException;
 import org.scribe.builder.ServiceBuilder;
 import org.scribe.model.OAuthRequest;
 import org.scribe.model.Response;
@@ -37,35 +42,31 @@
 import org.scribe.oauth.OAuthService;
 import org.slf4j.Logger;
 
-import java.io.IOException;
-
-import static com.google.gerrit.server.OutputFormat.JSON;
-import static javax.servlet.http.HttpServletResponse.SC_OK;
-import static org.slf4j.LoggerFactory.getLogger;
-
 @Singleton
 public class BitbucketOAuthService implements OAuthServiceProvider {
   private static final Logger log = getLogger(BitbucketOAuthService.class);
   static final String CONFIG_SUFFIX = "-bitbucket-oauth";
-  private static final String PROTECTED_RESOURCE_URL =
-      "https://bitbucket.org/api/1.0/user/";
+  private static final String BITBUCKET_PROVIDER_PREFIX = "bitbucket-oauth:";
+  private static final String PROTECTED_RESOURCE_URL = "https://bitbucket.org/api/1.0/user/";
+  private final boolean fixLegacyUserId;
   private final OAuthService service;
 
   @Inject
-  BitbucketOAuthService(PluginConfigFactory cfgFactory,
+  BitbucketOAuthService(
+      PluginConfigFactory cfgFactory,
       @PluginName String pluginName,
       @CanonicalWebUrl Provider<String> urlProvider) {
-    PluginConfig cfg =
-        cfgFactory.getFromGerritConfig(pluginName + CONFIG_SUFFIX);
+    PluginConfig cfg = cfgFactory.getFromGerritConfig(pluginName + CONFIG_SUFFIX);
 
-    String canonicalWebUrl =
-        CharMatcher.is('/').trimTrailingFrom(urlProvider.get()) + "/";
-
-    service = new ServiceBuilder().provider(BitbucketApi.class)
-        .apiKey(cfg.getString(InitOAuth.CLIENT_ID))
-        .apiSecret(cfg.getString(InitOAuth.CLIENT_SECRET))
-        .callback(canonicalWebUrl + "oauth")
-        .build();
+    String canonicalWebUrl = CharMatcher.is('/').trimTrailingFrom(urlProvider.get()) + "/";
+    fixLegacyUserId = cfg.getBoolean(InitOAuth.FIX_LEGACY_USER_ID, false);
+    service =
+        new ServiceBuilder()
+            .provider(BitbucketApi.class)
+            .apiKey(cfg.getString(InitOAuth.CLIENT_ID))
+            .apiSecret(cfg.getString(InitOAuth.CLIENT_SECRET))
+            .callback(canonicalWebUrl + "oauth")
+            .build();
   }
 
   @Override
@@ -75,11 +76,12 @@
     service.signRequest(t, request);
     Response response = request.send();
     if (response.getCode() != SC_OK) {
-      throw new IOException(String.format("Status %s (%s) for request %s",
-          response.getCode(), response.getBody(), request.getUrl()));
+      throw new IOException(
+          String.format(
+              "Status %s (%s) for request %s",
+              response.getCode(), response.getBody(), request.getUrl()));
     }
-    JsonElement userJson =
-        JSON.newGson().fromJson(response.getBody(), JsonElement.class);
+    JsonElement userJson = JSON.newGson().fromJson(response.getBody(), JsonElement.class);
     if (log.isDebugEnabled()) {
       log.debug("User info response: {}", response.getBody());
     }
@@ -93,14 +95,15 @@
       String username = usernameElement.getAsString();
 
       JsonElement displayName = jsonObject.get("display_name");
-      return new OAuthUserInfo(username, username, null,
-          displayName == null || displayName.isJsonNull() ? null
-              : displayName.getAsString(),
-          null);
-    } else {
-      throw new IOException(
-          String.format("Invalid JSON '%s': not a JSON Object", userJson));
+      return new OAuthUserInfo(
+          BITBUCKET_PROVIDER_PREFIX + username,
+          username,
+          null,
+          displayName == null || displayName.isJsonNull() ? null : displayName.getAsString(),
+          fixLegacyUserId ? username : null);
     }
+
+    throw new IOException(String.format("Invalid JSON '%s': not a JSON Object", userJson));
   }
 
   @Override
diff --git a/src/main/java/com/googlesource/gerrit/plugins/oauth/CasApi.java b/src/main/java/com/googlesource/gerrit/plugins/oauth/CasApi.java
index 6a96c26..76d4011 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/oauth/CasApi.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/oauth/CasApi.java
@@ -16,11 +16,13 @@
 
 import org.scribe.builder.api.DefaultApi20;
 import org.scribe.model.OAuthConfig;
+import org.scribe.model.Verb;
+import org.scribe.oauth.OAuthService;
 import org.scribe.utils.OAuthEncoder;
 
 public class CasApi extends DefaultApi20 {
   private static final String AUTHORIZE_URL =
-      "%s/oauth2.0/authorize?client_id=%s&redirect_uri=%s";
+      "%s/oauth2.0/authorize?response_type=code&client_id=%s&redirect_uri=%s";
 
   private final String rootUrl;
 
@@ -35,7 +37,17 @@
 
   @Override
   public String getAuthorizationUrl(OAuthConfig config) {
-    return String.format(AUTHORIZE_URL, rootUrl, config.getApiKey(),
-        OAuthEncoder.encode(config.getCallback()));
+    return String.format(
+        AUTHORIZE_URL, rootUrl, config.getApiKey(), OAuthEncoder.encode(config.getCallback()));
+  }
+
+  @Override
+  public Verb getAccessTokenVerb() {
+    return Verb.POST;
+  }
+
+  @Override
+  public OAuthService createService(OAuthConfig config) {
+    return new OAuth20ServiceImpl(this, config);
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/oauth/CasOAuthService.java b/src/main/java/com/googlesource/gerrit/plugins/oauth/CasOAuthService.java
index 2f92e72..6c5977d 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/oauth/CasOAuthService.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/oauth/CasOAuthService.java
@@ -30,7 +30,8 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
-
+import java.io.IOException;
+import javax.servlet.http.HttpServletResponse;
 import org.scribe.builder.ServiceBuilder;
 import org.scribe.model.OAuthRequest;
 import org.scribe.model.Response;
@@ -41,51 +42,48 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.io.IOException;
-
-import javax.servlet.http.HttpServletResponse;
-
 @Singleton
 class CasOAuthService implements OAuthServiceProvider {
-  private static final Logger log =
-      LoggerFactory.getLogger(CasOAuthService.class);
+  private static final Logger log = LoggerFactory.getLogger(CasOAuthService.class);
   static final String CONFIG_SUFFIX = "-cas-oauth";
-  private static final String PROTECTED_RESOURCE_URL =
-      "%s/oauth2.0/profile";
+  private static final String CAS_PROVIDER_PREFIX = "cas-oauth:";
+  private static final String PROTECTED_RESOURCE_URL = "%s/oauth2.0/profile";
 
   private final String rootUrl;
+  private final boolean fixLegacyUserId;
   private final OAuthService service;
 
   @Inject
-  CasOAuthService(PluginConfigFactory cfgFactory,
+  CasOAuthService(
+      PluginConfigFactory cfgFactory,
       @PluginName String pluginName,
       @CanonicalWebUrl Provider<String> urlProvider) {
-    PluginConfig cfg = cfgFactory.getFromGerritConfig(
-        pluginName + CONFIG_SUFFIX);
+    PluginConfig cfg = cfgFactory.getFromGerritConfig(pluginName + CONFIG_SUFFIX);
     rootUrl = cfg.getString(InitOAuth.ROOT_URL);
-    String canonicalWebUrl = CharMatcher.is('/').trimTrailingFrom(
-        urlProvider.get()) + "/";
-    service = new ServiceBuilder()
-        .provider(new CasApi(rootUrl))
-        .apiKey(cfg.getString(InitOAuth.CLIENT_ID))
-        .apiSecret(cfg.getString(InitOAuth.CLIENT_SECRET))
-        .callback(canonicalWebUrl + "oauth")
-        .build();
+    String canonicalWebUrl = CharMatcher.is('/').trimTrailingFrom(urlProvider.get()) + "/";
+    fixLegacyUserId = cfg.getBoolean(InitOAuth.FIX_LEGACY_USER_ID, false);
+    service =
+        new ServiceBuilder()
+            .provider(new CasApi(rootUrl))
+            .apiKey(cfg.getString(InitOAuth.CLIENT_ID))
+            .apiSecret(cfg.getString(InitOAuth.CLIENT_SECRET))
+            .callback(canonicalWebUrl + "oauth")
+            .build();
   }
 
   @Override
   public OAuthUserInfo getUserInfo(OAuthToken token) throws IOException {
-    final String protectedResourceUrl =
-        String.format(PROTECTED_RESOURCE_URL, rootUrl);
+    final String protectedResourceUrl = String.format(PROTECTED_RESOURCE_URL, rootUrl);
     OAuthRequest request = new OAuthRequest(Verb.GET, protectedResourceUrl);
-    Token t =
-        new Token(token.getToken(), token.getSecret(), token.getRaw());
+    Token t = new Token(token.getToken(), token.getSecret(), token.getRaw());
     service.signRequest(t, request);
 
     Response response = request.send();
     if (response.getCode() != HttpServletResponse.SC_OK) {
-      throw new IOException(String.format("Status %s (%s) for request %s",
-          response.getCode(), response.getBody(), request.getUrl()));
+      throw new IOException(
+          String.format(
+              "Status %s (%s) for request %s",
+              response.getCode(), response.getBody(), request.getUrl()));
     }
 
     if (log.isDebugEnabled()) {
@@ -93,54 +91,58 @@
     }
 
     JsonElement userJson =
-        OutputFormat.JSON.newGson().fromJson(response.getBody(),
-            JsonElement.class);
+        OutputFormat.JSON.newGson().fromJson(response.getBody(), JsonElement.class);
     if (!userJson.isJsonObject()) {
-      throw new IOException(String.format(
-          "Invalid JSON '%s': not a JSON Object", userJson));
+      throw new IOException(String.format("Invalid JSON '%s': not a JSON Object", userJson));
     }
     JsonObject jsonObject = userJson.getAsJsonObject();
 
     JsonElement id = jsonObject.get("id");
     if (id == null || id.isJsonNull()) {
-      throw new IOException(String.format(
-          "Response doesn't contain %s field", "id"));
+      throw new IOException(String.format("CAS response missing id: %s", response.getBody()));
     }
 
     JsonElement attrListJson = jsonObject.get("attributes");
-    if (attrListJson == null || !attrListJson.isJsonArray()) {
-      throw new IOException(String.format(
-          "Invalid JSON '%s': not a JSON Array", attrListJson));
+    if (attrListJson == null) {
+      throw new IOException(
+          String.format("CAS response missing attributes: %s", response.getBody()));
     }
 
     String email = null, name = null, login = null;
-    JsonArray attrJson = attrListJson.getAsJsonArray();
-    for (JsonElement elem : attrJson) {
-      if (elem == null || !elem.isJsonObject()) {
-        throw new IOException(String.format(
-            "Invalid JSON '%s': not a JSON Object", elem));
-      }
-      JsonObject obj = elem.getAsJsonObject();
 
-      String property = getStringElement(obj, "email");
-      if (property != null)
-        email = property;
-      property = getStringElement(obj, "name");
-      if (property != null)
-        name = property;
-      property = getStringElement(obj, "login");
-      if (property != null)
-        login = property;
+    if (attrListJson.isJsonArray()) {
+      // It is possible for CAS to be configured to not return any attributes (email, name, login),
+      // in which case,
+      // CAS returns an empty JSON object "attributes":{}, rather than "null" or an empty JSON array
+      // "attributes": []
+
+      JsonArray attrJson = attrListJson.getAsJsonArray();
+      for (JsonElement elem : attrJson) {
+        if (elem == null || !elem.isJsonObject()) {
+          throw new IOException(String.format("Invalid JSON '%s': not a JSON Object", elem));
+        }
+        JsonObject obj = elem.getAsJsonObject();
+
+        String property = getStringElement(obj, "email");
+        if (property != null) email = property;
+        property = getStringElement(obj, "name");
+        if (property != null) name = property;
+        property = getStringElement(obj, "login");
+        if (property != null) login = property;
+      }
     }
 
-    return new OAuthUserInfo(id.getAsString(), login, email, name, null);
+    return new OAuthUserInfo(
+        CAS_PROVIDER_PREFIX + id.getAsString(),
+        login,
+        email,
+        name,
+        fixLegacyUserId ? id.getAsString() : null);
   }
 
-  private String getStringElement(JsonObject o, String name)
-      throws IOException {
+  private String getStringElement(JsonObject o, String name) {
     JsonElement elem = o.get(name);
-    if (elem == null || elem.isJsonNull())
-      return null;
+    if (elem == null || elem.isJsonNull()) return null;
 
     return elem.getAsString();
   }
@@ -149,8 +151,7 @@
   public OAuthToken getAccessToken(OAuthVerifier rv) {
     Verifier vi = new Verifier(rv.getValue());
     Token to = service.getAccessToken(null, vi);
-    return new OAuthToken(to.getToken(),
-        to.getSecret(), to.getRawResponse());
+    return new OAuthToken(to.getToken(), to.getSecret(), to.getRawResponse());
   }
 
   @Override
diff --git a/src/main/java/com/googlesource/gerrit/plugins/oauth/DexApi.java b/src/main/java/com/googlesource/gerrit/plugins/oauth/DexApi.java
new file mode 100644
index 0000000..2386e24
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/oauth/DexApi.java
@@ -0,0 +1,65 @@
+// Copyright (C) 2017 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 org.scribe.builder.api.DefaultApi20;
+import org.scribe.extractors.AccessTokenExtractor;
+import org.scribe.extractors.JsonTokenExtractor;
+import org.scribe.model.OAuthConfig;
+import org.scribe.model.Verb;
+import org.scribe.oauth.OAuthService;
+import org.scribe.utils.OAuthEncoder;
+
+public class DexApi extends DefaultApi20 {
+
+  private static final String AUTHORIZE_URL =
+      "%s/dex/auth?client_id=%s&response_type=code&redirect_uri=%s&scope=%s";
+
+  private final String rootUrl;
+
+  public DexApi(String rootUrl) {
+    this.rootUrl = rootUrl;
+  }
+
+  @Override
+  public String getAuthorizationUrl(OAuthConfig config) {
+    return String.format(
+        AUTHORIZE_URL,
+        rootUrl,
+        config.getApiKey(),
+        OAuthEncoder.encode(config.getCallback()),
+        config.getScope().replaceAll(" ", "+"));
+  }
+
+  @Override
+  public String getAccessTokenEndpoint() {
+    return String.format("%s/dex/token", rootUrl);
+  }
+
+  @Override
+  public Verb getAccessTokenVerb() {
+    return Verb.POST;
+  }
+
+  @Override
+  public OAuthService createService(OAuthConfig config) {
+    return new OAuth20ServiceImpl(this, config);
+  }
+
+  @Override
+  public AccessTokenExtractor getAccessTokenExtractor() {
+    return new JsonTokenExtractor();
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/oauth/DexOAuthService.java b/src/main/java/com/googlesource/gerrit/plugins/oauth/DexOAuthService.java
new file mode 100644
index 0000000..ae1ca98
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/oauth/DexOAuthService.java
@@ -0,0 +1,137 @@
+// Copyright (C) 2017 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 static com.google.gerrit.server.OutputFormat.JSON;
+
+import com.google.common.base.CharMatcher;
+import com.google.common.base.Preconditions;
+import com.google.gerrit.extensions.annotations.PluginName;
+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.gerrit.server.config.PluginConfigFactory;
+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 java.io.IOException;
+import org.apache.commons.codec.binary.Base64;
+import org.scribe.builder.ServiceBuilder;
+import org.scribe.model.Token;
+import org.scribe.model.Verifier;
+import org.scribe.oauth.OAuthService;
+
+@Singleton
+public class DexOAuthService implements OAuthServiceProvider {
+
+  static final String CONFIG_SUFFIX = "-dex-oauth";
+  private static final String DEX_PROVIDER_PREFIX = "dex-oauth:";
+  private final OAuthService service;
+  private final String rootUrl;
+  private final String domain;
+  private final String serviceName;
+
+  @Inject
+  DexOAuthService(
+      PluginConfigFactory cfgFactory,
+      @PluginName String pluginName,
+      @CanonicalWebUrl Provider<String> urlProvider) {
+    PluginConfig cfg = cfgFactory.getFromGerritConfig(pluginName + CONFIG_SUFFIX);
+    String canonicalWebUrl = CharMatcher.is('/').trimTrailingFrom(urlProvider.get()) + "/";
+
+    rootUrl = cfg.getString(InitOAuth.ROOT_URL);
+    domain = cfg.getString(InitOAuth.DOMAIN, null);
+    serviceName = cfg.getString(InitOAuth.SERVICE_NAME, "Dex OAuth2");
+
+    service =
+        new ServiceBuilder()
+            .provider(new DexApi(rootUrl))
+            .apiKey(cfg.getString(InitOAuth.CLIENT_ID))
+            .apiSecret(cfg.getString(InitOAuth.CLIENT_SECRET))
+            .scope("openid profile email offline_access")
+            .callback(canonicalWebUrl + "oauth")
+            .build();
+  }
+
+  private String parseJwt(String input) {
+    String[] parts = input.split("\\.");
+    Preconditions.checkState(parts.length == 3);
+    Preconditions.checkNotNull(parts[1]);
+    return new String(Base64.decodeBase64(parts[1]));
+  }
+
+  @Override
+  public OAuthUserInfo getUserInfo(OAuthToken token) throws IOException {
+    JsonElement tokenJson = JSON.newGson().fromJson(token.getRaw(), JsonElement.class);
+    JsonObject tokenObject = tokenJson.getAsJsonObject();
+    JsonElement id_token = tokenObject.get("id_token");
+
+    JsonElement claimJson =
+        JSON.newGson().fromJson(parseJwt(id_token.getAsString()), JsonElement.class);
+
+    // Dex does not support basic profile currently (2017-09), extracting info
+    // from access token claim
+
+    JsonObject claimObject = claimJson.getAsJsonObject();
+    JsonElement emailElement = claimObject.get("email");
+    JsonElement nameElement = claimObject.get("name");
+    if (emailElement == null || emailElement.isJsonNull()) {
+      throw new IOException(String.format("Response doesn't contain email field"));
+    }
+    if (nameElement == null || nameElement.isJsonNull()) {
+      throw new IOException(String.format("Response doesn't contain name field"));
+    }
+    String email = emailElement.getAsString();
+    String name = nameElement.getAsString();
+    String username = email;
+    if (domain != null && domain.length() > 0) {
+      username = email.replace("@" + domain, "");
+    }
+
+    return new OAuthUserInfo(
+        DEX_PROVIDER_PREFIX + email /*externalId*/,
+        username /*username*/,
+        email /*email*/,
+        name /*displayName*/,
+        null /*claimedIdentity*/);
+  }
+
+  @Override
+  public OAuthToken getAccessToken(OAuthVerifier rv) {
+    Verifier vi = new Verifier(rv.getValue());
+    Token to = service.getAccessToken(null, vi);
+    return new OAuthToken(to.getToken(), to.getSecret(), to.getRawResponse());
+  }
+
+  @Override
+  public String getAuthorizationUrl() {
+    return service.getAuthorizationUrl(null);
+  }
+
+  @Override
+  public String getVersion() {
+    return service.getVersion();
+  }
+
+  @Override
+  public String getName() {
+    return serviceName;
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/oauth/DisabledOAuthLoginProvider.java b/src/main/java/com/googlesource/gerrit/plugins/oauth/DisabledOAuthLoginProvider.java
new file mode 100644
index 0000000..4a62e3d
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/oauth/DisabledOAuthLoginProvider.java
@@ -0,0 +1,38 @@
+// Copyright (C) 2017 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.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.extensions.auth.oauth.OAuthLoginProvider;
+import com.google.gerrit.extensions.auth.oauth.OAuthUserInfo;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.io.IOException;
+
+@Singleton
+class DisabledOAuthLoginProvider implements OAuthLoginProvider {
+  private final String pluginName;
+
+  @Inject
+  DisabledOAuthLoginProvider(@PluginName String pluginName) {
+    this.pluginName = pluginName;
+  }
+
+  @Override
+  public OAuthUserInfo login(String username, String secret) throws IOException {
+    throw new UnsupportedOperationException(
+        "git over oauth is not implemented by " + pluginName + " plugin");
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/oauth/Facebook2Api.java b/src/main/java/com/googlesource/gerrit/plugins/oauth/Facebook2Api.java
new file mode 100644
index 0000000..a547bfb
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/oauth/Facebook2Api.java
@@ -0,0 +1,25 @@
+// Copyright (C) 2018 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 org.scribe.builder.api.FacebookApi;
+import org.scribe.extractors.AccessTokenExtractor;
+
+public class Facebook2Api extends FacebookApi {
+  @Override
+  public AccessTokenExtractor getAccessTokenExtractor() {
+    return OAuth2AccessTokenJsonExtractor.instance();
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/oauth/FacebookOAuthService.java b/src/main/java/com/googlesource/gerrit/plugins/oauth/FacebookOAuthService.java
new file mode 100644
index 0000000..d3ebe8e
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/oauth/FacebookOAuthService.java
@@ -0,0 +1,142 @@
+// Copyright (C) 2017 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.google.common.base.CharMatcher;
+import com.google.gerrit.extensions.annotations.PluginName;
+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.OutputFormat;
+import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.gerrit.server.config.PluginConfig;
+import com.google.gerrit.server.config.PluginConfigFactory;
+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 java.io.IOException;
+import javax.servlet.http.HttpServletResponse;
+import org.scribe.builder.ServiceBuilder;
+import org.scribe.model.OAuthRequest;
+import org.scribe.model.Response;
+import org.scribe.model.Token;
+import org.scribe.model.Verb;
+import org.scribe.model.Verifier;
+import org.scribe.oauth.OAuthService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Singleton
+class FacebookOAuthService implements OAuthServiceProvider {
+  private static final Logger log = LoggerFactory.getLogger(FacebookOAuthService.class);
+  static final String CONFIG_SUFFIX = "-facebook-oauth";
+  private static final String PROTECTED_RESOURCE_URL = "https://graph.facebook.com/me";
+
+  private static final String FACEBOOK_PROVIDER_PREFIX = "facebook-oauth:";
+  private static final String SCOPE = "email";
+  private static final String FIELDS_QUERY = "fields";
+  private static final String FIELDS = "email,name";
+  private final OAuthService service;
+
+  @Inject
+  FacebookOAuthService(
+      PluginConfigFactory cfgFactory,
+      @PluginName String pluginName,
+      @CanonicalWebUrl Provider<String> urlProvider) {
+
+    PluginConfig cfg = cfgFactory.getFromGerritConfig(pluginName + CONFIG_SUFFIX);
+    String canonicalWebUrl = CharMatcher.is('/').trimTrailingFrom(urlProvider.get()) + "/";
+
+    service =
+        new ServiceBuilder()
+            .provider(Facebook2Api.class)
+            .apiKey(cfg.getString(InitOAuth.CLIENT_ID))
+            .apiSecret(cfg.getString(InitOAuth.CLIENT_SECRET))
+            .callback(canonicalWebUrl + "oauth")
+            .scope(SCOPE)
+            .build();
+  }
+
+  @Override
+  public OAuthUserInfo getUserInfo(OAuthToken token) throws IOException {
+    OAuthRequest request = new OAuthRequest(Verb.GET, PROTECTED_RESOURCE_URL);
+    Token t = new Token(token.getToken(), token.getSecret(), token.getRaw());
+    request.addQuerystringParameter(FIELDS_QUERY, FIELDS);
+    service.signRequest(t, request);
+    Response response = request.send();
+
+    if (response.getCode() != HttpServletResponse.SC_OK) {
+      throw new IOException(
+          String.format(
+              "Status %s (%s) for request %s",
+              response.getCode(), response.getBody(), request.getUrl()));
+    }
+    JsonElement userJson =
+        OutputFormat.JSON.newGson().fromJson(response.getBody(), JsonElement.class);
+
+    if (log.isDebugEnabled()) {
+      log.debug("User info response: {}", response.getBody());
+    }
+    if (userJson.isJsonObject()) {
+      JsonObject jsonObject = userJson.getAsJsonObject();
+      JsonElement id = jsonObject.get("id");
+      if (id == null || id.isJsonNull()) {
+        throw new IOException(String.format("Response doesn't contain id field"));
+      }
+      JsonElement email = jsonObject.get("email");
+      JsonElement name = jsonObject.get("name");
+      // Heads up!
+      // Lets keep `login` equal to `email`, since `username` field is
+      // deprecated for Facebook API versions v2.0 and higher
+      JsonElement login = jsonObject.get("email");
+
+      return new OAuthUserInfo(
+          FACEBOOK_PROVIDER_PREFIX + id.getAsString(),
+          login == null || login.isJsonNull() ? null : login.getAsString(),
+          email == null || email.isJsonNull() ? null : email.getAsString(),
+          name == null || name.isJsonNull() ? null : name.getAsString(),
+          null);
+    }
+
+    throw new IOException(String.format("Invalid JSON '%s': not a JSON Object", userJson));
+  }
+
+  @Override
+  public OAuthToken getAccessToken(OAuthVerifier rv) {
+    Verifier vi = new Verifier(rv.getValue());
+    Token to = service.getAccessToken(null, vi);
+    OAuthToken result = new OAuthToken(to.getToken(), to.getSecret(), to.getRawResponse());
+
+    return result;
+  }
+
+  @Override
+  public String getAuthorizationUrl() {
+    return service.getAuthorizationUrl(null);
+  }
+
+  @Override
+  public String getVersion() {
+    return service.getVersion();
+  }
+
+  @Override
+  public String getName() {
+    return "Facebook OAuth2";
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/oauth/GitHub2Api.java b/src/main/java/com/googlesource/gerrit/plugins/oauth/GitHub2Api.java
index 5853498..4b8419b 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/oauth/GitHub2Api.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/oauth/GitHub2Api.java
@@ -29,7 +29,7 @@
 
   @Override
   public String getAuthorizationUrl(OAuthConfig config) {
-    return String.format(AUTHORIZE_URL, config.getApiKey(),
-        OAuthEncoder.encode(config.getCallback()));
+    return String.format(
+        AUTHORIZE_URL, config.getApiKey(), OAuthEncoder.encode(config.getCallback()));
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/oauth/GitHubOAuthService.java b/src/main/java/com/googlesource/gerrit/plugins/oauth/GitHubOAuthService.java
index 7971b26..8a198d3 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/oauth/GitHubOAuthService.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/oauth/GitHubOAuthService.java
@@ -29,7 +29,8 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
-
+import java.io.IOException;
+import javax.servlet.http.HttpServletResponse;
 import org.scribe.builder.ServiceBuilder;
 import org.scribe.model.OAuthRequest;
 import org.scribe.model.Response;
@@ -40,52 +41,49 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.io.IOException;
-
-import javax.servlet.http.HttpServletResponse;
-
 @Singleton
 class GitHubOAuthService implements OAuthServiceProvider {
-  private static final Logger log =
-      LoggerFactory.getLogger(GitHubOAuthService.class);
+  private static final Logger log = LoggerFactory.getLogger(GitHubOAuthService.class);
   static final String CONFIG_SUFFIX = "-github-oauth";
-  private static final String PROTECTED_RESOURCE_URL =
-      "https://api.github.com/user";
+  private static final String GITHUB_PROVIDER_PREFIX = "github-oauth:";
+  private static final String PROTECTED_RESOURCE_URL = "https://api.github.com/user";
 
   private static final String SCOPE = "user:email";
+  private final boolean fixLegacyUserId;
   private final OAuthService service;
 
   @Inject
-  GitHubOAuthService(PluginConfigFactory cfgFactory,
+  GitHubOAuthService(
+      PluginConfigFactory cfgFactory,
       @PluginName String pluginName,
       @CanonicalWebUrl Provider<String> urlProvider) {
-    PluginConfig cfg = cfgFactory.getFromGerritConfig(
-        pluginName + CONFIG_SUFFIX);
-    String canonicalWebUrl = CharMatcher.is('/').trimTrailingFrom(
-        urlProvider.get()) + "/";
-    service = new ServiceBuilder()
-        .provider(GitHub2Api.class)
-        .apiKey(cfg.getString(InitOAuth.CLIENT_ID))
-        .apiSecret(cfg.getString(InitOAuth.CLIENT_SECRET))
-        .callback(canonicalWebUrl + "oauth")
-        .scope(SCOPE)
-        .build();
+    PluginConfig cfg = cfgFactory.getFromGerritConfig(pluginName + CONFIG_SUFFIX);
+    String canonicalWebUrl = CharMatcher.is('/').trimTrailingFrom(urlProvider.get()) + "/";
+    fixLegacyUserId = cfg.getBoolean(InitOAuth.FIX_LEGACY_USER_ID, false);
+    service =
+        new ServiceBuilder()
+            .provider(GitHub2Api.class)
+            .apiKey(cfg.getString(InitOAuth.CLIENT_ID))
+            .apiSecret(cfg.getString(InitOAuth.CLIENT_SECRET))
+            .callback(canonicalWebUrl + "oauth")
+            .scope(SCOPE)
+            .build();
   }
 
   @Override
   public OAuthUserInfo getUserInfo(OAuthToken token) throws IOException {
     OAuthRequest request = new OAuthRequest(Verb.GET, PROTECTED_RESOURCE_URL);
-    Token t =
-        new Token(token.getToken(), token.getSecret(), token.getRaw());
+    Token t = new Token(token.getToken(), token.getSecret(), token.getRaw());
     service.signRequest(t, request);
     Response response = request.send();
     if (response.getCode() != HttpServletResponse.SC_OK) {
-      throw new IOException(String.format("Status %s (%s) for request %s",
-          response.getCode(), response.getBody(), request.getUrl()));
+      throw new IOException(
+          String.format(
+              "Status %s (%s) for request %s",
+              response.getCode(), response.getBody(), request.getUrl()));
     }
     JsonElement userJson =
-        OutputFormat.JSON.newGson().fromJson(response.getBody(),
-            JsonElement.class);
+        OutputFormat.JSON.newGson().fromJson(response.getBody(), JsonElement.class);
     if (log.isDebugEnabled()) {
       log.debug("User info response: {}", response.getBody());
     }
@@ -93,29 +91,27 @@
       JsonObject jsonObject = userJson.getAsJsonObject();
       JsonElement id = jsonObject.get("id");
       if (id == null || id.isJsonNull()) {
-        throw new IOException(String.format(
-            "Response doesn't contain id field"));
+        throw new IOException(String.format("Response doesn't contain id field"));
       }
       JsonElement email = jsonObject.get("email");
       JsonElement name = jsonObject.get("name");
       JsonElement login = jsonObject.get("login");
-      return new OAuthUserInfo(id.getAsString(),
+      return new OAuthUserInfo(
+          GITHUB_PROVIDER_PREFIX + id.getAsString(),
           login == null || login.isJsonNull() ? null : login.getAsString(),
           email == null || email.isJsonNull() ? null : email.getAsString(),
           name == null || name.isJsonNull() ? null : name.getAsString(),
-          null);
-    } else {
-        throw new IOException(String.format(
-            "Invalid JSON '%s': not a JSON Object", userJson));
+          fixLegacyUserId ? id.getAsString() : null);
     }
+
+    throw new IOException(String.format("Invalid JSON '%s': not a JSON Object", userJson));
   }
 
   @Override
   public OAuthToken getAccessToken(OAuthVerifier rv) {
     Verifier vi = new Verifier(rv.getValue());
     Token to = service.getAccessToken(null, vi);
-    OAuthToken result = new OAuthToken(to.getToken(),
-        to.getSecret(), to.getRawResponse());
+    OAuthToken result = new OAuthToken(to.getToken(), to.getSecret(), to.getRawResponse());
     return result;
   }
 
diff --git a/src/main/java/com/googlesource/gerrit/plugins/oauth/GitLabApi.java b/src/main/java/com/googlesource/gerrit/plugins/oauth/GitLabApi.java
new file mode 100644
index 0000000..db0851f
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/oauth/GitLabApi.java
@@ -0,0 +1,57 @@
+// Copyright (C) 2017 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 org.scribe.builder.api.DefaultApi20;
+import org.scribe.extractors.AccessTokenExtractor;
+import org.scribe.model.OAuthConfig;
+import org.scribe.model.Verb;
+import org.scribe.oauth.OAuthService;
+
+public class GitLabApi extends DefaultApi20 {
+  private static final String AUTHORIZE_URL =
+      "%s/oauth/authorize?client_id=%s&response_type=code&redirect_uri=%s";
+
+  private final String rootUrl;
+
+  public GitLabApi(String rootUrl) {
+    this.rootUrl = rootUrl;
+  }
+
+  @Override
+  public String getAuthorizationUrl(OAuthConfig config) {
+    return String.format(AUTHORIZE_URL, rootUrl, config.getApiKey(), config.getCallback());
+  }
+
+  @Override
+  public String getAccessTokenEndpoint() {
+    return String.format("%s/oauth/token", rootUrl);
+  }
+
+  @Override
+  public Verb getAccessTokenVerb() {
+    return Verb.POST;
+  }
+
+  @Override
+  public OAuthService createService(OAuthConfig config) {
+    return new OAuth20ServiceImpl(this, config);
+  }
+
+  @Override
+  public AccessTokenExtractor getAccessTokenExtractor() {
+    return OAuth2AccessTokenJsonExtractor.instance();
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/oauth/GitLabOAuthService.java b/src/main/java/com/googlesource/gerrit/plugins/oauth/GitLabOAuthService.java
new file mode 100644
index 0000000..2d6870a
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/oauth/GitLabOAuthService.java
@@ -0,0 +1,126 @@
+// Copyright (C) 2017 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 static com.google.gerrit.server.OutputFormat.JSON;
+import static javax.servlet.http.HttpServletResponse.SC_OK;
+import static org.slf4j.LoggerFactory.getLogger;
+
+import com.google.common.base.CharMatcher;
+import com.google.gerrit.extensions.annotations.PluginName;
+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.gerrit.server.config.PluginConfigFactory;
+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 java.io.IOException;
+import org.scribe.builder.ServiceBuilder;
+import org.scribe.model.OAuthRequest;
+import org.scribe.model.Response;
+import org.scribe.model.Token;
+import org.scribe.model.Verb;
+import org.scribe.model.Verifier;
+import org.scribe.oauth.OAuthService;
+import org.slf4j.Logger;
+
+@Singleton
+public class GitLabOAuthService implements OAuthServiceProvider {
+  private static final Logger log = getLogger(GitLabOAuthService.class);
+  static final String CONFIG_SUFFIX = "-gitlab-oauth";
+  private static final String PROTECTED_RESOURCE_URL = "%s/api/v3/user";
+  private static final String GITLAB_PROVIDER_PREFIX = "gitlab-oauth:";
+  private final OAuthService service;
+  private final String rootUrl;
+
+  @Inject
+  GitLabOAuthService(
+      PluginConfigFactory cfgFactory,
+      @PluginName String pluginName,
+      @CanonicalWebUrl Provider<String> urlProvider) {
+    PluginConfig cfg = cfgFactory.getFromGerritConfig(pluginName + CONFIG_SUFFIX);
+    String canonicalWebUrl = CharMatcher.is('/').trimTrailingFrom(urlProvider.get()) + "/";
+    rootUrl = cfg.getString(InitOAuth.ROOT_URL);
+    service =
+        new ServiceBuilder()
+            .provider(new GitLabApi(rootUrl))
+            .apiKey(cfg.getString(InitOAuth.CLIENT_ID))
+            .apiSecret(cfg.getString(InitOAuth.CLIENT_SECRET))
+            .callback(canonicalWebUrl + "oauth")
+            .build();
+  }
+
+  @Override
+  public OAuthUserInfo getUserInfo(OAuthToken token) throws IOException {
+    final String protectedResourceUrl = String.format(PROTECTED_RESOURCE_URL, rootUrl);
+    OAuthRequest request = new OAuthRequest(Verb.GET, protectedResourceUrl);
+    Token t = new Token(token.getToken(), token.getSecret(), token.getRaw());
+    service.signRequest(t, request);
+
+    Response response = request.send();
+    if (response.getCode() != SC_OK) {
+      throw new IOException(
+          String.format(
+              "Status %s (%s) for request %s",
+              response.getCode(), response.getBody(), request.getUrl()));
+    }
+    JsonElement userJson = JSON.newGson().fromJson(response.getBody(), JsonElement.class);
+    if (log.isDebugEnabled()) {
+      log.debug("User info response: {}", response.getBody());
+    }
+    JsonObject jsonObject = userJson.getAsJsonObject();
+    if (jsonObject == null || jsonObject.isJsonNull()) {
+      throw new IOException("Response doesn't contain 'user' field" + jsonObject);
+    }
+    JsonElement id = jsonObject.get("id");
+    JsonElement username = jsonObject.get("username");
+    JsonElement email = jsonObject.get("email");
+    JsonElement name = jsonObject.get("name");
+    return new OAuthUserInfo(
+        GITLAB_PROVIDER_PREFIX + id.getAsString(),
+        username == null || username.isJsonNull() ? null : username.getAsString(),
+        email == null || email.isJsonNull() ? null : email.getAsString(),
+        name == null || name.isJsonNull() ? null : name.getAsString(),
+        null);
+  }
+
+  @Override
+  public OAuthToken getAccessToken(OAuthVerifier rv) {
+    Verifier vi = new Verifier(rv.getValue());
+    Token to = service.getAccessToken(null, vi);
+    return new OAuthToken(to.getToken(), to.getSecret(), null);
+  }
+
+  @Override
+  public String getAuthorizationUrl() {
+    return service.getAuthorizationUrl(null);
+  }
+
+  @Override
+  public String getVersion() {
+    return service.getVersion();
+  }
+
+  @Override
+  public String getName() {
+    return "GitLab OAuth2";
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/oauth/Google2Api.java b/src/main/java/com/googlesource/gerrit/plugins/oauth/Google2Api.java
index 7874b7e..88c640d 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/oauth/Google2Api.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/oauth/Google2Api.java
@@ -16,19 +16,10 @@
 
 import static org.scribe.utils.OAuthEncoder.encode;
 
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
 import org.scribe.builder.api.DefaultApi20;
-import org.scribe.exceptions.OAuthException;
 import org.scribe.extractors.AccessTokenExtractor;
 import org.scribe.model.OAuthConfig;
-import org.scribe.model.OAuthConstants;
-import org.scribe.model.OAuthRequest;
-import org.scribe.model.Response;
-import org.scribe.model.Token;
 import org.scribe.model.Verb;
-import org.scribe.model.Verifier;
 import org.scribe.oauth.OAuthService;
 import org.scribe.utils.Preconditions;
 
@@ -46,14 +37,13 @@
 
   @Override
   public String getAuthorizationUrl(OAuthConfig config) {
-    Preconditions.checkValidUrl(config.getCallback(),
-        "Must provide a valid url as callback. Google does not support OOB");
-    Preconditions
-        .checkEmptyString(config.getScope(),
-            "Must provide a valid value as scope. Google does not support no scope");
+    Preconditions.checkValidUrl(
+        config.getCallback(), "Must provide a valid url as callback. Google does not support OOB");
+    Preconditions.checkEmptyString(
+        config.getScope(), "Must provide a valid value as scope. Google does not support no scope");
 
-    return String.format(AUTHORIZE_URL, config.getApiKey(),
-        encode(config.getCallback()), encode(config.getScope()));
+    return String.format(
+        AUTHORIZE_URL, config.getApiKey(), encode(config.getCallback()), encode(config.getScope()));
   }
 
   @Override
@@ -63,106 +53,11 @@
 
   @Override
   public OAuthService createService(OAuthConfig config) {
-    return new GoogleOAuthService(this, config);
+    return new OAuth20ServiceImpl(this, config);
   }
 
   @Override
   public AccessTokenExtractor getAccessTokenExtractor() {
-    return new GoogleJsonTokenExtractor();
+    return OAuth2AccessTokenJsonExtractor.instance();
   }
-
-  private static final class GoogleOAuthService implements OAuthService {
-    private static final String VERSION = "2.0";
-
-    private static final String GRANT_TYPE = "grant_type";
-    private static final String GRANT_TYPE_VALUE = "authorization_code";
-
-    private final DefaultApi20 api;
-    private final OAuthConfig config;
-
-    /**
-     * Default constructor
-     *
-     * @param api OAuth2.0 api information
-     * @param config OAuth 2.0 configuration param object
-     */
-    public GoogleOAuthService(DefaultApi20 api, OAuthConfig config) {
-      this.api = api;
-      this.config = config;
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public Token getAccessToken(Token requestToken, Verifier verifier) {
-      OAuthRequest request =
-          new OAuthRequest(api.getAccessTokenVerb(),
-              api.getAccessTokenEndpoint());
-      request.addBodyParameter(OAuthConstants.CLIENT_ID, config.getApiKey());
-      request.addBodyParameter(OAuthConstants.CLIENT_SECRET,
-          config.getApiSecret());
-      request.addBodyParameter(OAuthConstants.CODE, verifier.getValue());
-      request.addBodyParameter(OAuthConstants.REDIRECT_URI,
-          config.getCallback());
-      if (config.hasScope())
-        request.addBodyParameter(OAuthConstants.SCOPE, config.getScope());
-      request.addBodyParameter(GRANT_TYPE, GRANT_TYPE_VALUE);
-      Response response = request.send();
-      return api.getAccessTokenExtractor().extract(response.getBody());
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public Token getRequestToken() {
-      throw new UnsupportedOperationException(
-          "Unsupported operation, please use 'getAuthorizationUrl' and redirect your users there");
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public String getVersion() {
-      return VERSION;
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public void signRequest(Token accessToken, OAuthRequest request) {
-      request.addQuerystringParameter(OAuthConstants.ACCESS_TOKEN,
-          accessToken.getToken());
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public String getAuthorizationUrl(Token requestToken) {
-      return api.getAuthorizationUrl(config);
-    }
-  }
-
-  private static final class GoogleJsonTokenExtractor implements
-      AccessTokenExtractor {
-    private Pattern accessTokenPattern = Pattern
-        .compile("\"access_token\"\\s*:\\s*\"(\\S*?)\"");
-
-    @Override
-    public Token extract(String response) {
-      Preconditions.checkEmptyString(response,
-          "Cannot extract a token from a null or empty String");
-      Matcher matcher = accessTokenPattern.matcher(response);
-      if (matcher.find()) {
-        return new Token(matcher.group(1), "", response);
-      } else {
-        throw new OAuthException(
-            "Cannot extract an acces token. Response was: " + response);
-      }
-    }
-  }
-}
\ No newline at end of file
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/oauth/GoogleOAuthService.java b/src/main/java/com/googlesource/gerrit/plugins/oauth/GoogleOAuthService.java
index 69453fd..e44843a 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/oauth/GoogleOAuthService.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/oauth/GoogleOAuthService.java
@@ -31,7 +31,13 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
-
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.List;
+import javax.servlet.http.HttpServletResponse;
 import org.apache.commons.codec.binary.Base64;
 import org.scribe.builder.ServiceBuilder;
 import org.scribe.model.OAuthRequest;
@@ -43,56 +49,47 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.io.IOException;
-import java.io.UnsupportedEncodingException;
-import java.net.URLEncoder;
-import java.nio.charset.StandardCharsets;
-
-import javax.servlet.http.HttpServletResponse;
-
 @Singleton
 class GoogleOAuthService implements OAuthServiceProvider {
-  private static final Logger log =
-      LoggerFactory.getLogger(GoogleOAuthService.class);
+  private static final Logger log = LoggerFactory.getLogger(GoogleOAuthService.class);
   static final String CONFIG_SUFFIX = "-google-oauth";
+  private static final String GOOGLE_PROVIDER_PREFIX = "google-oauth:";
   private static final String PROTECTED_RESOURCE_URL =
       "https://www.googleapis.com/oauth2/v2/userinfo";
   private static final String SCOPE = "email profile";
   private final OAuthService service;
   private final String canonicalWebUrl;
-  private final boolean linkToExistingOpenIDAccounts;
-  private final String domain;
+  private final List<String> domains;
   private final boolean useEmailAsUsername;
+  private final boolean fixLegacyUserId;
 
   @Inject
-  GoogleOAuthService(PluginConfigFactory cfgFactory,
+  GoogleOAuthService(
+      PluginConfigFactory cfgFactory,
       @PluginName String pluginName,
       @CanonicalWebUrl Provider<String> urlProvider) {
-    PluginConfig cfg = cfgFactory.getFromGerritConfig(
-        pluginName + CONFIG_SUFFIX);
-    this.canonicalWebUrl = CharMatcher.is('/').trimTrailingFrom(
-        urlProvider.get()) + "/";
-    this.linkToExistingOpenIDAccounts = cfg.getBoolean(
-        InitOAuth.LINK_TO_EXISTING_OPENID_ACCOUNT, false);
-    this.domain = cfg.getString(InitOAuth.DOMAIN);
-    this.useEmailAsUsername = cfg.getBoolean(
-        InitOAuth.USE_EMAIL_AS_USERNAME, false);
-    String scope = linkToExistingOpenIDAccounts
-        ? "openid " + SCOPE
-        : SCOPE;
-    this.service = new ServiceBuilder()
-        .provider(Google2Api.class)
-        .apiKey(cfg.getString(InitOAuth.CLIENT_ID))
-        .apiSecret(cfg.getString(InitOAuth.CLIENT_SECRET))
-        .callback(canonicalWebUrl + "oauth")
-        .scope(scope)
-        .build();
+    PluginConfig cfg = cfgFactory.getFromGerritConfig(pluginName + CONFIG_SUFFIX);
+    this.canonicalWebUrl = CharMatcher.is('/').trimTrailingFrom(urlProvider.get()) + "/";
+    if (cfg.getBoolean(InitOAuth.LINK_TO_EXISTING_OPENID_ACCOUNT, false)) {
+      log.warn(
+          String.format(
+              "The support for: %s is disconinued", InitOAuth.LINK_TO_EXISTING_OPENID_ACCOUNT));
+    }
+    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()
+            .provider(Google2Api.class)
+            .apiKey(cfg.getString(InitOAuth.CLIENT_ID))
+            .apiSecret(cfg.getString(InitOAuth.CLIENT_SECRET))
+            .callback(canonicalWebUrl + "oauth")
+            .scope(SCOPE)
+            .build();
     if (log.isDebugEnabled()) {
       log.debug("OAuth2: canonicalWebUrl={}", canonicalWebUrl);
-      log.debug("OAuth2: scope={}", scope);
-      log.debug("OAuth2: linkToExistingOpenIDAccounts={}",
-          linkToExistingOpenIDAccounts);
-      log.debug("OAuth2: domain={}", domain);
+      log.debug("OAuth2: scope={}", SCOPE);
+      log.debug("OAuth2: domains={}", domains);
       log.debug("OAuth2: useEmailAsUsername={}", useEmailAsUsername);
     }
   }
@@ -100,17 +97,17 @@
   @Override
   public OAuthUserInfo getUserInfo(OAuthToken token) throws IOException {
     OAuthRequest request = new OAuthRequest(Verb.GET, PROTECTED_RESOURCE_URL);
-    Token t =
-        new Token(token.getToken(), token.getSecret(), token.getRaw());
+    Token t = new Token(token.getToken(), token.getSecret(), token.getRaw());
     service.signRequest(t, request);
     Response response = request.send();
     if (response.getCode() != HttpServletResponse.SC_OK) {
-      throw new IOException(String.format("Status %s (%s) for request %s",
-          response.getCode(), response.getBody(), request.getUrl()));
+      throw new IOException(
+          String.format(
+              "Status %s (%s) for request %s",
+              response.getCode(), response.getBody(), request.getUrl()));
     }
     JsonElement userJson =
-        OutputFormat.JSON.newGson().fromJson(response.getBody(),
-            JsonElement.class);
+        OutputFormat.JSON.newGson().fromJson(response.getBody(), JsonElement.class);
     if (log.isDebugEnabled()) {
       log.debug("User info response: {}", response.getBody());
     }
@@ -118,47 +115,44 @@
       JsonObject jsonObject = userJson.getAsJsonObject();
       JsonElement id = jsonObject.get("id");
       if (id == null || id.isJsonNull()) {
-        throw new IOException(String.format(
-            "Response doesn't contain id field"));
+        throw new IOException(String.format("Response doesn't contain id field"));
       }
       JsonElement email = jsonObject.get("email");
       JsonElement name = jsonObject.get("name");
-      String claimedIdentifier = null;
       String login = null;
 
-      if (linkToExistingOpenIDAccounts
-          || !Strings.isNullOrEmpty(domain)) {
+      if (domains.size() > 0) {
+        boolean domainMatched = false;
         JsonObject jwtToken = retrieveJWTToken(token);
-        if (linkToExistingOpenIDAccounts) {
-          claimedIdentifier = retrieveClaimedIdentity(jwtToken);
-        }
-        if (!Strings.isNullOrEmpty(domain)) {
-          String hdClaim = retrieveHostedDomain(jwtToken);
-          if (!domain.equalsIgnoreCase(hdClaim)) {
-            // TODO(davido): improve error reporting in OAuth extension point
-            log.error("Error: hosted domain validation failed: {}",
-                Strings.nullToEmpty(hdClaim));
-            return null;
+        String hdClaim = retrieveHostedDomain(jwtToken);
+        for (String domain : domains) {
+          if (domain.equalsIgnoreCase(hdClaim)) {
+            domainMatched = true;
+            break;
           }
         }
+        if (!domainMatched) {
+          // TODO(davido): improve error reporting in OAuth extension point
+          log.error("Error: hosted domain validation failed: {}", Strings.nullToEmpty(hdClaim));
+          return null;
+        }
       }
       if (useEmailAsUsername && !email.isJsonNull()) {
         login = email.getAsString().split("@")[0];
       }
-      return new OAuthUserInfo(id.getAsString() /*externalId*/,
+      return new OAuthUserInfo(
+          GOOGLE_PROVIDER_PREFIX + id.getAsString() /*externalId*/,
           login /*username*/,
           email == null || email.isJsonNull() ? null : email.getAsString() /*email*/,
           name == null || name.isJsonNull() ? null : name.getAsString() /*displayName*/,
-	      claimedIdentifier /*claimedIdentity*/);
-    } else {
-        throw new IOException(String.format(
-            "Invalid JSON '%s': not a JSON Object", userJson));
+          fixLegacyUserId ? id.getAsString() : null /*claimedIdentity*/);
     }
+
+    throw new IOException(String.format("Invalid JSON '%s': not a JSON Object", userJson));
   }
 
   private JsonObject retrieveJWTToken(OAuthToken token) {
-    JsonElement idToken =
-        OutputFormat.JSON.newGson().fromJson(token.getRaw(), JsonElement.class);
+    JsonElement idToken = OutputFormat.JSON.newGson().fromJson(token.getRaw(), JsonElement.class);
     if (idToken != null && idToken.isJsonObject()) {
       JsonObject idTokenObj = idToken.getAsJsonObject();
       JsonElement idTokenElement = idTokenObj.get("id_token");
@@ -166,7 +160,7 @@
         String payload = decodePayload(idTokenElement.getAsString());
         if (!Strings.isNullOrEmpty(payload)) {
           JsonElement tokenJsonElement =
-            OutputFormat.JSON.newGson().fromJson(payload, JsonElement.class);
+              OutputFormat.JSON.newGson().fromJson(payload, JsonElement.class);
           if (tokenJsonElement.isJsonObject()) {
             return tokenJsonElement.getAsJsonObject();
           }
@@ -176,17 +170,6 @@
     return null;
   }
 
-  private static String retrieveClaimedIdentity(JsonObject jwtToken) {
-    JsonElement openidIdElement = jwtToken.get("openid_id");
-    if (openidIdElement != null && !openidIdElement.isJsonNull()) {
-      String openIdId = openidIdElement.getAsString();
-      log.debug("OAuth2: openid_id={}", openIdId);
-      return openIdId;
-    }
-    log.debug("OAuth2: JWT doesn't contain openid_id element");
-    return null;
-  }
-
   private static String retrieveHostedDomain(JsonObject jwtToken) {
     JsonElement hdClaim = jwtToken.get("hd");
     if (hdClaim != null && !hdClaim.isJsonNull()) {
@@ -199,8 +182,7 @@
   }
 
   /**
-   * Decode payload from JWT according to spec:
-   * "header.payload.signature"
+   * Decode payload from JWT according to spec: "header.payload.signature"
    *
    * @param idToken Base64 encoded tripple, separated with dot
    * @return openid_id part of payload, when contained, null otherwise
@@ -218,22 +200,18 @@
   public OAuthToken getAccessToken(OAuthVerifier rv) {
     Verifier vi = new Verifier(rv.getValue());
     Token to = service.getAccessToken(null, vi);
-    OAuthToken result = new OAuthToken(to.getToken(),
-        to.getSecret(), to.getRawResponse());
-     return result;
+    OAuthToken result = new OAuthToken(to.getToken(), to.getSecret(), to.getRawResponse());
+    return result;
   }
 
   @Override
   public String getAuthorizationUrl() {
     String url = service.getAuthorizationUrl(null);
     try {
-      if (linkToExistingOpenIDAccounts) {
-        url += "&openid.realm=" + URLEncoder.encode(canonicalWebUrl,
-            StandardCharsets.UTF_8.name());
-      }
-      if (!Strings.isNullOrEmpty(domain)) {
-        url += "&hd=" + URLEncoder.encode(domain,
-            StandardCharsets.UTF_8.name());
+      if (domains.size() == 1) {
+        url += "&hd=" + URLEncoder.encode(domains.get(0), StandardCharsets.UTF_8.name());
+      } else if (domains.size() > 1) {
+        url += "&hd=*";
       }
     } catch (UnsupportedEncodingException e) {
       throw new IllegalArgumentException(e);
diff --git a/src/main/java/com/googlesource/gerrit/plugins/oauth/HttpModule.java b/src/main/java/com/googlesource/gerrit/plugins/oauth/HttpModule.java
index 72121d0..d51d918 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/oauth/HttpModule.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/oauth/HttpModule.java
@@ -28,44 +28,75 @@
   private final String pluginName;
 
   @Inject
-  HttpModule(PluginConfigFactory cfgFactory,
-      @PluginName String pluginName) {
+  HttpModule(PluginConfigFactory cfgFactory, @PluginName String pluginName) {
     this.cfgFactory = cfgFactory;
     this.pluginName = pluginName;
   }
 
   @Override
   protected void configureServlets() {
-    PluginConfig cfg = cfgFactory.getFromGerritConfig(
-        pluginName + GoogleOAuthService.CONFIG_SUFFIX);
+    PluginConfig cfg =
+        cfgFactory.getFromGerritConfig(pluginName + GoogleOAuthService.CONFIG_SUFFIX);
     if (cfg.getString(InitOAuth.CLIENT_ID) != null) {
       bind(OAuthServiceProvider.class)
           .annotatedWith(Exports.named(GoogleOAuthService.CONFIG_SUFFIX))
           .to(GoogleOAuthService.class);
     }
 
-    cfg = cfgFactory.getFromGerritConfig(
-        pluginName + GitHubOAuthService.CONFIG_SUFFIX);
-    if (cfg.getString("client-id") != null) {
+    cfg = cfgFactory.getFromGerritConfig(pluginName + GitHubOAuthService.CONFIG_SUFFIX);
+    if (cfg.getString(InitOAuth.CLIENT_ID) != null) {
       bind(OAuthServiceProvider.class)
           .annotatedWith(Exports.named(GitHubOAuthService.CONFIG_SUFFIX))
           .to(GitHubOAuthService.class);
     }
 
-    cfg = cfgFactory.getFromGerritConfig(
-        pluginName + BitbucketOAuthService.CONFIG_SUFFIX);
+    cfg = cfgFactory.getFromGerritConfig(pluginName + BitbucketOAuthService.CONFIG_SUFFIX);
     if (cfg.getString(InitOAuth.CLIENT_ID) != null) {
       bind(OAuthServiceProvider.class)
           .annotatedWith(Exports.named(BitbucketOAuthService.CONFIG_SUFFIX))
           .to(BitbucketOAuthService.class);
     }
 
-    cfg = cfgFactory.getFromGerritConfig(
-        pluginName + CasOAuthService.CONFIG_SUFFIX);
+    cfg = cfgFactory.getFromGerritConfig(pluginName + CasOAuthService.CONFIG_SUFFIX);
     if (cfg.getString(InitOAuth.CLIENT_ID) != null) {
       bind(OAuthServiceProvider.class)
           .annotatedWith(Exports.named(CasOAuthService.CONFIG_SUFFIX))
           .to(CasOAuthService.class);
     }
+
+    cfg = cfgFactory.getFromGerritConfig(pluginName + FacebookOAuthService.CONFIG_SUFFIX);
+    if (cfg.getString(InitOAuth.CLIENT_ID) != null) {
+      bind(OAuthServiceProvider.class)
+          .annotatedWith(Exports.named(FacebookOAuthService.CONFIG_SUFFIX))
+          .to(FacebookOAuthService.class);
+    }
+
+    cfg = cfgFactory.getFromGerritConfig(pluginName + GitLabOAuthService.CONFIG_SUFFIX);
+    if (cfg.getString(InitOAuth.CLIENT_ID) != null) {
+      bind(OAuthServiceProvider.class)
+          .annotatedWith(Exports.named(GitLabOAuthService.CONFIG_SUFFIX))
+          .to(GitLabOAuthService.class);
+    }
+
+    cfg = cfgFactory.getFromGerritConfig(pluginName + DexOAuthService.CONFIG_SUFFIX);
+    if (cfg.getString(InitOAuth.CLIENT_ID) != null) {
+      bind(OAuthServiceProvider.class)
+          .annotatedWith(Exports.named(DexOAuthService.CONFIG_SUFFIX))
+          .to(DexOAuthService.class);
+    }
+
+    cfg = cfgFactory.getFromGerritConfig(pluginName + KeycloakOAuthService.CONFIG_SUFFIX);
+    if (cfg.getString(InitOAuth.CLIENT_ID) != null) {
+      bind(OAuthServiceProvider.class)
+          .annotatedWith(Exports.named(KeycloakOAuthService.CONFIG_SUFFIX))
+          .to(KeycloakOAuthService.class);
+    }
+
+    cfg = cfgFactory.getFromGerritConfig(pluginName + Office365OAuthService.CONFIG_SUFFIX);
+    if (cfg.getString(InitOAuth.CLIENT_ID) != null) {
+      bind(OAuthServiceProvider.class)
+          .annotatedWith(Exports.named(Office365OAuthService.CONFIG_SUFFIX))
+          .to(Office365OAuthService.class);
+    }
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/oauth/InitOAuth.java b/src/main/java/com/googlesource/gerrit/plugins/oauth/InitOAuth.java
index 0a5876d..b8e54e4 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/oauth/InitOAuth.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/oauth/InitOAuth.java
@@ -23,64 +23,113 @@
   static final String PLUGIN_SECTION = "plugin";
   static final String CLIENT_ID = "client-id";
   static final String CLIENT_SECRET = "client-secret";
-  static final String LINK_TO_EXISTING_OPENID_ACCOUNT =
-      "link-to-existing-openid-accounts";
+  static final String LINK_TO_EXISTING_OPENID_ACCOUNT = "link-to-existing-openid-accounts";
+  static final String FIX_LEGACY_USER_ID = "fix-legacy-user-id";
   static final String DOMAIN = "domain";
-  static final String USE_EMAIL_AS_USERNAME =
-      "use-email-as-username";
+  static final String USE_EMAIL_AS_USERNAME = "use-email-as-username";
   static final String ROOT_URL = "root-url";
+  static final String REALM = "realm";
+  static final String SERVICE_NAME = "service-name";
+  static String FIX_LEGACY_USER_ID_QUESTION = "Fix legacy user id, without oauth provider prefix?";
 
   private final ConsoleUI ui;
   private final Section googleOAuthProviderSection;
   private final Section githubOAuthProviderSection;
   private final Section bitbucketOAuthProviderSection;
   private final Section casOAuthProviderSection;
+  private final Section facebookOAuthProviderSection;
+  private final Section gitlabOAuthProviderSection;
+  private final Section dexOAuthProviderSection;
+  private final Section keycloakOAuthProviderSection;
+  private final Section office365OAuthProviderSection;
 
   @Inject
-  InitOAuth(ConsoleUI ui,
-      Section.Factory sections,
-      @PluginName String pluginName) {
+  InitOAuth(ConsoleUI ui, Section.Factory sections, @PluginName String pluginName) {
     this.ui = ui;
-    this.googleOAuthProviderSection = sections.get(
-        PLUGIN_SECTION, pluginName + GoogleOAuthService.CONFIG_SUFFIX);
-    this.githubOAuthProviderSection = sections.get(
-        PLUGIN_SECTION, pluginName + GitHubOAuthService.CONFIG_SUFFIX);
-    this.bitbucketOAuthProviderSection = sections.get(
-        PLUGIN_SECTION, pluginName + BitbucketOAuthService.CONFIG_SUFFIX);
-    this.casOAuthProviderSection = sections.get(
-        PLUGIN_SECTION, pluginName + CasOAuthService.CONFIG_SUFFIX);
+    this.googleOAuthProviderSection =
+        sections.get(PLUGIN_SECTION, pluginName + GoogleOAuthService.CONFIG_SUFFIX);
+    this.githubOAuthProviderSection =
+        sections.get(PLUGIN_SECTION, pluginName + GitHubOAuthService.CONFIG_SUFFIX);
+    this.bitbucketOAuthProviderSection =
+        sections.get(PLUGIN_SECTION, pluginName + BitbucketOAuthService.CONFIG_SUFFIX);
+    this.casOAuthProviderSection =
+        sections.get(PLUGIN_SECTION, pluginName + CasOAuthService.CONFIG_SUFFIX);
+    this.facebookOAuthProviderSection =
+        sections.get(PLUGIN_SECTION, pluginName + FacebookOAuthService.CONFIG_SUFFIX);
+    this.gitlabOAuthProviderSection =
+        sections.get(PLUGIN_SECTION, pluginName + GitLabOAuthService.CONFIG_SUFFIX);
+    this.dexOAuthProviderSection =
+        sections.get(PLUGIN_SECTION, pluginName + DexOAuthService.CONFIG_SUFFIX);
+    this.keycloakOAuthProviderSection =
+        sections.get(PLUGIN_SECTION, pluginName + KeycloakOAuthService.CONFIG_SUFFIX);
+    this.office365OAuthProviderSection =
+        sections.get(PLUGIN_SECTION, pluginName + Office365OAuthService.CONFIG_SUFFIX);
   }
 
   @Override
   public void run() throws Exception {
     ui.header("OAuth Authentication Provider");
 
-    boolean configureGoogleOAuthProvider = ui.yesno(
-        true, "Use Google OAuth provider for Gerrit login ?");
+    boolean configureGoogleOAuthProvider =
+        ui.yesno(true, "Use Google OAuth provider for Gerrit login ?");
     if (configureGoogleOAuthProvider) {
       configureOAuth(googleOAuthProviderSection);
-      googleOAuthProviderSection.string(
-          "Link to OpenID accounts?",
-          LINK_TO_EXISTING_OPENID_ACCOUNT, "true");
+      googleOAuthProviderSection.string(FIX_LEGACY_USER_ID_QUESTION, FIX_LEGACY_USER_ID, "false");
     }
 
-    boolean configueGitHubOAuthProvider = ui.yesno(
-        true, "Use GitHub OAuth provider for Gerrit login ?");
+    boolean configueGitHubOAuthProvider =
+        ui.yesno(true, "Use GitHub OAuth provider for Gerrit login ?");
     if (configueGitHubOAuthProvider) {
       configureOAuth(githubOAuthProviderSection);
+      githubOAuthProviderSection.string(FIX_LEGACY_USER_ID_QUESTION, FIX_LEGACY_USER_ID, "false");
     }
 
-    boolean configureBitbucketOAuthProvider = ui.yesno(
-        true, "Use Bitbucket OAuth provider for Gerrit login ?");
+    boolean configureBitbucketOAuthProvider =
+        ui.yesno(true, "Use Bitbucket OAuth provider for Gerrit login ?");
     if (configureBitbucketOAuthProvider) {
       configureOAuth(bitbucketOAuthProviderSection);
+      bitbucketOAuthProviderSection.string(
+          FIX_LEGACY_USER_ID_QUESTION, FIX_LEGACY_USER_ID, "false");
     }
 
-    boolean configureCasOAuthProvider = ui.yesno(
-        true, "Use CAS OAuth provider for Gerrit login ?");
+    boolean configureCasOAuthProvider = ui.yesno(true, "Use CAS OAuth provider for Gerrit login ?");
     if (configureCasOAuthProvider) {
       casOAuthProviderSection.string("CAS Root URL", ROOT_URL, null);
       configureOAuth(casOAuthProviderSection);
+      casOAuthProviderSection.string(FIX_LEGACY_USER_ID_QUESTION, FIX_LEGACY_USER_ID, "false");
+    }
+
+    boolean configueFacebookOAuthProvider =
+        ui.yesno(true, "Use Facebook OAuth provider for Gerrit login ?");
+    if (configueFacebookOAuthProvider) {
+      configureOAuth(facebookOAuthProviderSection);
+    }
+
+    boolean configureGitLabOAuthProvider =
+        ui.yesno(true, "Use GitLab OAuth provider for Gerrit login ?");
+    if (configureGitLabOAuthProvider) {
+      gitlabOAuthProviderSection.string("GitLab Root URL", ROOT_URL, null);
+      configureOAuth(gitlabOAuthProviderSection);
+    }
+
+    boolean configureDexOAuthProvider = ui.yesno(true, "Use Dex OAuth provider for Gerrit login ?");
+    if (configureDexOAuthProvider) {
+      dexOAuthProviderSection.string("Dex Root URL", ROOT_URL, null);
+      configureOAuth(dexOAuthProviderSection);
+    }
+
+    boolean configureKeycloakOAuthProvider =
+        ui.yesno(true, "Use Keycloak OAuth provider for Gerrit login ?");
+    if (configureKeycloakOAuthProvider) {
+      keycloakOAuthProviderSection.string("Keycloak Root URL", ROOT_URL, null);
+      keycloakOAuthProviderSection.string("Keycloak Realm", REALM, null);
+      configureOAuth(keycloakOAuthProviderSection);
+    }
+
+    boolean configureOffice365OAuthProvider =
+        ui.yesno(true, "Use Office365 OAuth provider for Gerrit login ?");
+    if (configureOffice365OAuthProvider) {
+      configureOAuth(office365OAuthProviderSection);
     }
   }
 
@@ -90,6 +139,5 @@
   }
 
   @Override
-  public void postRun() throws Exception {
-  }
+  public void postRun() throws Exception {}
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/oauth/KeycloakApi.java b/src/main/java/com/googlesource/gerrit/plugins/oauth/KeycloakApi.java
new file mode 100644
index 0000000..581d562
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/oauth/KeycloakApi.java
@@ -0,0 +1,68 @@
+// Copyright (C) 2017 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 org.scribe.builder.api.DefaultApi20;
+import org.scribe.extractors.AccessTokenExtractor;
+import org.scribe.extractors.JsonTokenExtractor;
+import org.scribe.model.OAuthConfig;
+import org.scribe.model.Verb;
+import org.scribe.oauth.OAuthService;
+import org.scribe.utils.OAuthEncoder;
+
+public class KeycloakApi extends DefaultApi20 {
+
+  private static final String AUTHORIZE_URL =
+      "%s/auth/realms/%s/protocol/openid-connect/auth?client_id=%s&response_type=code&redirect_uri=%s&scope=%s";
+
+  private final String rootUrl;
+  private final String realm;
+
+  public KeycloakApi(String rootUrl, String realm) {
+    this.rootUrl = rootUrl;
+    this.realm = realm;
+  }
+
+  @Override
+  public String getAuthorizationUrl(OAuthConfig config) {
+    return String.format(
+        AUTHORIZE_URL,
+        rootUrl,
+        realm,
+        config.getApiKey(),
+        OAuthEncoder.encode(config.getCallback()),
+        config.getScope().replaceAll(" ", "+"));
+  }
+
+  @Override
+  public String getAccessTokenEndpoint() {
+    return String.format("%s/auth/realms/%s/protocol/openid-connect/token", rootUrl, realm);
+  }
+
+  @Override
+  public Verb getAccessTokenVerb() {
+    return Verb.POST;
+  }
+
+  @Override
+  public OAuthService createService(OAuthConfig config) {
+    return new OAuth20ServiceImpl(this, config);
+  }
+
+  @Override
+  public AccessTokenExtractor getAccessTokenExtractor() {
+    return new JsonTokenExtractor();
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/oauth/KeycloakOAuthService.java b/src/main/java/com/googlesource/gerrit/plugins/oauth/KeycloakOAuthService.java
new file mode 100644
index 0000000..4b47fdf
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/oauth/KeycloakOAuthService.java
@@ -0,0 +1,138 @@
+// Copyright (C) 2017 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 static com.google.gerrit.server.OutputFormat.JSON;
+
+import com.google.common.base.CharMatcher;
+import com.google.common.base.Preconditions;
+import com.google.gerrit.extensions.annotations.PluginName;
+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.gerrit.server.config.PluginConfigFactory;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import java.io.IOException;
+import org.apache.commons.codec.binary.Base64;
+import org.scribe.builder.ServiceBuilder;
+import org.scribe.model.Token;
+import org.scribe.model.Verifier;
+import org.scribe.oauth.OAuthService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class KeycloakOAuthService implements OAuthServiceProvider {
+
+  private static final Logger log = LoggerFactory.getLogger(KeycloakOAuthService.class);
+
+  static final String CONFIG_SUFFIX = "-keycloak-oauth";
+  private static final String KEYCLOAK_PROVIDER_PREFIX = "keycloak-oauth:";
+  private final OAuthService service;
+  private final String serviceName;
+
+  @Inject
+  KeycloakOAuthService(
+      PluginConfigFactory cfgFactory,
+      @PluginName String pluginName,
+      @CanonicalWebUrl Provider<String> urlProvider) {
+    PluginConfig cfg = cfgFactory.getFromGerritConfig(pluginName + CONFIG_SUFFIX);
+    String canonicalWebUrl = CharMatcher.is('/').trimTrailingFrom(urlProvider.get()) + "/";
+
+    String rootUrl = cfg.getString(InitOAuth.ROOT_URL);
+    String realm = cfg.getString(InitOAuth.REALM);
+    serviceName = cfg.getString(InitOAuth.SERVICE_NAME, "Keycloak OAuth2");
+
+    service =
+        new ServiceBuilder()
+            .provider(new KeycloakApi(rootUrl, realm))
+            .apiKey(cfg.getString(InitOAuth.CLIENT_ID))
+            .apiSecret(cfg.getString(InitOAuth.CLIENT_SECRET))
+            .scope("openid")
+            .callback(canonicalWebUrl + "oauth")
+            .build();
+  }
+
+  private String parseJwt(String input) {
+    String[] parts = input.split("\\.");
+    Preconditions.checkState(parts.length == 3);
+    Preconditions.checkNotNull(parts[1]);
+    return new String(Base64.decodeBase64(parts[1]));
+  }
+
+  @Override
+  public OAuthUserInfo getUserInfo(OAuthToken token) throws IOException {
+    JsonElement tokenJson = JSON.newGson().fromJson(token.getRaw(), JsonElement.class);
+    JsonObject tokenObject = tokenJson.getAsJsonObject();
+    JsonElement id_token = tokenObject.get("id_token");
+
+    JsonElement claimJson =
+        JSON.newGson().fromJson(parseJwt(id_token.getAsString()), JsonElement.class);
+
+    JsonObject claimObject = claimJson.getAsJsonObject();
+    if (log.isDebugEnabled()) {
+      log.debug("Claim object: {}", claimObject);
+    }
+    JsonElement usernameElement = claimObject.get("preferred_username");
+    JsonElement emailElement = claimObject.get("email");
+    JsonElement nameElement = claimObject.get("name");
+    if (usernameElement == null || usernameElement.isJsonNull()) {
+      throw new IOException("Response doesn't contain preferred_username field");
+    }
+    if (emailElement == null || emailElement.isJsonNull()) {
+      throw new IOException("Response doesn't contain email field");
+    }
+    if (nameElement == null || nameElement.isJsonNull()) {
+      throw new IOException("Response doesn't contain name field");
+    }
+    String username = usernameElement.getAsString();
+    String email = emailElement.getAsString();
+    String name = nameElement.getAsString();
+
+    return new OAuthUserInfo(
+        KEYCLOAK_PROVIDER_PREFIX + username /*externalId*/,
+        username /*username*/,
+        email /*email*/,
+        name /*displayName*/,
+        null /*claimedIdentity*/);
+  }
+
+  @Override
+  public OAuthToken getAccessToken(OAuthVerifier rv) {
+    Verifier vi = new Verifier(rv.getValue());
+    Token to = service.getAccessToken(null, vi);
+    return new OAuthToken(to.getToken(), to.getSecret(), to.getRawResponse());
+  }
+
+  @Override
+  public String getAuthorizationUrl() {
+    return service.getAuthorizationUrl(null);
+  }
+
+  @Override
+  public String getVersion() {
+    return service.getVersion();
+  }
+
+  @Override
+  public String getName() {
+    return serviceName;
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/oauth/Module.java b/src/main/java/com/googlesource/gerrit/plugins/oauth/Module.java
new file mode 100644
index 0000000..72d59d1
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/oauth/Module.java
@@ -0,0 +1,37 @@
+// Copyright (C) 2017 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.google.gerrit.extensions.annotations.Exports;
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.extensions.auth.oauth.OAuthLoginProvider;
+import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
+
+public class Module extends AbstractModule {
+  private final String pluginName;
+
+  @Inject
+  Module(@PluginName String pluginName) {
+    this.pluginName = pluginName;
+  }
+
+  @Override
+  protected void configure() {
+    bind(OAuthLoginProvider.class)
+        .annotatedWith(Exports.named(pluginName))
+        .to(DisabledOAuthLoginProvider.class);
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/oauth/OAuth20ServiceImpl.java b/src/main/java/com/googlesource/gerrit/plugins/oauth/OAuth20ServiceImpl.java
new file mode 100644
index 0000000..a9eaddb
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/oauth/OAuth20ServiceImpl.java
@@ -0,0 +1,83 @@
+// Copyright (C) 2017 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 org.scribe.builder.api.DefaultApi20;
+import org.scribe.model.OAuthConfig;
+import org.scribe.model.OAuthConstants;
+import org.scribe.model.OAuthRequest;
+import org.scribe.model.Response;
+import org.scribe.model.Token;
+import org.scribe.model.Verifier;
+import org.scribe.oauth.OAuthService;
+
+/** TODO(gildur): remove when updating to newer scribe lib */
+final class OAuth20ServiceImpl implements OAuthService {
+
+  private static final String VERSION = "2.0";
+
+  private static final String GRANT_TYPE = "grant_type";
+  private static final String GRANT_TYPE_VALUE = "authorization_code";
+
+  private final DefaultApi20 api;
+  private final OAuthConfig config;
+
+  /**
+   * Default constructor
+   *
+   * @param api OAuth2.0 api information
+   * @param config OAuth 2.0 configuration param object
+   */
+  public OAuth20ServiceImpl(DefaultApi20 api, OAuthConfig config) {
+    this.api = api;
+    this.config = config;
+  }
+
+  @Override
+  public Token getAccessToken(Token requestToken, Verifier verifier) {
+    OAuthRequest request = new OAuthRequest(api.getAccessTokenVerb(), api.getAccessTokenEndpoint());
+    request.addBodyParameter(OAuthConstants.CLIENT_ID, config.getApiKey());
+    request.addBodyParameter(OAuthConstants.CLIENT_SECRET, config.getApiSecret());
+    request.addBodyParameter(OAuthConstants.CODE, verifier.getValue());
+    request.addBodyParameter(OAuthConstants.REDIRECT_URI, config.getCallback());
+    if (config.hasScope()) {
+      request.addBodyParameter(OAuthConstants.SCOPE, config.getScope());
+    }
+    request.addBodyParameter(GRANT_TYPE, GRANT_TYPE_VALUE);
+    Response response = request.send();
+    return api.getAccessTokenExtractor().extract(response.getBody());
+  }
+
+  @Override
+  public Token getRequestToken() {
+    throw new UnsupportedOperationException(
+        "Unsupported operation, please use 'getAuthorizationUrl' and redirect your users there");
+  }
+
+  @Override
+  public String getVersion() {
+    return VERSION;
+  }
+
+  @Override
+  public void signRequest(Token accessToken, OAuthRequest request) {
+    request.addQuerystringParameter(OAuthConstants.ACCESS_TOKEN, accessToken.getToken());
+  }
+
+  @Override
+  public String getAuthorizationUrl(Token requestToken) {
+    return api.getAuthorizationUrl(config);
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/oauth/OAuth2AccessTokenJsonExtractor.java b/src/main/java/com/googlesource/gerrit/plugins/oauth/OAuth2AccessTokenJsonExtractor.java
new file mode 100644
index 0000000..6c2f1a0
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/oauth/OAuth2AccessTokenJsonExtractor.java
@@ -0,0 +1,49 @@
+// Copyright (C) 2018 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 static org.scribe.model.OAuthConstants.ACCESS_TOKEN;
+
+import com.google.common.annotations.VisibleForTesting;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import org.scribe.exceptions.OAuthException;
+import org.scribe.extractors.AccessTokenExtractor;
+import org.scribe.model.Token;
+import org.scribe.utils.Preconditions;
+
+class OAuth2AccessTokenJsonExtractor implements AccessTokenExtractor {
+  private static final Pattern ACCESS_TOKEN_REGEX_PATTERN =
+      Pattern.compile("\"" + ACCESS_TOKEN + "\"\\s*:\\s*\"(\\S*?)\"");
+
+  private OAuth2AccessTokenJsonExtractor() {}
+
+  private static final AccessTokenExtractor INSTANCE = new OAuth2AccessTokenJsonExtractor();
+
+  static AccessTokenExtractor instance() {
+    return INSTANCE;
+  }
+
+  @VisibleForTesting
+  @Override
+  public Token extract(String response) {
+    Preconditions.checkEmptyString(response, "Cannot extract a token from a null or empty String");
+    Matcher matcher = ACCESS_TOKEN_REGEX_PATTERN.matcher(response);
+    if (matcher.find()) {
+      return new Token(matcher.group(1), "", response);
+    }
+    throw new OAuthException("Cannot extract an access token. Response was: " + response);
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/oauth/Office365Api.java b/src/main/java/com/googlesource/gerrit/plugins/oauth/Office365Api.java
new file mode 100644
index 0000000..8a28520
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/oauth/Office365Api.java
@@ -0,0 +1,62 @@
+// Copyright (C) 2018 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 static org.scribe.utils.OAuthEncoder.encode;
+
+import org.scribe.builder.api.DefaultApi20;
+import org.scribe.extractors.AccessTokenExtractor;
+import org.scribe.model.OAuthConfig;
+import org.scribe.model.Verb;
+import org.scribe.oauth.OAuthService;
+import org.scribe.utils.Preconditions;
+
+public class Office365Api extends DefaultApi20 {
+  private static final String AUTHORIZE_URL =
+      "https://login.microsoftonline.com/organizations/oauth2/v2.0/authorize?client_id=%s&response_type=code&redirect_uri=%s&scope=%s";
+
+  @Override
+  public String getAccessTokenEndpoint() {
+    return "https://login.microsoftonline.com/organizations/oauth2/v2.0/token";
+  }
+
+  @Override
+  public String getAuthorizationUrl(OAuthConfig config) {
+    Preconditions.checkValidUrl(
+        config.getCallback(),
+        "Must provide a valid url as callback. Office365 does not support OOB");
+    Preconditions.checkEmptyString(
+        config.getScope(),
+        "Must provide a valid value as scope. Office365 does not support no scope");
+
+    return String.format(
+        AUTHORIZE_URL, config.getApiKey(), encode(config.getCallback()), encode(config.getScope()));
+  }
+
+  @Override
+  public Verb getAccessTokenVerb() {
+    return Verb.POST;
+  }
+
+  @Override
+  public OAuthService createService(OAuthConfig config) {
+    return new OAuth20ServiceImpl(this, config);
+  }
+
+  @Override
+  public AccessTokenExtractor getAccessTokenExtractor() {
+    return OAuth2AccessTokenJsonExtractor.instance();
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/oauth/Office365OAuthService.java b/src/main/java/com/googlesource/gerrit/plugins/oauth/Office365OAuthService.java
new file mode 100644
index 0000000..360b650
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/oauth/Office365OAuthService.java
@@ -0,0 +1,143 @@
+// Copyright (C) 2018 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.google.common.base.CharMatcher;
+import com.google.gerrit.extensions.annotations.PluginName;
+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.OutputFormat;
+import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.gerrit.server.config.PluginConfig;
+import com.google.gerrit.server.config.PluginConfigFactory;
+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 java.io.IOException;
+import javax.servlet.http.HttpServletResponse;
+import org.scribe.builder.ServiceBuilder;
+import org.scribe.model.OAuthRequest;
+import org.scribe.model.Response;
+import org.scribe.model.Token;
+import org.scribe.model.Verb;
+import org.scribe.model.Verifier;
+import org.scribe.oauth.OAuthService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Singleton
+class Office365OAuthService implements OAuthServiceProvider {
+  private static final Logger log = LoggerFactory.getLogger(Office365OAuthService.class);
+  static final String CONFIG_SUFFIX = "-office365-oauth";
+  private static final String OFFICE365_PROVIDER_PREFIX = "office365-oauth:";
+  private static final String PROTECTED_RESOURCE_URL = "https://graph.microsoft.com/v1.0/me";
+  private static final String SCOPE =
+      "openid offline_access https://graph.microsoft.com/user.readbasic.all";
+  private final OAuthService service;
+  private final String canonicalWebUrl;
+  private final boolean useEmailAsUsername;
+
+  @Inject
+  Office365OAuthService(
+      PluginConfigFactory cfgFactory,
+      @PluginName String pluginName,
+      @CanonicalWebUrl Provider<String> urlProvider) {
+    PluginConfig cfg = cfgFactory.getFromGerritConfig(pluginName + CONFIG_SUFFIX);
+    this.canonicalWebUrl = CharMatcher.is('/').trimTrailingFrom(urlProvider.get()) + "/";
+    this.useEmailAsUsername = cfg.getBoolean(InitOAuth.USE_EMAIL_AS_USERNAME, false);
+    this.service =
+        new ServiceBuilder()
+            .provider(Office365Api.class)
+            .apiKey(cfg.getString(InitOAuth.CLIENT_ID))
+            .apiSecret(cfg.getString(InitOAuth.CLIENT_SECRET))
+            .callback(canonicalWebUrl + "oauth")
+            .scope(SCOPE)
+            .build();
+    if (log.isDebugEnabled()) {
+      log.debug("OAuth2: canonicalWebUrl={}", canonicalWebUrl);
+      log.debug("OAuth2: scope={}", SCOPE);
+      log.debug("OAuth2: useEmailAsUsername={}", useEmailAsUsername);
+    }
+  }
+
+  @Override
+  public OAuthUserInfo getUserInfo(OAuthToken token) throws IOException {
+    OAuthRequest request = new OAuthRequest(Verb.GET, PROTECTED_RESOURCE_URL);
+    request.addHeader("Accept", "*/*");
+    request.addHeader("Authorization", "Bearer " + token.getToken());
+    Response response = request.send();
+    if (response.getCode() != HttpServletResponse.SC_OK) {
+      throw new IOException(
+          String.format(
+              "Status %s (%s) for request %s",
+              response.getCode(), response.getBody(), request.getUrl()));
+    }
+    JsonElement userJson =
+        OutputFormat.JSON.newGson().fromJson(response.getBody(), JsonElement.class);
+    if (log.isDebugEnabled()) {
+      log.debug("User info response: {}", response.getBody());
+    }
+    if (userJson.isJsonObject()) {
+      JsonObject jsonObject = userJson.getAsJsonObject();
+      JsonElement id = jsonObject.get("id");
+      if (id == null || id.isJsonNull()) {
+        throw new IOException(String.format("Response doesn't contain id field"));
+      }
+      JsonElement email = jsonObject.get("mail");
+      JsonElement name = jsonObject.get("displayName");
+      String login = null;
+
+      if (useEmailAsUsername && !email.isJsonNull()) {
+        login = email.getAsString().split("@")[0];
+      }
+      return new OAuthUserInfo(
+          OFFICE365_PROVIDER_PREFIX + id.getAsString() /*externalId*/,
+          login /*username*/,
+          email == null || email.isJsonNull() ? null : email.getAsString() /*email*/,
+          name == null || name.isJsonNull() ? null : name.getAsString() /*displayName*/,
+          null);
+    }
+
+    throw new IOException(String.format("Invalid JSON '%s': not a JSON Object", userJson));
+  }
+
+  @Override
+  public OAuthToken getAccessToken(OAuthVerifier rv) {
+    Verifier vi = new Verifier(rv.getValue());
+    Token to = service.getAccessToken(null, vi);
+    OAuthToken result = new OAuthToken(to.getToken(), to.getSecret(), to.getRawResponse());
+    return result;
+  }
+
+  @Override
+  public String getAuthorizationUrl() {
+    String url = service.getAuthorizationUrl(null);
+    return url;
+  }
+
+  @Override
+  public String getVersion() {
+    return service.getVersion();
+  }
+
+  @Override
+  public String getName() {
+    return "Office365 OAuth2";
+  }
+}
diff --git a/src/main/resources/Documentation/build.md b/src/main/resources/Documentation/build.md
index 4c0fd4a..7237c9c 100644
--- a/src/main/resources/Documentation/build.md
+++ b/src/main/resources/Documentation/build.md
@@ -1,41 +1,32 @@
 Build
 =====
 
-This plugin is built with Buck.
+This plugin is built with Bazel. To install Bazel, follow
+the instruction on: https://www.bazel.io/versions/master/docs/install.html.
 
 Two build modes are supported: Standalone and in Gerrit tree.
-The standalone build mode is recommended, as this mode doesn't require
-the Gerrit tree to exist locally.
+The standalone build mode is recommended, as this mode doesn't
+require the Gerrit tree to exist locally.
 
-Build in Standalone mode
-------------------------
+### Build standalone
+
+Clone the plugin:
 
 ```
-  git clone --recursive https://github.com/davido/gerrit-oauth-provider
-  cd gerrit-oauth-provider
-  buck build plugin
+  git clone https://gerrit.googlesource.com/plugins/oauth
+  cd oauth
+```
+
+Issue the command:
+
+```
+  bazel build :all
 ```
 
 The output is created in
 
 ```
-  buck-out/gen/@PLUGIN@.jar
-```
-
-Build in Gerrit tree
---------------------
-
-Clone or link this plugin to the plugins directory of Gerrit's source
-tree, and issue the command:
-
-```
-  buck build plugins/gerrit-oauth-provider
-```
-
-The output is created in
-
-```
-  buck-out/gen/plugins/@PLUGIN@/@PLUGIN@.jar
+  bazel-genfiles/@PLUGIN@.jar
 ```
 
 This project can be imported into the Eclipse IDE:
@@ -43,3 +34,44 @@
 ```
   ./tools/eclipse/project.py
 ```
+
+### Build in Gerrit tree
+
+Clone or link this plugin to the plugins directory of Gerrit's
+source tree, and issue the command:
+
+```
+  git clone https://gerrit.googlesource.com/gerrit
+  git clone https://gerrit.googlesource.com/plugins/@PLUGIN@
+  cd gerrit/plugins
+  ln -s ../../@PLUGIN@ .
+```
+
+Put the external dependency Bazel build file into the Gerrit /plugins
+directory, replacing the existing empty one.
+
+```
+  cd gerrit/plugins
+  rm external_plugin_deps.bzl
+  ln -s @PLUGIN@/external_plugin_deps.bzl .
+```
+
+From Gerrit source tree issue the command:
+
+```
+  bazel build plugins/@PLUGIN@
+```
+
+The output is created in
+
+```
+  bazel-genfiles/plugins/@PLUGIN@/@PLUGIN@.jar
+```
+
+This project can be imported into the Eclipse IDE.
+Add the plugin name to the `CUSTOM_PLUGINS` set in
+Gerrit core in `tools/bzl/plugins.bzl`, and execute:
+
+```
+  ./tools/eclipse/project.py
+```
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index 8c1f9a2..c16d3ef 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -26,6 +26,18 @@
     root-url = "<cas url>"
     client-id = "<client-id>"
     client-secret = "<client-secret>"
+
+  [plugin "@PLUGIN@-gitlab-oauth"]
+    root-url = "<gitlab url>"
+    client-id = "<client-id>"
+    client-secret = "<client-secret>"
+
+  [plugin "@PLUGIN@-dex-oauth"]
+    domain = "<domain for username manipulation (optional)>"
+    service-name = "<custom service name (optional)>"
+    root-url = "<dex url>"
+    client-id = "<client-id>"
+    client-secret = "<client-secret>"
 ```
 
 When one from the sections above is omitted, OAuth SSO is used.
@@ -42,13 +54,22 @@
 
 to Google OAuth configuration section.
 
-It is possile to restrict sign-in to accounts of one (hosted) domain for
-Google OAuth. The `domain` option can be added:
+It is possile to restrict sign-in to accounts of one or more (hosted) domains for
+Google OAuth. Multiple `domain` options can be added:
 
 ```
 plugin.gerrit-oauth-provider-google-oauth.domain = "mycollege.edu"
+plugin.gerrit-oauth-provider-google-oauth.domain = "myschool.net"
 ```
 
+(See the spec)[https://developers.google.com/identity/protocols/OpenIDConnect#hd-param]
+for more information. To protect against client-side request modification, the returned
+ID token is checked to contain a matching hd claim (which is proof the account does belong
+to the hosted domain). If the hd claim wasn't included in ID token or didn't match the
+provided `domain` configuration option the authentication is rejected. Note: Because of
+current limitation of the OAuth extension point in gerrit (blame /me for that) the user
+would only see "Unauthorized" message.
+
 By default the Google OAuth provider will not set a username (used for ssh) and
 the user can choose one from the web ui (needed before using ssh). It is possible
 to automatically use the user part from the google apps email. This is deactivated
@@ -61,14 +82,6 @@
 Note: the usernames are unique in gerrit. If a username already exists this will
 be ignored and the user will have to choose a different one from the web ui.
 
-(See the spec)[https://developers.google.com/identity/protocols/OpenIDConnect#hd-param]
-for more information. To protect against client-side request modification, the returned
-ID token is checked to contain a matching hd claim (which is proof the account does belong
-to the hosted domain). If the hd claim wasn't included in ID token or didn't match the
-provided `domain` configuration option the authentication is rejected. Note: Because of
-current limitation of the OAuth extension point in gerrit (blame /me for that) the user
-would only see "Unauthorized" message.
-
 ### CAS OAuth
 
 For CAS OAuth setting
@@ -79,6 +92,8 @@
 
 is required, since CAS is a self-hosted application.
 
+Note that the CAS OAuth plugin only supports CAS V5 and higher.
+
 The plugin expects CAS to make several attributes available to it:
 
 | Name | Description | Required |
@@ -88,6 +103,16 @@
 | email |  Email address | no |
 | name | Display name | no |
 
+### CoreOS Dex OAuth
+
+For Dex OAuth setting
+
+```
+plugin.gerrit-oauth-provider-dex-oauth.root-url = "https://example.com"
+```
+
+is required, since Dex is a self-hosted application.
+
 ## Obtaining provider authorizations
 
 ### Google
@@ -147,3 +172,31 @@
 See
 [the CAS documentation](https://apereo.github.io/cas/4.2.x/installation/OAuth-OpenId-Authentication.html#add-oauth-clients)
 for an example.
+
+### GitLab
+
+To obtain client-id and client-secret for GitLab OAuth, go to
+Applications settings in your GitLab profile:
+
+- Select "Save application" and enter information about the
+  application.
+
+  Note that it is important that Redirect URI points to
+    `<canonical-web-uri-of-gerrit>/oauth`.
+
+  ![Save new application on GitLab](images/gitlab-1.png)
+
+
+After application is saved, the page will show generated client id and
+secret.
+
+![Generated client id and secret](images/gitlab-2.png)
+
+### CoreOS Dex
+
+The client-id and client-secret for Dex OAuth are part of the Dex
+setup and need to be set manually.
+
+See
+[Using Dex](https://github.com/coreos/dex/blob/master/Documentation/using-dex.md)
+for an example.
diff --git a/src/main/resources/Documentation/images/gitlab-1.png b/src/main/resources/Documentation/images/gitlab-1.png
new file mode 100644
index 0000000..aff16ba
--- /dev/null
+++ b/src/main/resources/Documentation/images/gitlab-1.png
Binary files differ
diff --git a/src/main/resources/Documentation/images/gitlab-2.png b/src/main/resources/Documentation/images/gitlab-2.png
new file mode 100644
index 0000000..c575bf3
--- /dev/null
+++ b/src/main/resources/Documentation/images/gitlab-2.png
Binary files differ
diff --git a/src/test/java/com/googlesource/gerrit/plugins/oauth/OAuth2AccessTokenJsonExtractorTest.java b/src/test/java/com/googlesource/gerrit/plugins/oauth/OAuth2AccessTokenJsonExtractorTest.java
new file mode 100644
index 0000000..09df7ee
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/oauth/OAuth2AccessTokenJsonExtractorTest.java
@@ -0,0 +1,70 @@
+// Copyright (C) 2018 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 static org.junit.Assert.assertEquals;
+import static org.scribe.model.OAuthConstants.ACCESS_TOKEN;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.scribe.exceptions.OAuthException;
+import org.scribe.extractors.AccessTokenExtractor;
+import org.scribe.model.Token;
+
+public class OAuth2AccessTokenJsonExtractorTest {
+  private static final AccessTokenExtractor extractor = OAuth2AccessTokenJsonExtractor.instance();
+  private static final String TOKEN = "I0122HHJKLEM21F3WLPYHDKGKZULAUO4SGMV3ABKFTDT3T3X";
+  private static final String RESPONSE = "{\"" + ACCESS_TOKEN + "\":\"" + TOKEN + "\"}'";
+  private static final String RESPONSE_NON_JSON = ACCESS_TOKEN + "=" + TOKEN;
+  private static final String RESPONSE_WITH_BLANKS =
+      "{ \"" + ACCESS_TOKEN + "\" : \"" + TOKEN + "\"}'";
+  private static final String MESSAGE = "Cannot extract a token from a null or empty String";
+
+  @Rule public ExpectedException exception = ExpectedException.none();
+
+  @Test
+  public void parseResponse() throws Exception {
+    Token token = extractor.extract(RESPONSE);
+    assertEquals(token.getToken(), TOKEN);
+  }
+
+  @Test
+  public void parseResponseWithBlanks() throws Exception {
+    Token token = extractor.extract(RESPONSE_WITH_BLANKS);
+    assertEquals(token.getToken(), TOKEN);
+  }
+
+  @Test
+  public void failParseNonJsonResponse() throws Exception {
+    exception.expect(OAuthException.class);
+    exception.expectMessage("Cannot extract an access token. Response was: " + RESPONSE_NON_JSON);
+    extractor.extract(RESPONSE_NON_JSON);
+  }
+
+  @Test
+  public void shouldThrowExceptionIfForNullParameter() throws Exception {
+    exception.expect(IllegalArgumentException.class);
+    exception.expectMessage(MESSAGE);
+    extractor.extract(null);
+  }
+
+  @Test
+  public void shouldThrowExceptionIfForEmptyString() throws Exception {
+    exception.expect(IllegalArgumentException.class);
+    exception.expectMessage(MESSAGE);
+    extractor.extract("");
+  }
+}
diff --git a/tools/bzl/BUILD b/tools/bzl/BUILD
new file mode 100644
index 0000000..c5ed0b7
--- /dev/null
+++ b/tools/bzl/BUILD
@@ -0,0 +1 @@
+# Empty file required by Bazel
diff --git a/tools/bzl/classpath.bzl b/tools/bzl/classpath.bzl
new file mode 100644
index 0000000..d5764f7
--- /dev/null
+++ b/tools/bzl/classpath.bzl
@@ -0,0 +1,4 @@
+load(
+    "@com_googlesource_gerrit_bazlets//tools:classpath.bzl",
+    "classpath_collector",
+)
diff --git a/tools/bzl/junit.bzl b/tools/bzl/junit.bzl
new file mode 100644
index 0000000..3af7e58
--- /dev/null
+++ b/tools/bzl/junit.bzl
@@ -0,0 +1,4 @@
+load(
+    "@com_googlesource_gerrit_bazlets//tools:junit.bzl",
+    "junit_tests",
+)
diff --git a/tools/bzl/maven_jar.bzl b/tools/bzl/maven_jar.bzl
new file mode 100644
index 0000000..2eabedb
--- /dev/null
+++ b/tools/bzl/maven_jar.bzl
@@ -0,0 +1 @@
+load("@com_googlesource_gerrit_bazlets//tools:maven_jar.bzl", "maven_jar")
diff --git a/tools/bzl/plugin.bzl b/tools/bzl/plugin.bzl
new file mode 100644
index 0000000..0b25d23
--- /dev/null
+++ b/tools/bzl/plugin.bzl
@@ -0,0 +1,6 @@
+load(
+    "@com_googlesource_gerrit_bazlets//:gerrit_plugin.bzl",
+    "PLUGIN_DEPS",
+    "PLUGIN_TEST_DEPS",
+    "gerrit_plugin",
+)
diff --git a/tools/eclipse/BUILD b/tools/eclipse/BUILD
new file mode 100644
index 0000000..f744058
--- /dev/null
+++ b/tools/eclipse/BUILD
@@ -0,0 +1,9 @@
+load("//tools/bzl:classpath.bzl", "classpath_collector")
+
+classpath_collector(
+    name = "main_classpath_collect",
+    testonly = 1,
+    deps = [
+        "//:oauth__plugin_test_deps",
+    ],
+)
diff --git a/tools/eclipse/project.sh b/tools/eclipse/project.sh
new file mode 100755
index 0000000..8e4ed79
--- /dev/null
+++ b/tools/eclipse/project.sh
@@ -0,0 +1,15 @@
+#!/bin/bash
+# Copyright (C) 2017 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.
+`bazel query @com_googlesource_gerrit_bazlets//tools/eclipse:project --output location | sed s/BUILD:.*//`project.py -n oauth -r .
diff --git a/tools/workspace-status.sh b/tools/workspace-status.sh
new file mode 100755
index 0000000..bb7b703
--- /dev/null
+++ b/tools/workspace-status.sh
@@ -0,0 +1,17 @@
+#!/bin/bash
+
+# This script will be run by bazel when the build process starts to
+# generate key-value information that represents the status of the
+# workspace. The output should be like
+#
+# KEY1 VALUE1
+# KEY2 VALUE2
+#
+# If the script exits with non-zero code, it's considered as a failure
+# and the output will be discarded.
+
+function rev() {
+  cd $1; git describe --always --match "v[0-9].*" --dirty
+}
+
+echo STABLE_BUILD_OAUTH_LABEL $(rev .)