Merge pull request #52 from Opalo/master

Added Bitbucket OAuth provider
diff --git a/README.md b/README.md
index 8ebb744..9a4dd26 100644
--- a/README.md
+++ b/README.md
@@ -7,6 +7,7 @@
 With this plugin Gerrit can use OAuth2 protocol for authentication. 
 Supported OAuth providers:
 
+* Bitbucket
 * GitHub
 * Google
 
@@ -39,6 +40,7 @@
   [...]
   *** OAuth Authentication Provider
   ***
+  Use Bitbucket OAuth provider for Gerrit login ? [Y/n]? n
   Use Google OAuth provider for Gerrit login ? [Y/n]?
   Application client id          : <client-id>
   Application client secret      : 
diff --git a/src/main/java/com/googlesource/gerrit/plugins/oauth/BitbucketApi.java b/src/main/java/com/googlesource/gerrit/plugins/oauth/BitbucketApi.java
new file mode 100644
index 0000000..52a9280
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/oauth/BitbucketApi.java
@@ -0,0 +1,153 @@
+// Copyright (C) 2015 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.google.common.io.BaseEncoding;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import org.scribe.builder.api.DefaultApi20;
+import org.scribe.exceptions.OAuthException;
+import org.scribe.extractors.AccessTokenExtractor;
+import org.scribe.model.OAuthConfig;
+import org.scribe.model.OAuthRequest;
+import org.scribe.model.Response;
+import org.scribe.model.Token;
+import org.scribe.model.Verb;
+import org.scribe.model.Verifier;
+import org.scribe.oauth.OAuthService;
+
+import static com.google.gerrit.server.OutputFormat.JSON;
+import static java.lang.String.format;
+import static javax.servlet.http.HttpServletResponse.SC_OK;
+import static org.scribe.model.OAuthConstants.ACCESS_TOKEN;
+import static org.scribe.model.OAuthConstants.CODE;
+
+public class BitbucketApi extends DefaultApi20 {
+
+  private static final String AUTHORIZE_URL =
+      "https://bitbucket.org/site/oauth2/authorize?client_id=%s&response_type=code";
+  private static final String ACCESS_TOKEN_ENDPOINT =
+      "https://bitbucket.org/site/oauth2/access_token";
+
+  public BitbucketApi() {
+  }
+
+  @Override
+  public String getAuthorizationUrl(OAuthConfig config) {
+    return format(AUTHORIZE_URL, config.getApiKey());
+  }
+
+  @Override
+  public String getAccessTokenEndpoint() {
+    return ACCESS_TOKEN_ENDPOINT;
+  }
+
+  @Override
+  public Verb getAccessTokenVerb() {
+    return Verb.POST;
+  }
+
+  @Override
+  public OAuthService createService(OAuthConfig config) {
+    return new BitbucketOAuthService(this, config);
+  }
+
+  @Override
+  public AccessTokenExtractor getAccessTokenExtractor() {
+    return new BitbucketTokenExtractor();
+  }
+
+  private static final class BitbucketOAuthService implements OAuthService {
+    private static final String VERSION = "2.0";
+
+    private static final String GRANT_TYPE = "grant_type";
+    private static final String GRANT_TYPE_VALUE = "authorization_code";
+
+    private final DefaultApi20 api;
+    private final OAuthConfig config;
+
+    private BitbucketOAuthService(DefaultApi20 api, OAuthConfig config) {
+      this.config = config;
+      this.api = api;
+    }
+
+    @Override
+    public Token getAccessToken(Token token, Verifier verifier) {
+      OAuthRequest request = new OAuthRequest(api.getAccessTokenVerb(),
+          api.getAccessTokenEndpoint());
+      request.addHeader("Authorization", prepareAuthorizationHeaderValue());
+      request.addBodyParameter(GRANT_TYPE, GRANT_TYPE_VALUE);
+      request.addBodyParameter(CODE, verifier.getValue());
+      Response response = request.send();
+      if (response.getCode() == SC_OK) {
+        Token t = api.getAccessTokenExtractor().extract(response.getBody());
+        return new Token(t.getToken(), config.getApiSecret());
+      } else {
+        throw new OAuthException(
+            String.format("Error response received: %s, HTTP status: %s",
+                response.getBody(), response.getCode()));
+      }
+    }
+
+    private String prepareAuthorizationHeaderValue() {
+      String value = String.format("%s:%s", config.getApiKey(), config.getApiSecret());
+      String valueBase64 = BaseEncoding.base64().encode(value.getBytes());
+      return String.format("Basic %s", valueBase64);
+    }
+
+    @Override
+    public Token getRequestToken() {
+      throw new UnsupportedOperationException(
+          "Unsupported operation, please use 'getAuthorizationUrl' and redirect your users there");
+    }
+
+    @Override
+    public String getVersion() {
+      return VERSION;
+    }
+
+    @Override
+    public void signRequest(Token token, OAuthRequest request) {
+      request.addQuerystringParameter(ACCESS_TOKEN, token.getToken());
+    }
+
+    @Override
+    public String getAuthorizationUrl(Token token) {
+      return api.getAuthorizationUrl(config);
+    }
+  }
+
+  private static final class BitbucketTokenExtractor
+      implements AccessTokenExtractor {
+
+    @Override
+    public Token extract(String response) {
+      JsonElement json = JSON.newGson().fromJson(response, JsonElement.class);
+      if (json.isJsonObject()) {
+        JsonObject jsonObject = json.getAsJsonObject();
+        JsonElement id = jsonObject.get(ACCESS_TOKEN);
+        if (id == null || id.isJsonNull()) {
+          throw new OAuthException(
+              "Response doesn't contain 'access_token' field");
+        }
+        JsonElement accessToken = jsonObject.get(ACCESS_TOKEN);
+        return new Token(accessToken.getAsString(), "");
+      } else {
+        throw new OAuthException(
+            String.format("Invalid JSON '%s': not a JSON Object", json));
+      }
+    }
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/oauth/BitbucketOAuthService.java b/src/main/java/com/googlesource/gerrit/plugins/oauth/BitbucketOAuthService.java
new file mode 100644
index 0000000..80bbda4
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/oauth/BitbucketOAuthService.java
@@ -0,0 +1,127 @@
+// Copyright (C) 2015 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.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 org.scribe.builder.ServiceBuilder;
+import org.scribe.model.OAuthRequest;
+import org.scribe.model.Response;
+import org.scribe.model.Token;
+import org.scribe.model.Verb;
+import org.scribe.model.Verifier;
+import org.scribe.oauth.OAuthService;
+import org.slf4j.Logger;
+
+import java.io.IOException;
+
+import static com.google.gerrit.server.OutputFormat.JSON;
+import static javax.servlet.http.HttpServletResponse.SC_OK;
+import static org.slf4j.LoggerFactory.getLogger;
+
+@Singleton
+public class BitbucketOAuthService implements OAuthServiceProvider {
+  private static final Logger log = getLogger(BitbucketOAuthService.class);
+  static final String CONFIG_SUFFIX = "-bitbucket-oauth";
+  private static final String PROTECTED_RESOURCE_URL =
+      "https://bitbucket.org/api/1.0/user/";
+  private final OAuthService service;
+
+  @Inject
+  BitbucketOAuthService(PluginConfigFactory cfgFactory,
+      @PluginName String pluginName,
+      @CanonicalWebUrl Provider<String> urlProvider) {
+    PluginConfig cfg =
+        cfgFactory.getFromGerritConfig(pluginName + CONFIG_SUFFIX);
+
+    String canonicalWebUrl =
+        CharMatcher.is('/').trimTrailingFrom(urlProvider.get()) + "/";
+
+    service = new ServiceBuilder().provider(BitbucketApi.class)
+        .apiKey(cfg.getString(InitOAuth.CLIENT_ID))
+        .apiSecret(cfg.getString(InitOAuth.CLIENT_SECRET))
+        .callback(canonicalWebUrl + "oauth")
+        .build();
+  }
+
+  @Override
+  public OAuthUserInfo getUserInfo(OAuthToken token) throws IOException {
+    OAuthRequest request = new OAuthRequest(Verb.GET, PROTECTED_RESOURCE_URL);
+    Token t = new Token(token.getToken(), token.getSecret(), token.getRaw());
+    service.signRequest(t, request);
+    Response response = request.send();
+    if (response.getCode() != SC_OK) {
+      throw new IOException(String.format("Status %s (%s) for request %s",
+          response.getCode(), response.getBody(), request.getUrl()));
+    }
+    JsonElement 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();
+      JsonObject userObject = jsonObject.getAsJsonObject("user");
+      if (userObject == null || userObject.isJsonNull()) {
+        throw new IOException("Response doesn't contain 'user' field");
+      }
+      JsonElement usernameElement = userObject.get("username");
+      String username = usernameElement.getAsString();
+
+      JsonElement displayName = jsonObject.get("display_name");
+      return new OAuthUserInfo(username, username, null,
+          displayName == null || displayName.isJsonNull() ? null
+              : displayName.getAsString(),
+          null);
+    } else {
+      throw new IOException(
+          String.format("Invalid JSON '%s': not a JSON Object", userJson));
+    }
+  }
+
+  @Override
+  public OAuthToken getAccessToken(OAuthVerifier rv) {
+    Verifier vi = new Verifier(rv.getValue());
+    Token to = service.getAccessToken(null, vi);
+    return new OAuthToken(to.getToken(), to.getSecret(), null);
+  }
+
+  @Override
+  public String getAuthorizationUrl() {
+    return service.getAuthorizationUrl(null);
+  }
+
+  @Override
+  public String getVersion() {
+    return service.getVersion();
+  }
+
+  @Override
+  public String getName() {
+    return "Bitbucket OAuth2";
+  }
+}
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 bb99423..8fcef0d 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/oauth/HttpModule.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/oauth/HttpModule.java
@@ -51,5 +51,13 @@
           .annotatedWith(Exports.named(GitHubOAuthService.CONFIG_SUFFIX))
           .to(GitHubOAuthService.class);
     }
