Merge branch 'stable-2.10'
* stable-2.10:
Bump plugin version to 0.4
Add support for hosted domain to Google OAuth provider
Conflicts:
VERSION
diff --git a/VERSION b/VERSION
index f5bb986..8026f1e 100644
--- a/VERSION
+++ b/VERSION
@@ -1,4 +1,4 @@
# Used by BUCK to include "Implementation-Version" in plugin Manifest.
# If this file doesn't exist the output of 'git describe' is used
# instead.
-PLUGIN_VERSION = '2.11'
+PLUGIN_VERSION = '2.11.1'
diff --git a/src/main/java/com/googlesource/gerrit/plugins/oauth/GoogleOAuthService.java b/src/main/java/com/googlesource/gerrit/plugins/oauth/GoogleOAuthService.java
index 81cd416..438455a 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/oauth/GoogleOAuthService.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/oauth/GoogleOAuthService.java
@@ -62,6 +62,7 @@
private final OAuthService service;
private final String canonicalWebUrl;
private final boolean linkToExistingOpenIDAccounts;
+ private final String domain;
@Inject
GoogleOAuthService(PluginConfigFactory cfgFactory,
@@ -73,6 +74,7 @@
urlProvider.get()) + "/";
this.linkToExistingOpenIDAccounts = cfg.getBoolean(
InitOAuth.LINK_TO_EXISTING_OPENID_ACCOUNT, false);
+ this.domain = cfg.getString(InitOAuth.DOMAIN);
String scope = linkToExistingOpenIDAccounts
? "openid " + SCOPE
: SCOPE;
@@ -88,6 +90,7 @@
log.debug("OAuth2: scope={}", scope);
log.debug("OAuth2: linkToExistingOpenIDAccounts={}",
linkToExistingOpenIDAccounts);
+ log.debug("OAuth2: domain={}", domain);
}
}
@@ -119,8 +122,21 @@
JsonElement name = jsonObject.get("name");
String claimedIdentifier = null;
- if (linkToExistingOpenIDAccounts) {
- claimedIdentifier = lookupClaimedIdentity(token);
+ if (linkToExistingOpenIDAccounts
+ || !Strings.isNullOrEmpty(domain)) {
+ JsonObject jwtToken = retrieveJWTToken(token);
+ if (linkToExistingOpenIDAccounts) {
+ claimedIdentifier = retrieveClaimedIdentity(jwtToken);
+ }
+ if (!Strings.isNullOrEmpty(domain)) {
+ String hdClaim = retrieveHostedDomain(jwtToken);
+ if (!domain.equalsIgnoreCase(hdClaim)) {
+ // TODO(davido): improve error reporting in OAuth extension point
+ log.error("Error: hosted domain validation failed: {}",
+ Strings.nullToEmpty(hdClaim));
+ return null;
+ }
+ }
}
return new OAuthUserInfo(id.getAsString() /*externalId*/,
null /*username*/,
@@ -133,30 +149,19 @@
}
}
- /**
- * @param token
- * @return OpenID id token, when contained in id_token, null otherwise
- */
- private static String lookupClaimedIdentity(OAuthToken token) {
+ private JsonObject retrieveJWTToken(OAuthToken token) {
JsonElement idToken =
- OutputFormat.JSON.newGson().fromJson(token.getRaw(), JsonElement.class);
- if (idToken.isJsonObject()) {
+ OutputFormat.JSON.newGson().fromJson(token.getRaw(), JsonElement.class);
+ if (idToken != null && idToken.isJsonObject()) {
JsonObject idTokenObj = idToken.getAsJsonObject();
JsonElement idTokenElement = idTokenObj.get("id_token");
- if (!idTokenElement.isJsonNull()) {
+ if (idTokenElement != null && !idTokenElement.isJsonNull()) {
String payload = decodePayload(idTokenElement.getAsString());
if (!Strings.isNullOrEmpty(payload)) {
- JsonElement openidIdToken =
+ JsonElement tokenJsonElement =
OutputFormat.JSON.newGson().fromJson(payload, JsonElement.class);
- if (openidIdToken.isJsonObject()) {
- JsonObject openidIdObj = openidIdToken.getAsJsonObject();
- JsonElement openidIdElement = openidIdObj.get("openid_id");
- if (!openidIdElement.isJsonNull()) {
- String openIdId = openidIdElement.getAsString();
- log.debug("OAuth2: openid_id={}", openIdId);
- return openIdId;
- }
- log.debug("OAuth2: JWT doesn't contain openid_id element");
+ if (tokenJsonElement.isJsonObject()) {
+ return tokenJsonElement.getAsJsonObject();
}
}
}
@@ -164,6 +169,28 @@
return null;
}
+ private static String retrieveClaimedIdentity(JsonObject jwtToken) {
+ JsonElement openidIdElement = jwtToken.get("openid_id");
+ if (openidIdElement != null && !openidIdElement.isJsonNull()) {
+ String openIdId = openidIdElement.getAsString();
+ log.debug("OAuth2: openid_id={}", openIdId);
+ return openIdId;
+ }
+ log.debug("OAuth2: JWT doesn't contain openid_id element");
+ return null;
+ }
+
+ private static String retrieveHostedDomain(JsonObject jwtToken) {
+ JsonElement hdClaim = jwtToken.get("hd");
+ if (hdClaim != null && !hdClaim.isJsonNull()) {
+ String hd = hdClaim.getAsString();
+ log.debug("OAuth2: hd={}", hd);
+ return hd;
+ }
+ log.debug("OAuth2: JWT doesn't contain hd element");
+ return null;
+ }
+
/**
* Decode payload from JWT according to spec:
* "header.payload.signature"
@@ -197,6 +224,10 @@
url += "&openid.realm=" + URLEncoder.encode(canonicalWebUrl,
StandardCharsets.UTF_8.name());
}
+ if (!Strings.isNullOrEmpty(domain)) {
+ url += "&hd=" + URLEncoder.encode(domain,
+ StandardCharsets.UTF_8.name());
+ }
} catch (UnsupportedEncodingException e) {
throw new IllegalArgumentException(e);
}
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 9dcf725..14c60a9 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/oauth/InitOAuth.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/oauth/InitOAuth.java
@@ -25,6 +25,7 @@
static final String CLIENT_SECRET = "client-secret";
static final String LINK_TO_EXISTING_OPENID_ACCOUNT =
"link-to-existing-openid-accounts";
+ static final String DOMAIN = "domain";
private final ConsoleUI ui;
private final Section googleOAuthProviderSection;
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index d80a584..03a45bf 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -37,6 +37,21 @@
to Google OAuth configuration section.
+It is possile to restrict sign-in to accounts of one (hosted) domain for
+Google OAuth. The `domain` option can be added:
+
+```
+plugin.gerrit-oauth-provider-google-oauth.domain = "mycollege.edu"
+```
+
+(See the spec)[https://developers.google.com/identity/protocols/OpenIDConnect#hd-param]
+for more information. To protect against client-side request modification, the returned
+ID token is checked to contain a matching hd claim (which is proof the account does belong
+to the hosted domain). If the hd claim wasn't included in ID token or didn't match the
+provided `domain` configuration option the authentication is rejected. Note: Because of
+current limitation of the OAuth extension point in gerrit (blame /me for that) the user
+would only see "Unauthorized" message.
+
## Obtaining provider authorizations
### Google