Skip to content

Commit a41d6d6

Browse files
David OstrovskyGerrit Code Review
David Ostrovsky
authored and
Gerrit Code Review
committed
Merge "Add GitLab oauth provider"
2 parents a29e972 + 0ec9318 commit a41d6d6

File tree

8 files changed

+334
-0
lines changed

8 files changed

+334
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Supported OAuth providers:
1111
* [CAS](https://www.apereo.org/projects/cas)
1212
* [Facebook](https://developers.facebook.com/docs/facebook-login)
1313
* [GitHub](https://developer.github.com/v3/oauth/)
14+
* [GitLab](https://about.gitlab.com/)
1415
* [Google](https://developers.google.com/identity/protocols/OAuth2)
1516

1617
See the [Wiki](https://github.com/davido/gerrit-oauth-provider/wiki) what it can do for you.
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
// Copyright (C) 2017 The Android Open Source Project
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package com.googlesource.gerrit.plugins.oauth;
16+
17+
import org.scribe.builder.api.DefaultApi20;
18+
import org.scribe.exceptions.OAuthException;
19+
import org.scribe.extractors.AccessTokenExtractor;
20+
import org.scribe.model.*;
21+
import org.scribe.oauth.OAuthService;
22+
23+
import org.scribe.utils.Preconditions;
24+
25+
import java.util.regex.Matcher;
26+
import java.util.regex.Pattern;
27+
28+
import static java.lang.String.format;
29+
30+
public class GitLabApi extends DefaultApi20 {
31+
private static final String AUTHORIZE_URL =
32+
"%s/oauth/authorize?client_id=%s&response_type=code&redirect_uri=%s";
33+
34+
private final String rootUrl;
35+
36+
public GitLabApi(String rootUrl) {
37+
this.rootUrl = rootUrl;
38+
}
39+
40+
@Override
41+
public String getAuthorizationUrl(OAuthConfig config) {
42+
return String.format(AUTHORIZE_URL, rootUrl, config.getApiKey(),
43+
config.getCallback());
44+
}
45+
46+
@Override
47+
public String getAccessTokenEndpoint() {
48+
return String.format("%s/oauth/token", rootUrl);
49+
}
50+
51+
@Override
52+
public Verb getAccessTokenVerb() {
53+
return Verb.POST;
54+
}
55+
56+
@Override
57+
public OAuthService createService(OAuthConfig config) {
58+
return new GitLabOAuthService(this, config);
59+
}
60+
61+
@Override
62+
public AccessTokenExtractor getAccessTokenExtractor() {
63+
return new GitLabJsonTokenExtractor();
64+
}
65+
66+
private static final class GitLabOAuthService implements OAuthService {
67+
private static final String VERSION = "2.0";
68+
69+
private static final String GRANT_TYPE = "grant_type";
70+
private static final String GRANT_TYPE_VALUE = "authorization_code";
71+
72+
private final DefaultApi20 api;
73+
private final OAuthConfig config;
74+
75+
/**
76+
* Default constructor
77+
*
78+
* @param api OAuth2.0 api information
79+
* @param config OAuth 2.0 configuration param object
80+
*/
81+
public GitLabOAuthService(DefaultApi20 api, OAuthConfig config) {
82+
this.api = api;
83+
this.config = config;
84+
}
85+
86+
/**
87+
* {@inheritDoc}
88+
*/
89+
@Override
90+
public Token getAccessToken(Token requestToken, Verifier verifier) {
91+
OAuthRequest request =
92+
new OAuthRequest(api.getAccessTokenVerb(),
93+
api.getAccessTokenEndpoint());
94+
request.addBodyParameter(OAuthConstants.CLIENT_ID, config.getApiKey());
95+
request.addBodyParameter(OAuthConstants.CLIENT_SECRET,
96+
config.getApiSecret());
97+
request.addBodyParameter(OAuthConstants.CODE, verifier.getValue());
98+
request.addBodyParameter(OAuthConstants.REDIRECT_URI,
99+
config.getCallback());
100+
if (config.hasScope()) {
101+
request.addBodyParameter(OAuthConstants.SCOPE, config.getScope());
102+
}
103+
request.addBodyParameter(GRANT_TYPE, GRANT_TYPE_VALUE);
104+
Response response = request.send();
105+
return api.getAccessTokenExtractor().extract(response.getBody());
106+
}
107+
108+
/**
109+
* {@inheritDoc}
110+
*/
111+
@Override
112+
public Token getRequestToken() {
113+
throw new UnsupportedOperationException(
114+
"Unsupported operation, please use 'getAuthorizationUrl' and redirect your users there");
115+
}
116+
117+
/**
118+
* {@inheritDoc}
119+
*/
120+
@Override
121+
public String getVersion() {
122+
return VERSION;
123+
}
124+
125+
/**
126+
* {@inheritDoc}
127+
*/
128+
@Override
129+
public void signRequest(Token accessToken, OAuthRequest request) {
130+
request.addQuerystringParameter(OAuthConstants.ACCESS_TOKEN,
131+
accessToken.getToken());
132+
}
133+
134+
/**
135+
* {@inheritDoc}
136+
*/
137+
@Override
138+
public String getAuthorizationUrl(Token requestToken) {
139+
return api.getAuthorizationUrl(config);
140+
}
141+
}
142+
143+
private static final class GitLabJsonTokenExtractor implements
144+
AccessTokenExtractor {
145+
private Pattern accessTokenPattern = Pattern
146+
.compile("\"access_token\"\\s*:\\s*\"(\\S*?)\"");
147+
148+
@Override
149+
public Token extract(String response) {
150+
Preconditions.checkEmptyString(response,
151+
"Cannot extract a token from a null or empty String");
152+
Matcher matcher = accessTokenPattern.matcher(response);
153+
if (matcher.find()) {
154+
return new Token(matcher.group(1), "", response);
155+
} else {
156+
throw new OAuthException(
157+
"Cannot extract an acces token. Response was: " + response);
158+
}
159+
}
160+
}
161+
}
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
// Copyright (C) 2017 The Android Open Source Project
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package com.googlesource.gerrit.plugins.oauth;
16+
17+
import com.google.common.base.CharMatcher;
18+
import com.google.gerrit.extensions.annotations.PluginName;
19+
import com.google.gerrit.extensions.auth.oauth.OAuthServiceProvider;
20+
import com.google.gerrit.extensions.auth.oauth.OAuthToken;
21+
import com.google.gerrit.extensions.auth.oauth.OAuthUserInfo;
22+
import com.google.gerrit.extensions.auth.oauth.OAuthVerifier;
23+
import com.google.gerrit.server.config.CanonicalWebUrl;
24+
import com.google.gerrit.server.config.PluginConfig;
25+
import com.google.gerrit.server.config.PluginConfigFactory;
26+
import com.google.gson.JsonElement;
27+
import com.google.gson.JsonObject;
28+
import com.google.inject.Inject;
29+
import com.google.inject.Provider;
30+
import com.google.inject.Singleton;
31+
import org.scribe.builder.ServiceBuilder;
32+
import org.scribe.model.OAuthRequest;
33+
import org.scribe.model.Response;
34+
import org.scribe.model.Token;
35+
import org.scribe.model.Verb;
36+
import org.scribe.model.Verifier;
37+
import org.scribe.oauth.OAuthService;
38+
import org.slf4j.Logger;
39+
40+
import java.io.IOException;
41+
42+
import static com.google.gerrit.server.OutputFormat.JSON;
43+
import static javax.servlet.http.HttpServletResponse.SC_OK;
44+
import static org.slf4j.LoggerFactory.getLogger;
45+
46+
@Singleton
47+
public class GitLabOAuthService implements OAuthServiceProvider {
48+
private static final Logger log = getLogger(GitLabOAuthService.class);
49+
static final String CONFIG_SUFFIX = "-gitlab-oauth";
50+
private static final String PROTECTED_RESOURCE_URL =
51+
"%s/api/v3/user";
52+
private static final String GITLAB_PROVIDER_PREFIX ="gitlab-oauth:";
53+
private final OAuthService service;
54+
private final String rootUrl;
55+
56+
@Inject
57+
GitLabOAuthService(PluginConfigFactory cfgFactory,
58+
@PluginName String pluginName,
59+
@CanonicalWebUrl Provider<String> urlProvider) {
60+
PluginConfig cfg = cfgFactory.getFromGerritConfig(
61+
pluginName + CONFIG_SUFFIX);
62+
String canonicalWebUrl = CharMatcher.is('/').trimTrailingFrom(
63+
urlProvider.get()) + "/";
64+
rootUrl = cfg.getString(InitOAuth.ROOT_URL);
65+
service = new ServiceBuilder().provider(new GitLabApi(rootUrl))
66+
.apiKey(cfg.getString(InitOAuth.CLIENT_ID))
67+
.apiSecret(cfg.getString(InitOAuth.CLIENT_SECRET))
68+
.callback(canonicalWebUrl + "oauth")
69+
.build();
70+
}
71+
72+
@Override
73+
public OAuthUserInfo getUserInfo(OAuthToken token) throws IOException {
74+
final String protectedResourceUrl =
75+
String.format(PROTECTED_RESOURCE_URL, rootUrl);
76+
OAuthRequest request = new OAuthRequest(Verb.GET, protectedResourceUrl);
77+
Token t =
78+
new Token(token.getToken(), token.getSecret(), token.getRaw());
79+
service.signRequest(t, request);
80+
81+
Response response = request.send();
82+
if (response.getCode() != SC_OK) {
83+
throw new IOException(String.format("Status %s (%s) for request %s",
84+
response.getCode(), response.getBody(), request.getUrl()));
85+
}
86+
JsonElement userJson =
87+
JSON.newGson().fromJson(response.getBody(), JsonElement.class);
88+
if (log.isDebugEnabled()) {
89+
log.debug("User info response: {}", response.getBody());
90+
}
91+
JsonObject jsonObject = userJson.getAsJsonObject();
92+
if (jsonObject == null || jsonObject.isJsonNull()) {
93+
throw new IOException(
94+
"Response doesn't contain 'user' field" + jsonObject);
95+
}
96+
JsonElement id = jsonObject.get("id");
97+
JsonElement username = jsonObject.get("username");
98+
JsonElement email = jsonObject.get("email");
99+
JsonElement name = jsonObject.get("name");
100+
return new OAuthUserInfo(GITLAB_PROVIDER_PREFIX + id.getAsString(),
101+
username == null || username.isJsonNull()
102+
? null
103+
: username.getAsString(),
104+
email == null || email.isJsonNull() ? null : email.getAsString(),
105+
name == null || name.isJsonNull() ? null : name.getAsString(),
106+
null);
107+
}
108+
109+
@Override
110+
public OAuthToken getAccessToken(OAuthVerifier rv) {
111+
Verifier vi = new Verifier(rv.getValue());
112+
Token to = service.getAccessToken(null, vi);
113+
return new OAuthToken(to.getToken(), to.getSecret(), null);
114+
}
115+
116+
@Override
117+
public String getAuthorizationUrl() {
118+
return service.getAuthorizationUrl(null);
119+
}
120+
121+
@Override
122+
public String getVersion() {
123+
return service.getVersion();
124+
}
125+
126+
@Override
127+
public String getName() {
128+
return "GitLab OAuth2";
129+
}
130+
}

src/main/java/com/googlesource/gerrit/plugins/oauth/HttpModule.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,5 +75,13 @@ protected void configureServlets() {
7575
Exports.named(FacebookOAuthService.CONFIG_SUFFIX)).to(
7676
FacebookOAuthService.class);
7777
}
78+
79+
cfg = cfgFactory.getFromGerritConfig(
80+
pluginName + GitLabOAuthService.CONFIG_SUFFIX);
81+
if (cfg.getString(InitOAuth.CLIENT_ID) != null) {
82+
bind(OAuthServiceProvider.class)
83+
.annotatedWith(Exports.named(GitLabOAuthService.CONFIG_SUFFIX))
84+
.to(GitLabOAuthService.class);
85+
}
7886
}
7987
}

src/main/java/com/googlesource/gerrit/plugins/oauth/InitOAuth.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ class InitOAuth implements InitStep {
4040
private final Section bitbucketOAuthProviderSection;
4141
private final Section casOAuthProviderSection;
4242
private final Section facebookOAuthProviderSection;
43+
private final Section gitlabOAuthProviderSection;
4344

4445
@Inject
4546
InitOAuth(ConsoleUI ui,
@@ -56,6 +57,8 @@ class InitOAuth implements InitStep {
5657
PLUGIN_SECTION, pluginName + CasOAuthService.CONFIG_SUFFIX);
5758
this.facebookOAuthProviderSection = sections.get(
5859
PLUGIN_SECTION, pluginName + FacebookOAuthService.CONFIG_SUFFIX);
60+
this.gitlabOAuthProviderSection = sections.get(
61+
PLUGIN_SECTION, pluginName + GitLabOAuthService.CONFIG_SUFFIX);
5962
}
6063

6164
@Override
@@ -100,6 +103,13 @@ public void run() throws Exception {
100103
if (configueFacebookOAuthProvider) {
101104
configureOAuth(facebookOAuthProviderSection);
102105
}
106+
107+
boolean configureGitLabOAuthProvider = ui.yesno(
108+
true, "Use GitLab OAuth provider for Gerrit login ?");
109+
if (configureGitLabOAuthProvider) {
110+
gitlabOAuthProviderSection.string("GitLab Root URL", ROOT_URL, null);
111+
configureOAuth(gitlabOAuthProviderSection);
112+
}
103113
}
104114

105115
private void configureOAuth(Section s) {

src/main/resources/Documentation/config.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ appended with provider suffix: e.g. `-google-oauth` or `-github-oauth`:
2626
root-url = "<cas url>"
2727
client-id = "<client-id>"
2828
client-secret = "<client-secret>"
29+
30+
[plugin "@PLUGIN@-gitlab-oauth"]
31+
root-url = "<gitlab url>"
32+
client-id = "<client-id>"
33+
client-secret = "<client-secret>"
2934
```
3035

3136
When one from the sections above is omitted, OAuth SSO is used.
@@ -147,3 +152,22 @@ service definition and need to be set manually.
147152
See
148153
[the CAS documentation](https://apereo.github.io/cas/4.2.x/installation/OAuth-OpenId-Authentication.html#add-oauth-clients)
149154
for an example.
155+
156+
### GitLab
157+
158+
To obtain client-id and client-secret for GitLab OAuth, go to
159+
Applications settings in your GitLab profile:
160+
161+
- Select "Save application" and enter information about the
162+
application.
163+
164+
Note that it is important that Redirect URI points to
165+
`<canonical-web-uri-of-gerrit>/oauth`.
166+
167+
![Save new application on GitLab](images/gitlab-1.png)
168+
169+
170+
After application is saved, the page will show generated client id and
171+
secret.
172+
173+
![Generated client id and secret](images/gitlab-2.png)
Loading
Loading

0 commit comments

Comments
 (0)