Merge "Decode Keycloak JWTs as UTF-8"
diff --git a/BUILD b/BUILD
index b5d591b..0d2e0e4 100644
--- a/BUILD
+++ b/BUILD
@@ -22,6 +22,7 @@
     deps = [
         "@commons-codec//jar:neverlink",
         "@jackson-databind//jar",
+        "@scribejava-apis//jar",
         "@scribejava-core//jar",
     ],
 )
@@ -32,6 +33,7 @@
     tags = ["oauth"],
     deps = [
         ":oauth__plugin_test_deps",
+        "@scribejava-apis//jar",
         "@scribejava-core//jar",
     ],
 )
diff --git a/README.md b/README.md
index de036fb..3e70431 100644
--- a/README.md
+++ b/README.md
@@ -17,7 +17,7 @@
 * [Google](https://developers.google.com/identity/protocols/OAuth2)
 * [Keycloak](http://www.keycloak.org/)
 * [LemonLDAP::NG](https://lemonldap-ng.org)
-* [Office365](https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-v2-protocols)
+* [Azure (previously named Office365)](https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-v2-protocols)
 * [Phabricator](https://secure.phabricator.com/book/phabcontrib/article/using_oauthserver/)
 
 See the [Wiki](https://github.com/davido/gerrit-oauth-provider/wiki) what it can do for you.
diff --git a/external_plugin_deps.bzl b/external_plugin_deps.bzl
index 4615c8d..ffe8a31 100644
--- a/external_plugin_deps.bzl
+++ b/external_plugin_deps.bzl
@@ -2,12 +2,18 @@
 
 def external_plugin_deps(omit_commons_codec = True):
     JACKSON_VERS = "2.10.2"
+    SCRIBEJAVA_VERS = "6.9.0"
     maven_jar(
         name = "scribejava-core",
-        artifact = "com.github.scribejava:scribejava-core:6.9.0",
+        artifact = "com.github.scribejava:scribejava-core:" + SCRIBEJAVA_VERS,
         sha1 = "ed761f450d8382f75787e8fee9ae52e7ec768747",
     )
     maven_jar(
+        name = "scribejava-apis",
+        artifact = "com.github.scribejava:scribejava-apis:" + SCRIBEJAVA_VERS,
+        sha1 = "a374c7a36533e58e53b42b584a8b3751ab1e13c4",
+    )
+    maven_jar(
         name = "jackson-annotations",
         artifact = "com.fasterxml.jackson.core:jackson-annotations:" + JACKSON_VERS,
         sha1 = "3a13b6105946541b8d4181a0506355b5fae63260",
diff --git a/src/main/java/com/googlesource/gerrit/plugins/oauth/AzureActiveDirectoryService.java b/src/main/java/com/googlesource/gerrit/plugins/oauth/AzureActiveDirectoryService.java
new file mode 100644
index 0000000..81dc53f
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/oauth/AzureActiveDirectoryService.java
@@ -0,0 +1,242 @@
+// 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 com.google.gerrit.json.OutputFormat.JSON;
+
+import com.github.scribejava.apis.MicrosoftAzureActiveDirectory20Api;
+import com.github.scribejava.core.builder.ServiceBuilder;
+import com.github.scribejava.core.exceptions.OAuthException;
+import com.github.scribejava.core.model.OAuth2AccessToken;
+import com.github.scribejava.core.model.OAuthRequest;
+import com.github.scribejava.core.model.Response;
+import com.github.scribejava.core.model.Verb;
+import com.github.scribejava.core.oauth.OAuth20Service;
+import com.google.common.base.CharMatcher;
+import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.extensions.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.Gson;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.concurrent.ExecutionException;
+import javax.servlet.http.HttpServletResponse;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Singleton
+class AzureActiveDirectoryService implements OAuthServiceProvider {
+  private static final Logger log = LoggerFactory.getLogger(AzureActiveDirectoryService.class);
+  static final String CONFIG_SUFFIX_LEGACY = "-office365-oauth";
+  static final String CONFIG_SUFFIX = "-azure-oauth";
+  private static final String AZURE_PROVIDER_PREFIX = "azure-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";
+  public static final String DEFAULT_TENANT = "organizations";
+  private static final ImmutableSet<String> TENANTS_WITHOUT_VALIDATION =
+      ImmutableSet.<String>builder().add(DEFAULT_TENANT).add("common").add("consumers").build();
+  private final OAuth20Service service;
+  private final Gson gson;
+  private final String canonicalWebUrl;
+  private final boolean useEmailAsUsername;
+  private final String tenant;
+  private final String clientId;
+  private String providerPrefix;
+  private final boolean linkOffice365Id;
+
+  @Inject
+  AzureActiveDirectoryService(
+      PluginConfigFactory cfgFactory,
+      @PluginName String pluginName,
+      @CanonicalWebUrl Provider<String> urlProvider) {
+    PluginConfig cfg = cfgFactory.getFromGerritConfig(pluginName + CONFIG_SUFFIX);
+    providerPrefix = AZURE_PROVIDER_PREFIX;
+
+    // ?: Did we find the client_id with the CONFIG_SUFFIX
+    if (cfg.getString(InitOAuth.CLIENT_ID) == null) {
+      // -> No, we did not find the client_id in the azure config so we should try the old legacy
+      // office365 section
+      cfg = cfgFactory.getFromGerritConfig(pluginName + CONFIG_SUFFIX_LEGACY);
+      // We must also use the new provider prefix
+      providerPrefix = OFFICE365_PROVIDER_PREFIX;
+    }
+    this.linkOffice365Id = cfg.getBoolean(InitOAuth.LINK_TO_EXISTING_OFFICE365_ACCOUNT, false);
+    this.canonicalWebUrl = CharMatcher.is('/').trimTrailingFrom(urlProvider.get()) + "/";
+    this.useEmailAsUsername = cfg.getBoolean(InitOAuth.USE_EMAIL_AS_USERNAME, false);
+    this.tenant = cfg.getString(InitOAuth.TENANT, DEFAULT_TENANT);
+    this.clientId = cfg.getString(InitOAuth.CLIENT_ID);
+    this.service =
+        new ServiceBuilder(cfg.getString(InitOAuth.CLIENT_ID))
+            .apiSecret(cfg.getString(InitOAuth.CLIENT_SECRET))
+            .callback(canonicalWebUrl + "oauth")
+            .defaultScope(SCOPE)
+            .build(MicrosoftAzureActiveDirectory20Api.custom(tenant));
+    this.gson = JSON.newGson();
+    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 {
+    // ?: Have we set a custom tenant and is this a tenant other than the one set in
+    // TENANTS_WITHOUT_VALIDATION
+    if (!TENANTS_WITHOUT_VALIDATION.contains(tenant)) {
+      // -> Yes, we are using a tenant that should be validated, so verify that is issued for the
+      // same one that we
+      // have set.
+      String tid = getTokenJson(token.getToken()).get("tid").getAsString();
+
+      // ?: Verify that this token has the same tenant as we are currently using
+      if (!tenant.equals(tid)) {
+        // -> No, this tenant does not equals the one in the token. So we should stop processing
+        log.warn(
+            String.format(
+                "The token was issued by the tenant [%s] while we are set to use [%s]",
+                tid, tenant));
+        // Return null so the user will be shown Unauthorized.
+        return null;
+      }
+    }
+
+    // Due to scribejava does not expose the id_token we need to do this a bit convoluted way to
+    // extract this our self
+    // see <a href="https://github.com/scribejava/scribejava/issues/968">Obtaining id_token from
+    // access_token</a> for
+    // the scribejava issue on this.
+    String rawToken = token.getRaw();
+    JsonObject jwtJson = gson.fromJson(rawToken, JsonObject.class);
+    String idTokenBase64 = jwtJson.get("id_token").getAsString();
+    String aud = getTokenJson(idTokenBase64).get("aud").getAsString();
+
+    // ?: Does this token have the same clientId set in the 'aud' part of the id_token as we are
+    // using.
+    // If not we should reject it
+    // see <a href="https://docs.microsoft.com/en-us/azure/active-directory/develop/id-tokens">id
+    // tokens Payload claims></a>
+    // for information on the aud claim.
+    if (!clientId.equals(aud)) {
+      log.warn(
+          String.format(
+              "The id_token had aud [%s] while we expected it to be equal to the clientId [%s]",
+              aud, clientId));
+      // Return null so the user will be shown Unauthorized.
+      return null;
+    }
+
+    OAuthRequest request = new OAuthRequest(Verb.GET, PROTECTED_RESOURCE_URL);
+    OAuth2AccessToken t = new OAuth2AccessToken(token.getToken(), token.getRaw());
+    service.signRequest(t, request);
+    request.addHeader("Accept", "*/*");
+
+    JsonElement userJson = null;
+    try (Response response = service.execute(request)) {
+      if (response.getCode() != HttpServletResponse.SC_OK) {
+        throw new IOException(
+            String.format(
+                "Status %s (%s) for request %s",
+                response.getCode(), response.getBody(), request.getUrl()));
+      }
+      userJson = 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("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(
+            providerPrefix + id.getAsString() /*externalId*/,
+            login /*username*/,
+            email == null || email.isJsonNull() ? null : email.getAsString() /*email*/,
+            name == null || name.isJsonNull() ? null : name.getAsString() /*displayName*/,
+            linkOffice365Id ? OFFICE365_PROVIDER_PREFIX + id.getAsString() : null);
+      }
+    } catch (ExecutionException | InterruptedException e) {
+      throw new RuntimeException("Cannot retrieve user info resource", e);
+    }
+
+    throw new IOException(String.format("Invalid JSON '%s': not a JSON Object", userJson));
+  }
+
+  @Override
+  public OAuthToken getAccessToken(OAuthVerifier rv) {
+    try {
+      OAuth2AccessToken accessToken = service.getAccessToken(rv.getValue());
+      return new OAuthToken(
+          accessToken.getAccessToken(), accessToken.getTokenType(), accessToken.getRawResponse());
+    } catch (InterruptedException | ExecutionException | IOException e) {
+      String msg = "Cannot retrieve access token";
+      log.error(msg, e);
+      throw new RuntimeException(msg, e);
+    }
+  }
+
+  @Override
+  public String getAuthorizationUrl() {
+    String url = service.getAuthorizationUrl();
+    return url;
+  }
+
+  @Override
+  public String getVersion() {
+    return service.getVersion();
+  }
+
+  @Override
+  public String getName() {
+    return "Office365 OAuth2";
+  }
+
+  /** Get the {@link JsonObject} of a given token. */
+  private JsonObject getTokenJson(String tokenBase64) {
+    String[] tokenParts = tokenBase64.split("\\.");
+    if (tokenParts.length != 3) {
+      throw new OAuthException("Token does not contain expected number of parts");
+    }
+
+    // Extract the payload part from the JWT token (header.payload.signature) by retrieving
+    // tokenParts[1].
+    return gson.fromJson(
+        new String(Base64.getDecoder().decode(tokenParts[1]), StandardCharsets.UTF_8),
+        JsonObject.class);
+  }
+}
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 80bc605..b9517b2 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/oauth/HttpModule.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/oauth/HttpModule.java
@@ -20,6 +20,7 @@
 import com.google.gerrit.server.config.PluginConfig;
 import com.google.gerrit.server.config.PluginConfigFactory;
 import com.google.inject.Inject;
+import com.google.inject.ProvisionException;
 import com.google.inject.servlet.ServletModule;
 
 class HttpModule extends ServletModule {
@@ -99,11 +100,28 @@
           .to(KeycloakOAuthService.class);
     }
 
-    cfg = cfgFactory.getFromGerritConfig(pluginName + Office365OAuthService.CONFIG_SUFFIX);
+    boolean office365LegacyProviderBound = false;
+    cfg =
+        cfgFactory.getFromGerritConfig(
+            pluginName + AzureActiveDirectoryService.CONFIG_SUFFIX_LEGACY);
     if (cfg.getString(InitOAuth.CLIENT_ID) != null) {
+      office365LegacyProviderBound = true;
       bind(OAuthServiceProvider.class)
-          .annotatedWith(Exports.named(Office365OAuthService.CONFIG_SUFFIX))
-          .to(Office365OAuthService.class);
+          .annotatedWith(Exports.named(AzureActiveDirectoryService.CONFIG_SUFFIX))
+          .to(AzureActiveDirectoryService.class);
+    }
+    cfg = cfgFactory.getFromGerritConfig(pluginName + AzureActiveDirectoryService.CONFIG_SUFFIX);
+    if (cfg.getString(InitOAuth.CLIENT_ID) != null) {
+      // ?: Check if the legacy Office365 is already bound, we can only have one of these bound at
+      // one time
+      if (office365LegacyProviderBound) {
+        // -> Yes, the legacy Office365 is already bound and we are trying to bind the
+        // AzureActiveDirectoryService.CONFIG_SUFFIX at the same time.
+        throw new ProvisionException("Legacy Office365 OAuth provider is already bound!");
+      }
+      bind(OAuthServiceProvider.class)
+          .annotatedWith(Exports.named(AzureActiveDirectoryService.CONFIG_SUFFIX))
+          .to(AzureActiveDirectoryService.class);
     }
 
     cfg = cfgFactory.getFromGerritConfig(pluginName + AirVantageOAuthService.CONFIG_SUFFIX);
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 854ba5b..3fbb2ca 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/oauth/InitOAuth.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/oauth/InitOAuth.java
@@ -34,6 +34,8 @@
   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 TENANT = "tenant";
+  static final String LINK_TO_EXISTING_OFFICE365_ACCOUNT = "link-to-existing-office365-accounts";
   static final String SERVICE_NAME = "service-name";
   static String FIX_LEGACY_USER_ID_QUESTION = "Fix legacy user id, without oauth provider prefix?";
 
@@ -48,6 +50,7 @@
   private final Section dexOAuthProviderSection;
   private final Section keycloakOAuthProviderSection;
   private final Section office365OAuthProviderSection;
+  private final Section azureActiveDirectoryAuthProviderSection;
   private final Section airVantageOAuthProviderSection;
   private final Section phabricatorOAuthProviderSection;
 
@@ -73,7 +76,9 @@
     this.keycloakOAuthProviderSection =
         sections.get(PLUGIN_SECTION, pluginName + KeycloakOAuthService.CONFIG_SUFFIX);
     this.office365OAuthProviderSection =
-        sections.get(PLUGIN_SECTION, pluginName + Office365OAuthService.CONFIG_SUFFIX);
+        sections.get(PLUGIN_SECTION, pluginName + AzureActiveDirectoryService.CONFIG_SUFFIX_LEGACY);
+    this.azureActiveDirectoryAuthProviderSection =
+        sections.get(PLUGIN_SECTION, pluginName + AzureActiveDirectoryService.CONFIG_SUFFIX);
     this.airVantageOAuthProviderSection =
         sections.get(PLUGIN_SECTION, pluginName + AirVantageOAuthService.CONFIG_SUFFIX);
     this.phabricatorOAuthProviderSection =
@@ -158,12 +163,28 @@
       keycloakOAuthProviderSection.string("Keycloak Realm", REALM, null);
     }
 
-    boolean configureOffice365OAuthProvider =
-        ui.yesno(
-            isConfigured(office365OAuthProviderSection),
-            "Use Office365 OAuth provider for Gerrit login ?");
-    if (configureOffice365OAuthProvider) {
-      configureOAuth(office365OAuthProviderSection);
+    // ?: Are there legacy office365 already configured on the system?
+    if (isConfigured(office365OAuthProviderSection)) {
+      // -> Yes, this system has already configured the old legacy office365.
+      boolean configureOffice365OAuthProvider =
+          ui.yesno(
+              isConfigured(office365OAuthProviderSection),
+              "Use Office365 OAuth provider for Gerrit login ?");
+      if (configureOffice365OAuthProvider) {
+        configureOAuth(office365OAuthProviderSection);
+      }
+    }
+    // E-> No, we either are setting up on an new system or using the new azure config
+    else {
+      boolean configureAzureActiveDirectoryAuthProvider =
+          ui.yesno(
+              isConfigured(azureActiveDirectoryAuthProviderSection),
+              "Use Azure OAuth provider for Gerrit login ?");
+      if (configureAzureActiveDirectoryAuthProvider) {
+        configureOAuth(azureActiveDirectoryAuthProviderSection);
+        azureActiveDirectoryAuthProviderSection.string(
+            "Tenant", TENANT, AzureActiveDirectoryService.DEFAULT_TENANT);
+      }
     }
 
     boolean configureAirVantageOAuthProvider =
diff --git a/src/main/java/com/googlesource/gerrit/plugins/oauth/Office365Api.java b/src/main/java/com/googlesource/gerrit/plugins/oauth/Office365Api.java
deleted file mode 100644
index 7e9bef8..0000000
--- a/src/main/java/com/googlesource/gerrit/plugins/oauth/Office365Api.java
+++ /dev/null
@@ -1,36 +0,0 @@
-// 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.github.scribejava.core.builder.api.DefaultApi20;
-import com.github.scribejava.core.oauth2.clientauthentication.ClientAuthentication;
-import com.github.scribejava.core.oauth2.clientauthentication.RequestBodyAuthenticationScheme;
-
-public class Office365Api extends DefaultApi20 {
-  @Override
-  public String getAccessTokenEndpoint() {
-    return "https://login.microsoftonline.com/organizations/oauth2/v2.0/token";
-  }
-
-  @Override
-  public String getAuthorizationBaseUrl() {
-    return "https://login.microsoftonline.com/organizations/oauth2/v2.0/authorize";
-  }
-
-  @Override
-  public ClientAuthentication getClientAuthentication() {
-    return RequestBodyAuthenticationScheme.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
deleted file mode 100644
index 4e0803b..0000000
--- a/src/main/java/com/googlesource/gerrit/plugins/oauth/Office365OAuthService.java
+++ /dev/null
@@ -1,152 +0,0 @@
-// 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 com.google.gerrit.json.OutputFormat.JSON;
-
-import com.github.scribejava.core.builder.ServiceBuilder;
-import com.github.scribejava.core.model.OAuth2AccessToken;
-import com.github.scribejava.core.model.OAuthRequest;
-import com.github.scribejava.core.model.Response;
-import com.github.scribejava.core.model.Verb;
-import com.github.scribejava.core.oauth.OAuth20Service;
-import com.google.common.base.CharMatcher;
-import com.google.gerrit.extensions.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 java.util.concurrent.ExecutionException;
-import javax.servlet.http.HttpServletResponse;
-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 OAuth20Service 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(cfg.getString(InitOAuth.CLIENT_ID))
-            .apiSecret(cfg.getString(InitOAuth.CLIENT_SECRET))
-            .callback(canonicalWebUrl + "oauth")
-            .defaultScope(SCOPE)
-            .build(new Office365Api());
-    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);
-    OAuth2AccessToken t = new OAuth2AccessToken(token.getToken(), token.getRaw());
-    service.signRequest(t, request);
-    request.addHeader("Accept", "*/*");
-
-    JsonElement userJson = null;
-    try (Response response = service.execute(request)) {
-      if (response.getCode() != HttpServletResponse.SC_OK) {
-        throw new IOException(
-            String.format(
-                "Status %s (%s) for request %s",
-                response.getCode(), response.getBody(), request.getUrl()));
-      }
-      userJson = 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("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);
-      }
-    } catch (ExecutionException | InterruptedException e) {
-      throw new RuntimeException("Cannot retrieve user info resource", e);
-    }
-
-    throw new IOException(String.format("Invalid JSON '%s': not a JSON Object", userJson));
-  }
-
-  @Override
-  public OAuthToken getAccessToken(OAuthVerifier rv) {
-    try {
-      OAuth2AccessToken accessToken = service.getAccessToken(rv.getValue());
-      return new OAuthToken(
-          accessToken.getAccessToken(), accessToken.getTokenType(), accessToken.getRawResponse());
-    } catch (InterruptedException | ExecutionException | IOException e) {
-      String msg = "Cannot retrieve access token";
-      log.error(msg, e);
-      throw new RuntimeException(msg, e);
-    }
-  }
-
-  @Override
-  public String getAuthorizationUrl() {
-    String url = service.getAuthorizationUrl();
-    return url;
-  }
-
-  @Override
-  public String getVersion() {
-    return service.getVersion();
-  }
-
-  @Override
-  public String getName() {
-    return "Office365 OAuth2";
-  }
-}
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index 19eb426..78eb908 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -48,6 +48,19 @@
     client-id = "<client-id>"
     client-secret = "<client-secret>"
     root-url = "<phabricator url>"
+
+  # The office365 has been renamed to azure and is deprecated.
+  [plugin "@PLUGIN@-office365-oauth"]
+    client-id = "<client-id>"
+    client-secret = "<client-secret>"
+    tenant = "<tenant (optional defaults to organizations if not set)>"
+
+  [plugin "@PLUGIN@-azure-oauth"]
+    client-id = "<client-id>"
+    client-secret = "<client-secret>"
+    tenant = "<tenant (optional defaults to organizations if not set)>"
+    link-to-existing-office365-accounts = true #Optional, if set will try to link old account with the @PLUGIN@-office365-oauth naming
+
 ```
 
 When one from the sections above is omitted, OAuth SSO is used.
@@ -222,3 +235,42 @@
 The client-id and client-secret for Phabricator can be obtained by registering a
 Client application.
 See [Using the Phabricator OAuth Server](https://secure.phabricator.com/book/phabcontrib/article/using_oauthserver/).
+
+### Azure (previously named Office365)
+Were previously named Office365 but both `plugin.gerrit-oauth-provider-azure-oauth` and
+`plugin.gerrit-oauth-provider-office365-oauth` is supported by the Azure OAuth.
+When running *java gerrit.war init* it will check the existing config to see if it finds the old
+naming and use that during the init run, if it does not find the `office365-oauth` it will
+use the new `azure-oauth` naming.
+
+The client-id and client-secret for Azure can be obtained by registering a new application,
+see [OAuth 2.0 and OpenID Connect protocols on Microsoft identity platform](https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-v2-protocols).
+
+####Username
+By default, Azure OAuth will not set a username (used for ssh) and the user can choose one from the web ui
+can be used.
+```
+plugin.gerrit-oauth-provider-azure-oauth.use-email-as-username = true
+```
+
+####Tenant
+The Azure OAuth is default set to use the tenant `organizations` but a specific tenant can be used by
+the option `tenant`. If a tenant other than `common`, `organizations` or `consumers` is used then the tokens will be
+validated that they are originating from the same tenant that is configured in the Gerrit OAuth plugin.
+See [Microsoft identity platform and OpenID Connect protocol](https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-protocols-oidc#fetch-the-openid-connect-metadata-document)
+```
+plugin.gerrit-oauth-provider-azure-oauth.tenant = <tenant to use>
+```
+
+Regardless of tenant all tokens will be checked that they contain the client_id set
+in the Azure OAuth.
+
+####Migrating from Office365 naming
+If this where previously installed with the `office365-oauth` you can migrate to `azure-oauth` by setting the
+flag.
+```
+plugin.gerrit-oauth-provider-azure-oauth.link-to-existing-office365-accounts = true
+```
+This will try to link the old `office365-oauth` external id to the new `azure-oauth` external id automatically.
+Another option is to migrate these manually offline, see [External IDs](https://gerrit-review.googlesource.com/Documentation/config-accounts.html#external-ids)
+for more information.
diff --git a/src/test/java/com/googlesource/gerrit/plugins/oauth/Office365ApiTest.java b/src/test/java/com/googlesource/gerrit/plugins/oauth/MicrosoftAzureActiveDirectory20ApiTest.java
similarity index 81%
rename from src/test/java/com/googlesource/gerrit/plugins/oauth/Office365ApiTest.java
rename to src/test/java/com/googlesource/gerrit/plugins/oauth/MicrosoftAzureActiveDirectory20ApiTest.java
index 6bcf313..a98348e 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/oauth/Office365ApiTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/oauth/MicrosoftAzureActiveDirectory20ApiTest.java
@@ -16,16 +16,17 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import com.github.scribejava.apis.MicrosoftAzureActiveDirectory20Api;
 import com.github.scribejava.core.extractors.OAuth2AccessTokenJsonExtractor;
 import org.junit.Before;
 import org.junit.Test;
 
-public class Office365ApiTest {
-  private Office365Api api;
+public class MicrosoftAzureActiveDirectory20ApiTest {
+  private MicrosoftAzureActiveDirectory20Api api;
 
   @Before
   public void setUp() {
-    api = new Office365Api();
+    api = MicrosoftAzureActiveDirectory20Api.instance();
   }
 
   @Test