Skip to content

Commit d21d172

Browse files
committed
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
2 parents 296a005 + f9bef74 commit d21d172

20 files changed

+579
-103
lines changed

.bazelversion

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
2.2.0
1+
4.2.0

BUILD

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ gerrit_plugin(
2020
resources = glob(["src/main/resources/**/*"]),
2121
deps = [
2222
"@commons-codec//jar:neverlink",
23+
"@jackson-core//jar",
2324
"@jackson-databind//jar",
25+
"@scribejava-apis//jar",
2426
"@scribejava-core//jar",
2527
],
2628
)
@@ -31,6 +33,7 @@ junit_tests(
3133
tags = ["oauth"],
3234
deps = [
3335
":gerrit-oauth-provider__plugin_test_deps",
36+
"@scribejava-apis//jar",
3437
"@scribejava-core//jar",
3538
],
3639
)

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ Supported OAuth providers:
1717
* [Google](https://developers.google.com/identity/protocols/OAuth2)
1818
* [Keycloak](http://www.keycloak.org/)
1919
* [LemonLDAP::NG](https://lemonldap-ng.org)
20-
* [Office365](https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-v2-protocols)
20+
* [Azure (previously named Office365)](https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-v2-protocols)
21+
* [Phabricator](https://secure.phabricator.com/book/phabcontrib/article/using_oauthserver/)
2122

2223
See the [Wiki](https://github.com/davido/gerrit-oauth-provider/wiki) what it can do for you.
2324

WORKSPACE

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,10 @@ workspace(name = "com_github_davido_gerrit_oauth_provider")
33
load("//:bazlets.bzl", "load_bazlets")
44

55
load_bazlets(
6-
commit = "2add9fecf8bec8634bddf354815bd0fa93f58dd5",
6+
commit = "8fa44957c3b3b89ce1d96eba67441882c54503fc",
77
#local_path = "/home/<user>/projects/bazlets",
88
)
99

10-
# Snapshot Plugin API
11-
#load(
12-
# "@com_googlesource_gerrit_bazlets//:gerrit_api_maven_local.bzl",
13-
# "gerrit_api_maven_local",
14-
#)
15-
16-
# Load snapshot Plugin API
17-
#gerrit_api_maven_local()
18-
19-
# Release Plugin API
2010
load(
2111
"@com_googlesource_gerrit_bazlets//:gerrit_api.bzl",
2212
"gerrit_api",

external_plugin_deps.bzl

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,17 @@ load("//tools/bzl:maven_jar.bzl", "maven_jar")
22

33
def external_plugin_deps(omit_commons_codec = True):
44
JACKSON_VERS = "2.10.2"
5+
SCRIBEJAVA_VERS = "6.9.0"
56
maven_jar(
67
name = "scribejava-core",
7-
artifact = "com.github.scribejava:scribejava-core:6.9.0",
8+
artifact = "com.github.scribejava:scribejava-core:" + SCRIBEJAVA_VERS,
89
sha1 = "ed761f450d8382f75787e8fee9ae52e7ec768747",
910
)
11+
maven_jar(
12+
name = "scribejava-apis",
13+
artifact = "com.github.scribejava:scribejava-apis:" + SCRIBEJAVA_VERS,
14+
sha1 = "a374c7a36533e58e53b42b584a8b3751ab1e13c4",
15+
)
1016
maven_jar(
1117
name = "jackson-annotations",
1218
artifact = "com.fasterxml.jackson.core:jackson-annotations:" + JACKSON_VERS,
@@ -20,6 +26,11 @@ def external_plugin_deps(omit_commons_codec = True):
2026
"@jackson-annotations//jar",
2127
],
2228
)
29+
maven_jar(
30+
name = "jackson-core",
31+
artifact = "com.fasterxml.jackson.core:jackson-core:" + JACKSON_VERS,
32+
sha1 = "73d4322a6bda684f676a2b5fe918361c4e5c7cca",
33+
)
2334
if not omit_commons_codec:
2435
maven_jar(
2536
name = "commons-codec",
Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
// Copyright (C) 2018 The Android Open Source Project
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package com.googlesource.gerrit.plugins.oauth;
16+
17+
import static com.google.gerrit.json.OutputFormat.JSON;
18+
19+
import com.github.scribejava.apis.MicrosoftAzureActiveDirectory20Api;
20+
import com.github.scribejava.core.builder.ServiceBuilder;
21+
import com.github.scribejava.core.exceptions.OAuthException;
22+
import com.github.scribejava.core.model.OAuth2AccessToken;
23+
import com.github.scribejava.core.model.OAuthRequest;
24+
import com.github.scribejava.core.model.Response;
25+
import com.github.scribejava.core.model.Verb;
26+
import com.github.scribejava.core.oauth.OAuth20Service;
27+
import com.google.common.base.CharMatcher;
28+
import com.google.common.collect.ImmutableSet;
29+
import com.google.gerrit.extensions.annotations.PluginName;
30+
import com.google.gerrit.extensions.auth.oauth.OAuthServiceProvider;
31+
import com.google.gerrit.extensions.auth.oauth.OAuthToken;
32+
import com.google.gerrit.extensions.auth.oauth.OAuthUserInfo;
33+
import com.google.gerrit.extensions.auth.oauth.OAuthVerifier;
34+
import com.google.gerrit.server.config.CanonicalWebUrl;
35+
import com.google.gerrit.server.config.PluginConfig;
36+
import com.google.gerrit.server.config.PluginConfigFactory;
37+
import com.google.gson.Gson;
38+
import com.google.gson.JsonElement;
39+
import com.google.gson.JsonObject;
40+
import com.google.inject.Inject;
41+
import com.google.inject.Provider;
42+
import com.google.inject.Singleton;
43+
import java.io.IOException;
44+
import java.nio.charset.StandardCharsets;
45+
import java.util.Base64;
46+
import java.util.concurrent.ExecutionException;
47+
import javax.servlet.http.HttpServletResponse;
48+
import org.slf4j.Logger;
49+
import org.slf4j.LoggerFactory;
50+
51+
@Singleton
52+
class AzureActiveDirectoryService implements OAuthServiceProvider {
53+
private static final Logger log = LoggerFactory.getLogger(AzureActiveDirectoryService.class);
54+
static final String CONFIG_SUFFIX_LEGACY = "-office365-oauth";
55+
static final String CONFIG_SUFFIX = "-azure-oauth";
56+
private static final String AZURE_PROVIDER_PREFIX = "azure-oauth:";
57+
private static final String OFFICE365_PROVIDER_PREFIX = "office365-oauth:";
58+
private static final String PROTECTED_RESOURCE_URL = "https://graph.microsoft.com/v1.0/me";
59+
private static final String SCOPE =
60+
"openid offline_access https://graph.microsoft.com/user.readbasic.all";
61+
public static final String DEFAULT_TENANT = "organizations";
62+
private static final ImmutableSet<String> TENANTS_WITHOUT_VALIDATION =
63+
ImmutableSet.<String>builder().add(DEFAULT_TENANT).add("common").add("consumers").build();
64+
private final OAuth20Service service;
65+
private final Gson gson;
66+
private final String canonicalWebUrl;
67+
private final boolean useEmailAsUsername;
68+
private final String tenant;
69+
private final String clientId;
70+
private String providerPrefix;
71+
private final boolean linkOffice365Id;
72+
73+
@Inject
74+
AzureActiveDirectoryService(
75+
PluginConfigFactory cfgFactory,
76+
@PluginName String pluginName,
77+
@CanonicalWebUrl Provider<String> urlProvider) {
78+
PluginConfig cfg = cfgFactory.getFromGerritConfig(pluginName + CONFIG_SUFFIX);
79+
providerPrefix = AZURE_PROVIDER_PREFIX;
80+
81+
// ?: Did we find the client_id with the CONFIG_SUFFIX
82+
if (cfg.getString(InitOAuth.CLIENT_ID) == null) {
83+
// -> No, we did not find the client_id in the azure config so we should try the old legacy
84+
// office365 section
85+
cfg = cfgFactory.getFromGerritConfig(pluginName + CONFIG_SUFFIX_LEGACY);
86+
// We must also use the new provider prefix
87+
providerPrefix = OFFICE365_PROVIDER_PREFIX;
88+
}
89+
this.linkOffice365Id = cfg.getBoolean(InitOAuth.LINK_TO_EXISTING_OFFICE365_ACCOUNT, false);
90+
this.canonicalWebUrl = CharMatcher.is('/').trimTrailingFrom(urlProvider.get()) + "/";
91+
this.useEmailAsUsername = cfg.getBoolean(InitOAuth.USE_EMAIL_AS_USERNAME, false);
92+
this.tenant = cfg.getString(InitOAuth.TENANT, DEFAULT_TENANT);
93+
this.clientId = cfg.getString(InitOAuth.CLIENT_ID);
94+
this.service =
95+
new ServiceBuilder(cfg.getString(InitOAuth.CLIENT_ID))
96+
.apiSecret(cfg.getString(InitOAuth.CLIENT_SECRET))
97+
.callback(canonicalWebUrl + "oauth")
98+
.defaultScope(SCOPE)
99+
.build(MicrosoftAzureActiveDirectory20Api.custom(tenant));
100+
this.gson = JSON.newGson();
101+
if (log.isDebugEnabled()) {
102+
log.debug("OAuth2: canonicalWebUrl={}", canonicalWebUrl);
103+
log.debug("OAuth2: scope={}", SCOPE);
104+
log.debug("OAuth2: useEmailAsUsername={}", useEmailAsUsername);
105+
}
106+
}
107+
108+
@Override
109+
public OAuthUserInfo getUserInfo(OAuthToken token) throws IOException {
110+
// ?: Have we set a custom tenant and is this a tenant other than the one set in
111+
// TENANTS_WITHOUT_VALIDATION
112+
if (!TENANTS_WITHOUT_VALIDATION.contains(tenant)) {
113+
// -> Yes, we are using a tenant that should be validated, so verify that is issued for the
114+
// same one that we
115+
// have set.
116+
String tid = getTokenJson(token.getToken()).get("tid").getAsString();
117+
118+
// ?: Verify that this token has the same tenant as we are currently using
119+
if (!tenant.equals(tid)) {
120+
// -> No, this tenant does not equals the one in the token. So we should stop processing
121+
log.warn(
122+
String.format(
123+
"The token was issued by the tenant [%s] while we are set to use [%s]",
124+
tid, tenant));
125+
// Return null so the user will be shown Unauthorized.
126+
return null;
127+
}
128+
}
129+
130+
// Due to scribejava does not expose the id_token we need to do this a bit convoluted way to
131+
// extract this our self
132+
// see <a href="https://github.com/scribejava/scribejava/issues/968">Obtaining id_token from
133+
// access_token</a> for
134+
// the scribejava issue on this.
135+
String rawToken = token.getRaw();
136+
JsonObject jwtJson = gson.fromJson(rawToken, JsonObject.class);
137+
String idTokenBase64 = jwtJson.get("id_token").getAsString();
138+
String aud = getTokenJson(idTokenBase64).get("aud").getAsString();
139+
140+
// ?: Does this token have the same clientId set in the 'aud' part of the id_token as we are
141+
// using.
142+
// If not we should reject it
143+
// see <a href="https://docs.microsoft.com/en-us/azure/active-directory/develop/id-tokens">id
144+
// tokens Payload claims></a>
145+
// for information on the aud claim.
146+
if (!clientId.equals(aud)) {
147+
log.warn(
148+
String.format(
149+
"The id_token had aud [%s] while we expected it to be equal to the clientId [%s]",
150+
aud, clientId));
151+
// Return null so the user will be shown Unauthorized.
152+
return null;
153+
}
154+
155+
OAuthRequest request = new OAuthRequest(Verb.GET, PROTECTED_RESOURCE_URL);
156+
OAuth2AccessToken t = new OAuth2AccessToken(token.getToken(), token.getRaw());
157+
service.signRequest(t, request);
158+
request.addHeader("Accept", "*/*");
159+
160+
JsonElement userJson = null;
161+
try (Response response = service.execute(request)) {
162+
if (response.getCode() != HttpServletResponse.SC_OK) {
163+
throw new IOException(
164+
String.format(
165+
"Status %s (%s) for request %s",
166+
response.getCode(), response.getBody(), request.getUrl()));
167+
}
168+
userJson = JSON.newGson().fromJson(response.getBody(), JsonElement.class);
169+
if (log.isDebugEnabled()) {
170+
log.debug("User info response: {}", response.getBody());
171+
}
172+
if (userJson.isJsonObject()) {
173+
JsonObject jsonObject = userJson.getAsJsonObject();
174+
JsonElement id = jsonObject.get("id");
175+
if (id == null || id.isJsonNull()) {
176+
throw new IOException("Response doesn't contain id field");
177+
}
178+
JsonElement email = jsonObject.get("mail");
179+
JsonElement name = jsonObject.get("displayName");
180+
String login = null;
181+
182+
if (useEmailAsUsername && !email.isJsonNull()) {
183+
login = email.getAsString().split("@")[0];
184+
}
185+
186+
return new OAuthUserInfo(
187+
providerPrefix + id.getAsString() /*externalId*/,
188+
login /*username*/,
189+
email == null || email.isJsonNull() ? null : email.getAsString() /*email*/,
190+
name == null || name.isJsonNull() ? null : name.getAsString() /*displayName*/,
191+
linkOffice365Id ? OFFICE365_PROVIDER_PREFIX + id.getAsString() : null);
192+
}
193+
} catch (ExecutionException | InterruptedException e) {
194+
throw new RuntimeException("Cannot retrieve user info resource", e);
195+
}
196+
197+
throw new IOException(String.format("Invalid JSON '%s': not a JSON Object", userJson));
198+
}
199+
200+
@Override
201+
public OAuthToken getAccessToken(OAuthVerifier rv) {
202+
try {
203+
OAuth2AccessToken accessToken = service.getAccessToken(rv.getValue());
204+
return new OAuthToken(
205+
accessToken.getAccessToken(), accessToken.getTokenType(), accessToken.getRawResponse());
206+
} catch (InterruptedException | ExecutionException | IOException e) {
207+
String msg = "Cannot retrieve access token";
208+
log.error(msg, e);
209+
throw new RuntimeException(msg, e);
210+
}
211+
}
212+
213+
@Override
214+
public String getAuthorizationUrl() {
215+
String url = service.getAuthorizationUrl();
216+
return url;
217+
}
218+
219+
@Override
220+
public String getVersion() {
221+
return service.getVersion();
222+
}
223+
224+
@Override
225+
public String getName() {
226+
return "Office365 OAuth2";
227+
}
228+
229+
/** Get the {@link JsonObject} of a given token. */
230+
private JsonObject getTokenJson(String tokenBase64) {
231+
String[] tokenParts = tokenBase64.split("\\.");
232+
if (tokenParts.length != 3) {
233+
throw new OAuthException("Token does not contain expected number of parts");
234+
}
235+
236+
// Extract the payload part from the JWT token (header.payload.signature) by retrieving
237+
// tokenParts[1].
238+
return gson.fromJson(
239+
new String(Base64.getUrlDecoder().decode(tokenParts[1]), StandardCharsets.UTF_8),
240+
JsonObject.class);
241+
}
242+
}

src/main/java/com/googlesource/gerrit/plugins/oauth/CasOAuthService.java

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
package com.googlesource.gerrit.plugins.oauth;
1616

17+
import static com.google.common.base.Strings.nullToEmpty;
1718
import static com.google.gerrit.json.OutputFormat.JSON;
1819

1920
import com.github.scribejava.core.builder.ServiceBuilder;
@@ -128,11 +129,17 @@ public OAuthUserInfo getUserInfo(OAuthToken token) throws IOException {
128129
JsonObject obj = elem.getAsJsonObject();
129130

130131
String property = getStringElement(obj, "email");
131-
if (property != null) email = property;
132+
if (property != null) {
133+
email = property;
134+
}
132135
property = getStringElement(obj, "name");
133-
if (property != null) name = property;
136+
if (property != null) {
137+
name = property;
138+
}
134139
property = getStringElement(obj, "login");
135-
if (property != null) login = property;
140+
if (property != null) {
141+
login = property;
142+
}
136143
}
137144
}
138145

@@ -149,7 +156,9 @@ public OAuthUserInfo getUserInfo(OAuthToken token) throws IOException {
149156

150157
private String getStringElement(JsonObject o, String name) {
151158
JsonElement elem = o.get(name);
152-
if (elem == null || elem.isJsonNull()) return null;
159+
if (elem == null || elem.isJsonNull()) {
160+
return null;
161+
}
153162

154163
return elem.getAsString();
155164
}
@@ -159,7 +168,9 @@ public OAuthToken getAccessToken(OAuthVerifier rv) {
159168
try {
160169
OAuth2AccessToken accessToken = service.getAccessToken(rv.getValue());
161170
return new OAuthToken(
162-
accessToken.getAccessToken(), accessToken.getTokenType(), accessToken.getRawResponse());
171+
accessToken.getAccessToken(),
172+
nullToEmpty(accessToken.getTokenType()),
173+
accessToken.getRawResponse());
163174
} catch (InterruptedException | ExecutionException | IOException e) {
164175
String msg = "Cannot retrieve access token";
165176
log.error(msg, e);

0 commit comments

Comments
 (0)