+
+    cfg = cfgFactory.getFromGerritConfig(
+        pluginName + BitbucketOAuthService.CONFIG_SUFFIX);
+    if (cfg.getString(InitOAuth.CLIENT_ID) != null) {
+      bind(OAuthServiceProvider.class)
+          .annotatedWith(Exports.named(BitbucketOAuthService.CONFIG_SUFFIX))
+          .to(BitbucketOAuthService.class);
+    }
   }
 }
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 9b3a1ec..a202555 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/oauth/InitOAuth.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/oauth/InitOAuth.java
@@ -32,6 +32,7 @@
   private final ConsoleUI ui;
   private final Section googleOAuthProviderSection;
   private final Section githubOAuthProviderSection;
+  private final Section bitbucketOAuthProviderSection;
 
   @Inject
   InitOAuth(ConsoleUI ui,
@@ -42,6 +43,8 @@
         PLUGIN_SECTION, pluginName + GoogleOAuthService.CONFIG_SUFFIX);
     this.githubOAuthProviderSection = sections.get(
         PLUGIN_SECTION, pluginName + GitHubOAuthService.CONFIG_SUFFIX);
+    this.bitbucketOAuthProviderSection = sections.get(
+        PLUGIN_SECTION, pluginName + BitbucketOAuthService.CONFIG_SUFFIX);
   }
 
   @Override
@@ -62,6 +65,12 @@
     if (configueGitHubOAuthProvider) {
       configureOAuth(githubOAuthProviderSection);
     }
+
+    boolean configureBitbucketOAuthProvider = ui.yesno(
+        true, "Use Bitbucket OAuth provider for Gerrit login ?");
+    if (configureBitbucketOAuthProvider) {
+      configureOAuth(bitbucketOAuthProviderSection);
+    }
   }
 
   private void configureOAuth(Section s) {