Skip to content

Commit

Permalink
Merge "Add GitLab oauth provider"
Browse files Browse the repository at this point in the history
  • Loading branch information
David Ostrovsky authored and Gerrit Code Review committed Mar 2, 2017
2 parents a29e972 + 0ec9318 commit a41d6d6
Show file tree
Hide file tree
Showing 8 changed files with 334 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Supported OAuth providers:
* [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.
Expand Down
161 changes: 161 additions & 0 deletions src/main/java/com/googlesource/gerrit/plugins/oauth/GitLabApi.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -75,5 +75,13 @@ protected void configureServlets() {
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);
}
}
}
10 changes: 10 additions & 0 deletions src/main/java/com/googlesource/gerrit/plugins/oauth/InitOAuth.java
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ class InitOAuth implements InitStep {
private final Section bitbucketOAuthProviderSection;
private final Section casOAuthProviderSection;
private final Section facebookOAuthProviderSection;
private final Section gitlabOAuthProviderSection;

@Inject
InitOAuth(ConsoleUI ui,
Expand All @@ -56,6 +57,8 @@ class InitOAuth implements InitStep {
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
Expand Down Expand Up @@ -100,6 +103,13 @@ public void run() throws Exception {
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) {
Expand Down
24 changes: 24 additions & 0 deletions src/main/resources/Documentation/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ appended with provider suffix: e.g. `-google-oauth` or `-github-oauth`:
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.
Expand Down Expand Up @@ -147,3 +152,22 @@ service definition and need to be set manually.
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)
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit a41d6d6

Please sign in to comment.