Merge "GoogleOAuthService: Decode 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