Add GitLab oauth provider
Change-Id: Id62104ad953e955787387c74e2e6dd9eba6e9a10
diff --git a/README.md b/README.md
index 754a048..58f9ea4 100644
--- a/README.md
+++ b/README.md
@@ -11,6 +11,7 @@
* [CAS](https://www.apereo.org/projects/cas)
* [Facebook](https://developers.facebook.com/docs/facebook-login)
* [GitHub](https://developer.github.com/v3/oauth/)
+* [GitLab](https://about.gitlab.com/)
* [Google](https://developers.google.com/identity/protocols/OAuth2)
See the [Wiki](https://github.com/davido/gerrit-oauth-provider/wiki) what it can do for you.
diff --git a/src/main/java/com/googlesource/gerrit/plugins/oauth/GitLabApi.java b/src/main/java/com/googlesource/gerrit/plugins/oauth/GitLabApi.java
new file mode 100644
index 0000000..9cc7618
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/oauth/GitLabApi.java
@@ -0,0 +1,161 @@
+// Copyright (C) 2017 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 org.scribe.builder.api.DefaultApi20;
+import org.scribe.exceptions.OAuthException;
+import org.scribe.extractors.AccessTokenExtractor;
+import org.scribe.model.*;
+import org.scribe.oauth.OAuthService;
+
+import org.scribe.utils.Preconditions;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import static java.lang.String.format;
+
+public class GitLabApi extends DefaultApi20 {
+ private static final String AUTHORIZE_URL =
+ "%s/oauth/authorize?client_id=%s&response_type=code&redirect_uri=%s";
+
+ private final String rootUrl;
+
+ public GitLabApi(String rootUrl) {
+ this.rootUrl = rootUrl;
+ }
+
+ @Override
+ public String getAuthorizationUrl(OAuthConfig config) {
+ return String.format(AUTHORIZE_URL, rootUrl, config.getApiKey(),
+ config.getCallback());
+ }
+
+ @Override
+ public String getAccessTokenEndpoint() {
+ return String.format("%s/oauth/token", rootUrl);
+ }
+
+ @Override
+ public Verb getAccessTokenVerb() {
+ return Verb.POST;
+ }
+
+ @Override
+ public OAuthService createService(OAuthConfig config) {
+ return new GitLabOAuthService(this, config);
+ }
+
+ @Override
+ public AccessTokenExtractor getAccessTokenExtractor() {
+ return new GitLabJsonTokenExtractor();
+ }
+
+ private static final class GitLabOAuthService 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;
+
+ /**
+ * Default constructor
+ *
+ * @param api OAuth2.0 api information
+ * @param config OAuth 2.0 configuration param object
+ */
+ public GitLabOAuthService(DefaultApi20 api, OAuthConfig config) {
+ this.api = api;
+ this.config = config;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Token getAccessToken(Token requestToken, Verifier verifier) {
+ OAuthRequest request =
+ new OAuthRequest(api.getAccessTokenVerb(),
+ api.getAccessTokenEndpoint());
+ request.addBodyParameter(OAuthConstants.CLIENT_ID, config.getApiKey());
+ request.addBodyParameter(OAuthConstants.CLIENT_SECRET,
+ config.getApiSecret());
+ request.addBodyParameter(OAuthConstants.CODE, verifier.getValue());
+ request.addBodyParameter(OAuthConstants.REDIRECT_URI,
+ config.getCallback());
+ if (config.hasScope()) {
+ request.addBodyParameter(OAuthConstants.SCOPE, config.getScope());
+ }
+ request.addBodyParameter(GRANT_TYPE, GRANT_TYPE_VALUE);
+ Response response = request.send();
+ return api.getAccessTokenExtractor().extract(response.getBody());
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Token getRequestToken() {
+ throw new UnsupportedOperationException(
+ "Unsupported operation, please use 'getAuthorizationUrl' and redirect your users there");
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getVersion() {
+ return VERSION;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void signRequest(Token accessToken, OAuthRequest request) {
+ request.addQuerystringParameter(OAuthConstants.ACCESS_TOKEN,
+ accessToken.getToken());
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getAuthorizationUrl(Token requestToken) {
+ return api.getAuthorizationUrl(config);
+ }
+ }
+
+ private static final class GitLabJsonTokenExtractor implements
+ AccessTokenExtractor {
+ private Pattern accessTokenPattern = Pattern
+ .compile("\"access_token\"\\s*:\\s*\"(\\S*?)\"");
+
+ @Override
+ public Token extract(String response) {
+ Preconditions.checkEmptyString(response,
+ "Cannot extract a token from a null or empty String");
+ Matcher matcher = accessTokenPattern.matcher(response);
+ if (matcher.find()) {
+ return new Token(matcher.group(1), "", response);
+ } else {
+ throw new OAuthException(
+ "Cannot extract an acces token. Response was: " + response);
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/oauth/GitLabOAuthService.java b/src/main/java/com/googlesource/gerrit/plugins/oauth/GitLabOAuthService.java
new file mode 100644
index 0000000..46ca4fc
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/oauth/GitLabOAuthService.java
@@ -0,0 +1,130 @@
+// Copyright (C) 2017 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 GitLabOAuthService implements OAuthServiceProvider {
+ private static final Logger log = getLogger(GitLabOAuthService.class);
+ static final String CONFIG_SUFFIX = "-gitlab-oauth";
+ private static final String PROTECTED_RESOURCE_URL =
+ "%s/api/v3/user";
+ private static final String GITLAB_PROVIDER_PREFIX ="gitlab-oauth:";
+ private final OAuthService service;
+ private final String rootUrl;
+
+ @Inject
+ GitLabOAuthService(PluginConfigFactory cfgFactory,
+ @PluginName String pluginName,
+ @CanonicalWebUrl Provider<String> urlProvider) {
+ PluginConfig cfg = cfgFactory.getFromGerritConfig(
+ pluginName + CONFIG_SUFFIX);
+ String canonicalWebUrl = CharMatcher.is('/').trimTrailingFrom(
+ urlProvider.get()) + "/";
+ rootUrl = cfg.getString(InitOAuth.ROOT_URL);
+ service = new ServiceBuilder().provider(new GitLabApi(rootUrl))
+ .apiKey(cfg.getString(InitOAuth.CLIENT_ID))
+ .apiSecret(cfg.getString(InitOAuth.CLIENT_SECRET))
+ .callback(canonicalWebUrl + "oauth")
+ .build();
+ }
+
+ @Override
+ public OAuthUserInfo getUserInfo(OAuthToken token) throws IOException {
+ final String protectedResourceUrl =
+ String.format(PROTECTED_RESOURCE_URL, rootUrl);
+ OAuthRequest request = new OAuthRequest(Verb.GET, protectedResourceUrl);
+ 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());
+ }
+ JsonObject jsonObject = userJson.getAsJsonObject();
+ if (jsonObject == null || jsonObject.isJsonNull()) {
+ throw new IOException(
+ "Response doesn't contain 'user' field" + jsonObject);
+ }
+ JsonElement id = jsonObject.get("id");
+ JsonElement username = jsonObject.get("username");
+ JsonElement email = jsonObject.get("email");
+ JsonElement name = jsonObject.get("name");
+ return new OAuthUserInfo(GITLAB_PROVIDER_PREFIX + id.getAsString(),
+ username == null || username.isJsonNull()
+ ? null
+ : username.getAsString(),
+ email == null || email.isJsonNull() ? null : email.getAsString(),
+ name == null || name.isJsonNull() ? null : name.getAsString(),
+ null);
+ }
+
+ @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 "GitLab OAuth2";
+ }
+}
\ No newline at end of file
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 2b9b533..bfdcbc3 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/oauth/HttpModule.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/oauth/HttpModule.java
@@ -75,5 +75,13 @@
Exports.named(FacebookOAuthService.CONFIG_SUFFIX)).to(
FacebookOAuthService.class);
}
+
+ cfg = cfgFactory.getFromGerritConfig(
+ pluginName + GitLabOAuthService.CONFIG_SUFFIX);
+ if (cfg.getString(InitOAuth.CLIENT_ID) != null) {
+ bind(OAuthServiceProvider.class)
+ .annotatedWith(Exports.named(GitLabOAuthService.CONFIG_SUFFIX))
+ .to(GitLabOAuthService.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 608bfb6..f4b8963 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/oauth/InitOAuth.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/oauth/InitOAuth.java
@@ -40,6 +40,7 @@
private final Section bitbucketOAuthProviderSection;
private final Section casOAuthProviderSection;
private final Section facebookOAuthProviderSection;
+ private final Section gitlabOAuthProviderSection;
@Inject
InitOAuth(ConsoleUI ui,
@@ -56,6 +57,8 @@
PLUGIN_SECTION, pluginName + CasOAuthService.CONFIG_SUFFIX);
this.facebookOAuthProviderSection = sections.get(
PLUGIN_SECTION, pluginName + FacebookOAuthService.CONFIG_SUFFIX);
+ this.gitlabOAuthProviderSection = sections.get(
+ PLUGIN_SECTION, pluginName + GitLabOAuthService.CONFIG_SUFFIX);
}
@Override
@@ -100,6 +103,13 @@
if (configueFacebookOAuthProvider) {
configureOAuth(facebookOAuthProviderSection);
}
+
+ boolean configureGitLabOAuthProvider = ui.yesno(
+ true, "Use GitLab OAuth provider for Gerrit login ?");
+ if (configureGitLabOAuthProvider) {
+ gitlabOAuthProviderSection.string("GitLab Root URL", ROOT_URL, null);
+ configureOAuth(gitlabOAuthProviderSection);
+ }
}
private void configureOAuth(Section s) {
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index 8c1f9a2..20b1024 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -26,6 +26,11 @@
root-url = "<cas url>"
client-id = "<client-id>"
client-secret = "<client-secret>"
+
+ [plugin "@PLUGIN@-gitlab-oauth"]
+ root-url = "<gitlab url>"
+ client-id = "<client-id>"
+ client-secret = "<client-secret>"
```
When one from the sections above is omitted, OAuth SSO is used.
@@ -147,3 +152,22 @@
See
[the CAS documentation](https://apereo.github.io/cas/4.2.x/installation/OAuth-OpenId-Authentication.html#add-oauth-clients)
for an example.
+
+### GitLab
+
+To obtain client-id and client-secret for GitLab OAuth, go to
+Applications settings in your GitLab profile:
+
+- Select "Save application" and enter information about the
+ application.
+
+ Note that it is important that Redirect URI points to
+ `<canonical-web-uri-of-gerrit>/oauth`.
+
+ ![Save new application on GitLab](images/gitlab-1.png)
+
+
+After application is saved, the page will show generated client id and
+secret.
+
+![Generated client id and secret](images/gitlab-2.png)
diff --git a/src/main/resources/Documentation/images/gitlab-1.png b/src/main/resources/Documentation/images/gitlab-1.png
new file mode 100644
index 0000000..aff16ba
--- /dev/null
+++ b/src/main/resources/Documentation/images/gitlab-1.png
Binary files differ
diff --git a/src/main/resources/Documentation/images/gitlab-2.png b/src/main/resources/Documentation/images/gitlab-2.png
new file mode 100644
index 0000000..c575bf3
--- /dev/null
+++ b/src/main/resources/Documentation/images/gitlab-2.png
Binary files differ