Added Bitbucket OAuth plugin.
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) {