-
Notifications
You must be signed in to change notification settings - Fork 84
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge remote-tracking branch 'refs/remotes/origin/master'
* refs/remotes/origin/master: GoogleOAuthService: Use StringBuilder CasOAuthService: Format using google-java-format Make preferred_username optional for Keycloak Upgrade bazlets to latest version to build with 3.5.0.1 API Bump Bazel version to 4.2.0 AzureActiveDirectoryService: Use UrlDecoder for base64 decoding Include jackson-core explicitly Rename Office365AuthService to AzureActiveDirectoryService Using MicrosoftAzureActiveDirectory20Api from scribejava-apis Validate the tokens issued from Azure Office365 OAuth: Add support for specifying a tenant GoogleOAuthService: Decode JWTs as UTF-8 DexOAuthService: Decode JWTs as UTF-8 Decode Keycloak JWTs as UTF-8 Upgrade bazlets to latest stable-3.1 to build with 3.1.10 API Bump Bazel version to 3.7.0 Upgrade bazlets to latest stable-3.0 to build with 3.0.13 API GithubApiUrlTest: Adapt to PluginConfig update change in master Bump Bazel version to 3.5.0 Upgrade bazlets to latest master to build with 3.2.3 API Upgrade bazlets to latest stable-3.1 to build with 3.1.8 API Upgrade bazlets to latest stable-3.0 to build with 3.0.12 API Upgrade bazlets to latest stable-2.16 to build with 2.16.22 API Add support for Phabricator OAuth provider Bump Bazel version to 3.4.1 LemonLDAP::NG: Remove getBearerSignature() override LemonLDAP::NG: Fix default scope name LemonLDAP::NG: Set username claim name in accordance with specs Office365OAuthService: Restore Accept header in user info request Upgrade bazlets to latest master to build with 3.2.2 API Upgrade bazlets to latest stable-3.1 to build with 3.1.7 API Upgrade bazlets to latest stable-3.0 to build with 3.0.11 API Remove the commented-out snapshot plugin api lines Adapt SNAPSHOT plugin api example to the 3.1 version Upgrade bazlets to latest stable-3.1 Prepare for new gerrit_api snapshot version usage Upgrade bazlets to latest stable-3.0 Upgrade bazlets to latest stable-3.0 Upgrade bazlets to latest stable-2.16 Upgrade bazlets to latest master to build with 3.2.1 API Upgrade bazlets to latest stable-3.1 to build with 3.1.6 API Upgrade bazlets to latest stable-3.0 to build with 3.0.10 API Upgrade bazlets to latest stable-2.16 to build with 2.16.21 API Upgrade bazlets to latest stable-2.15 to build with 2.15.19 API Upgrade bazlets to latest stable-2.14 to build with 2.14.21 API Prevent NPE in Cas service Upgrade bazlets to latest stable-2.16 to build with 2.16.20 API Fix bazlets using latest stable-3.1 to build with 3.1.5 API Upgrade bazlets to latest stable-3.1 to build with 3.1.5 API Upgrade bazlets to latest stable-3.0 to build with 3.0.9 API Upgrade bazlets to latest stable-2.16 to build with 2.16.19 API Upgrade bazlets to latest stable-2.16 to build with 2.16.18 API Bump Bazel version to 3.1.0 Upgrade bazlets to latest master to build with 3.2.0-rc0 API Bump Bazel version to 3.0.0 Ensure that LemonLDAP is configured on init Upgrade bazlets to latest master to build with 3.1.4 API Upgrade bazlets to latest stable-2.16 to build with 2.16.17 API Upgrade bazlets to latest stable-3.0 to build with 3.0.8 API Change-Id: I285feedaa4d741d7c567d10376ea934456260c94
- Loading branch information
Showing
20 changed files
with
579 additions
and
103 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
2.2.0 | ||
4.2.0 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
242 changes: 242 additions & 0 deletions
242
src/main/java/com/googlesource/gerrit/plugins/oauth/AzureActiveDirectoryService.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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.getUrlDecoder().decode(tokenParts[1]), StandardCharsets.UTF_8), | ||
JsonObject.class); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.