Validate the tokens issued from Azure
* If a non-default tenant is used office 365 will now verify the token
is issued by the tenant we are set to use.
* All incoming tokens will be validated that they have set the same
client_id that gerrit is set to use.
Change-Id: I81e242c50ffd62e739e75a8c3a93d331049934a2
diff --git a/src/main/java/com/googlesource/gerrit/plugins/oauth/Office365OAuthService.java b/src/main/java/com/googlesource/gerrit/plugins/oauth/Office365OAuthService.java
index 1edec0e..3df8937 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/oauth/Office365OAuthService.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/oauth/Office365OAuthService.java
@@ -17,6 +17,7 @@
import static com.google.gerrit.json.OutputFormat.JSON;
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;
@@ -31,12 +32,15 @@
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;
@@ -51,9 +55,11 @@
private static final String SCOPE =
"openid offline_access https://graph.microsoft.com/user.readbasic.all";
private final OAuth20Service service;
+ private final Gson gson;
private final String canonicalWebUrl;
private final boolean useEmailAsUsername;
private final String tenant;
+ private final String clientId;
@Inject
Office365OAuthService(
@@ -64,12 +70,14 @@
this.canonicalWebUrl = CharMatcher.is('/').trimTrailingFrom(urlProvider.get()) + "/";
this.useEmailAsUsername = cfg.getBoolean(InitOAuth.USE_EMAIL_AS_USERNAME, false);
this.tenant = cfg.getString(InitOAuth.TENANT, Office365Api.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(new Office365Api(tenant));
+ this.gson = JSON.newGson();
if (log.isDebugEnabled()) {
log.debug("OAuth2: canonicalWebUrl={}", canonicalWebUrl);
log.debug("OAuth2: scope={}", SCOPE);
@@ -79,6 +87,52 @@
@Override
public OAuthUserInfo getUserInfo(OAuthToken token) throws IOException {
+ // ?: Have we set a custom tenant, if so we should validate that the token is issued by the same
+ // tenant as
+ // we have set.
+ if (!tenant.equals(Office365Api.DEFAULT_TENANT)) {
+ // -> Yes, we are using a non-default tenant so we should validate that is delegated from 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);
@@ -151,4 +205,18 @@
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/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index 24addfd..98b0b56 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -246,3 +246,9 @@
```
plugin.gerrit-oauth-provider-office365-oauth.tenant = <tenant to use>
```
+
+If a specific tenant is set Gerrit OAuth plugin will inspect the token and validate that this is
+originating from the Azure AD with the tenant specified in the option.
+
+By default, all tokens will be checked that they contain the client_id set
+in the Gerrit OAuth plugin.