OAuth2 support for Git-over-HTTP communication
CFOAuthService now implements also the extension point
OAuthLoginProvider. If an UAA access token is passed to the
service the UAA /check_token endpoint is called to verify
the token. If instead of an access token a password is passed
to the service it tries to obtain a fresh access token by
sending the user credentials to the UAA ("Resource Owner
Password Credentials Grant", see RFC6749 section 4.3).
The plugin supports both ordinary users and clients,
which are some sort of technical users provided by UAA.
Both can obtain access tokens for communication with a
resource server, i.e. Gerrit, but the attributes of
these tokens are different and must therefore be
evaluated differently.
This patch depends on
https://gerrit-review.googlesource.com/#/c/71735
Change-Id: I6ba255dde92563ef6ebad9481683d89a151bea61
Signed-off-by: Michael Ochmann <michael.ochmann@sap.com>
diff --git a/BUCK b/BUCK
index f20aa8a..39d571d 100644
--- a/BUCK
+++ b/BUCK
@@ -10,6 +10,7 @@
'Gerrit-PluginName: cfoauth',
'Gerrit-ApiType: plugin',
'Gerrit-ApiVersion: 2.12-SNAPSHOT',
+ 'Gerrit-Module: com.googlesource.gerrit.plugins.cfoauth.OAuthModule',
'Gerrit-HttpModule: com.googlesource.gerrit.plugins.cfoauth.HttpModule',
'Gerrit-InitStep: com.googlesource.gerrit.plugins.cfoauth.InitOAuthConfig',
'Implementation-Title: Cloud Foundry UAA OAuth 2.0 Authentication Provider',
diff --git a/src/main/java/com/googlesource/gerrit/plugins/cfoauth/CFOAuthService.java b/src/main/java/com/googlesource/gerrit/plugins/cfoauth/CFOAuthService.java
index ee919cb..2fde5d9 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/cfoauth/CFOAuthService.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/cfoauth/CFOAuthService.java
@@ -16,10 +16,12 @@
import com.google.common.base.CharMatcher;
import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.extensions.auth.oauth.OAuthLoginProvider;
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.reviewdb.client.AccountExternalId;
import com.google.gerrit.server.config.CanonicalWebUrl;
import com.google.gerrit.server.config.PluginConfig;
import com.google.gerrit.server.config.PluginConfigFactory;
@@ -30,7 +32,7 @@
import java.io.IOException;
@Singleton
-class CFOAuthService implements OAuthServiceProvider {
+class CFOAuthService implements OAuthServiceProvider, OAuthLoginProvider {
private static final String OAUTH_VERSION = "2.0";
private static final String NAME = "Cloud Foundry UAA OAuth2";
@@ -78,6 +80,45 @@
}
@Override
+ public OAuthUserInfo login(String username, String secret)
+ throws IOException {
+ if (username == null || secret == null) {
+ throw new IOException("Authentication error");
+ }
+ AccessToken accessToken;
+ try {
+ if (uaaClient.isAccessTokenForClient(username, secret)) {
+ // "secret" is an access token for a client, i.e. a
+ // technical user; send it to UAA for verification
+ if (!uaaClient.verifyAccessToken(secret)) {
+ throw new IOException("Authentication error");
+ }
+ return getAsOAuthUserInfo(username);
+ } else {
+ if (uaaClient.isAccessTokenForUser(username, secret)) {
+ // "secret" is an access token for an ordinary user;
+ // send it to UAA for verification
+ if (!uaaClient.verifyAccessToken(secret)) {
+ throw new IOException("Authentication error");
+ }
+ accessToken = uaaClient.toAccessToken(secret);
+ } else {
+ // "secret" is not an access token but likely a password;
+ // send username and password to UAA and try to get an access
+ // token; if that succeeds the user is authenticated
+ accessToken = uaaClient.getAccessToken(username, secret);
+ }
+ UserInfo userInfo = accessToken.getUserInfo();
+ userInfo.setDisplayName(
+ uaaClient.getDisplayName(accessToken.getValue()));
+ return getAsOAuthUserInfo(userInfo);
+ }
+ } catch (UAAClientException e) {
+ throw new IOException("Authentication error", e);
+ }
+ }
+
+ @Override
public String getVersion() {
return OAUTH_VERSION;
}
@@ -96,4 +137,9 @@
userInfo.getUserName(), userInfo.getEmailAddress(),
userInfo.getDisplayName(), null);
}
+
+ private static OAuthUserInfo getAsOAuthUserInfo(String username) {
+ return new OAuthUserInfo(AccountExternalId.SCHEME_EXTERNAL + username,
+ username, null, null, null);
+ }
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/cfoauth/OAuthModule.java b/src/main/java/com/googlesource/gerrit/plugins/cfoauth/OAuthModule.java
new file mode 100644
index 0000000..b1dcc8c
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/cfoauth/OAuthModule.java
@@ -0,0 +1,47 @@
+// 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.cfoauth;
+
+import com.google.gerrit.extensions.annotations.Exports;
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.extensions.auth.oauth.OAuthLoginProvider;
+import com.google.gerrit.server.config.PluginConfig;
+import com.google.gerrit.server.config.PluginConfigFactory;
+import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
+
+class OAuthModule extends AbstractModule {
+
+ private final PluginConfigFactory cfgFactory;
+ private final String pluginName;
+
+ @Inject
+ OAuthModule(PluginConfigFactory cfgFactory,
+ @PluginName String pluginName) {
+ this.cfgFactory = cfgFactory;
+ this.pluginName = pluginName;
+ }
+
+ @Override
+ protected void configure() {
+ PluginConfig cfg = cfgFactory.getFromGerritConfig(pluginName);
+ if (cfg.getString(InitOAuthConfig.CLIENT_ID) != null) {
+ bind(OAuthLoginProvider.class)
+ .annotatedWith(Exports.named(pluginName))
+ .to(CFOAuthService.class);
+ }
+ }
+
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/cfoauth/UAAClient.java b/src/main/java/com/googlesource/gerrit/plugins/cfoauth/UAAClient.java
index 259339d..c1a5f1f 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/cfoauth/UAAClient.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/cfoauth/UAAClient.java
@@ -42,11 +42,17 @@
private static final String AUTHORIZE_ENDPOINT = OAUTH_ENDPOINT
+ "authorize?response_type=code&client_id=%s&redirect_uri=%s";
private static final String TOKEN_ENDPOINT = OAUTH_ENDPOINT + "token";
+ private static final String CHECK_TOKEN_ENDPOINT = "%s/check_token";
private static final String TOKEN_KEY_ENDPOINT = "%s/token_key";
private static final String USERINFO_ENDPOINT = "%s/userinfo";
private static final String GRANT_TYPE = "grant_type";
private static final String BY_AUTHORIZATION_CODE = "authorization_code";
+ private static final String BY_PASSWORD = "password";
+
+ private static final String USERNAME_PARAMETER = "username";
+ private static final String PASSWORD_PARAMETER = "password";
+ private static final String TOKEN_PARAMETER = "token";
private static final String ALG_ATTRIBUTE = "alg";
private static final String VALUE_ATTRIBUTE = "value";
@@ -54,6 +60,7 @@
private static final String MODULUS_ATTRIBUTE = "n";
private static final String ACCESS_TOKEN_ATTRIBUTE = "access_token";
private static final String EXP_ATTRIBUTE = "exp";
+ private static final String SUB_ATTRIBUTE = "sub";
private static final String USER_NAME_ATTRIBUTE = "user_name";
private static final String EMAIL_ATTRIBUTE = "email";
private static final String NAME_ATTRIBUTE = "name";
@@ -67,6 +74,7 @@
private final String authorizationEndpoint;
private final String accessTokenEndpoint;
+ private final String checkTokenEndpoint;
private final String tokenKeyEndpoint;
private final String userInfoEndpoint;
@@ -90,6 +98,7 @@
this.authorizationEndpoint = String.format(AUTHORIZE_ENDPOINT,
uaaServerUrl, encode(clientId), encode(redirectUrl));
this.accessTokenEndpoint = String.format(TOKEN_ENDPOINT, uaaServerUrl);
+ this.checkTokenEndpoint = String.format(CHECK_TOKEN_ENDPOINT, uaaServerUrl);
this.tokenKeyEndpoint = String.format(TOKEN_KEY_ENDPOINT, uaaServerUrl);
this.userInfoEndpoint = String.format(USERINFO_ENDPOINT, uaaServerUrl);
}
@@ -132,6 +141,106 @@
}
/**
+ * Retrieves an access token from the UAA server providing a user name
+ * and password following the "Resource Owner Password Credentials Grant"
+ * scheme of RFC6749 section 4.3.
+ *
+ * @param username the name of the resource owner.
+ * @param password the password of the resource owner.
+ * @return an access token.
+ *
+ * @throws UAAClientException if the UAA request failed.
+ */
+ public AccessToken getAccessToken(String username, String password)
+ throws UAAClientException{
+ if (username == null || password == null) {
+ throw new UAAClientException("Must provide user name and password");
+ }
+ OAuthRequest request = new OAuthRequest(POST, accessTokenEndpoint);
+ request.addHeader(AUTHORIZATION_HEADER, clientCredentials);
+ request.addQuerystringParameter(GRANT_TYPE, BY_PASSWORD);
+ request.addQuerystringParameter(USERNAME_PARAMETER, username);
+ request.addQuerystringParameter(PASSWORD_PARAMETER, password);
+ Response response = request.send();
+ if (response.getCode() == 401) {
+ throw new UAAClientException("Invalid username or password");
+ }
+ if (response.getCode() != 200) {
+ throw new UAAClientException(MessageFormat.format(
+ "POST /oauth/token failed with status {0}", response.getCode()));
+ }
+ String tokenResponse = response.getBody();
+ if (Strings.isNullOrEmpty(tokenResponse)) {
+ throw new UAAClientException(
+ "POST /oauth/token failed: invalid access token response");
+ }
+ return parseAccessTokenResponse(tokenResponse);
+ }
+
+ /**
+ * Verifies the given access token with the UAA server.
+ * This method passes the access token to the <tt>/check_token</tt>
+ * endpoint of the UAA server.
+ *
+ * @param accessToken the access token to verify.
+ * @return <code>true</code> if the token could be verified.
+ *
+ * @throws UAAClientException if the UAA request failed.
+ */
+ public boolean verifyAccessToken(String accessToken)
+ throws UAAClientException {
+ OAuthRequest request = new OAuthRequest(POST, checkTokenEndpoint);
+ request.addHeader(AUTHORIZATION_HEADER, clientCredentials);
+ request.addBodyParameter(TOKEN_PARAMETER, accessToken);
+ Response response = request.send();
+ if (response.getCode() == 400) {
+ return false;
+ }
+ if (response.getCode() != 200) {
+ throw new UAAClientException(MessageFormat.format(
+ "POST /check_token failed with status {0}", response.getCode()));
+ }
+ return true;
+ }
+
+ /**
+ * Checks if the given access token is valid and is owned by the given user.
+ *
+ * @param username the name of the token owner.
+ * @param accessToken the access token to check.
+ *
+ * @return <code>true</code> if the token is valid and belongs to
+ * the given user.
+ */
+ public boolean isAccessTokenForUser(String username, String accessToken) {
+ try {
+ JsonObject jsonWebToken = toJsonWebToken(accessToken);
+ return username.equals(getAttribute(jsonWebToken, USER_NAME_ATTRIBUTE));
+ } catch (UAAClientException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Checks if the given access token is valid and is owned by the given client.
+ *
+ * @param clientname the name of the client.
+ * @param accessToken the access token to check.
+ *
+ * @return <code>true</code> if the token is valid and belongs to
+ * the given client.
+ */
+ public boolean isAccessTokenForClient(String clientname, String accessToken) {
+ try {
+ JsonObject jsonWebToken = toJsonWebToken(accessToken);
+ return getAttribute(jsonWebToken, USER_NAME_ATTRIBUTE) == null &&
+ clientname.equals(getAttribute(jsonWebToken, SUB_ATTRIBUTE));
+ } catch (UAAClientException e) {
+ return false;
+ }
+ }
+
+ /**
* Converts an access token given as string represenation
* into an {@link AccessToken}.
*
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index 75fc683..1ca272f 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -5,7 +5,7 @@
Gerrit must be registered as client with the [CloudFoundry User Account and
Authentication (UAA) Server](https://github.com/cloudfoundry/uaa) that acts
-as OAuth2 authentication and authorization backend.
+as OAuth 2 authentication and authorization backend.
The following sequence assumes that the UAA client application (`uaac`) is
installed. It will create a client with name `gerrit`.
@@ -22,20 +22,27 @@
--autoapprove openid
--access_token_validity <time in seconds>
--redirect_uri <URL of the Gerrit server>/oauth
- --secret <client secret>
+ --secret <secret>
```
-Make sure to choose a strong password for `secret`. Gerrit uses
-this password to obtain access tokens on behalf of its users.
+Make sure to choose a reasonable access token validity if you want to use
+access tokens to authenticate Git over HTTP communication. If tokens expire
+frequently using them with a native Git client might be cumbersome.
+On the other side, acccess tokens should be treated like passwords and
+should be changed from time to time for security reasons.
+
+Make sure to choose a strong password for `secret`.
## Configuring the @PLUGIN@ Plugin
The configuration of the @PLUGIN@ plugin is done in the `gerrit.config`
-file. Note that `auth.type` must be set to `OAUTH`.
+file.
```
[auth]
type = OAUTH
+ gitBacicAuth = true
+ gitOAuthProvider = cfoauth:cfoauth
[plugin "@PLUGIN@"]
serverUrl = <URL of the UAA server>
@@ -44,26 +51,24 @@
verifySignatures = true
```
+The `type` must be set to `OAUTH`.
+
+When the `gitBasicAuth` parameter is set to `true`, the UAA will be used
+to also authenticate Git over HTTP communication. If there are multiple
+OAuth providers installed that are capable of authenticating Git over HTTP
+traffic, add the parameter `gitOAuthProvider = cfoauth:cfoauth` to select
+the @PLUGIN@ plugin as default OAuth provider.
+
+For Git over HTTP communication the plugin accepts passwords and OAuth2
+access tokens sent in an `Authorization` header following the `BASIC`
+authentication scheme (RFC 2617 section 2). The plugin will pass
+credentials directly to UAA for verification.
+
+The `serverUrl` must point to the UAA server and include the
+context path, e.g `http(s)://example.org/uaa`.
+
The parameters `clientId` and `clientSecret` must match the name and
-password of the Gerrit client as registered with the UAA server above.
-The `serverUrl` must point to the UAA server and include the context path,
-e.g `http(s)://example.org/uaa`.
-
-Alternatively, re-run `init` to configure the @PLUGIN@ plugin:
-
-```
- java -jar gerrit.war init -d <site>
- [...]
-
- *** Cloud Foundry UAA OAuth 2.0 Authentication Provider
- ***
-
- UAA server URL [http://localhost:8080/uaa]: <URL of the UAA server>
- Client id [gerrit]: <client id>
- Client secret : <client secret>
- confirm password : <client secret>
- Verify token signatures [Y/n]?
-```
+password of the Gerrit client as registered with the UAA server.
UAA issues so-called [JSON Web Tokens](http://tools.ietf.org/html/rfc7519]),
which include a signature. By default, the @PLUGIN@ plugin will verify
@@ -73,3 +78,22 @@
the verification by setting the parameter `verifySignatures` to `false`.
Note that this is strongly discouraged for security reasons.
+## Using Init for Configuring the @PLUGIN@ Plugin
+
+The @PLUGIN@ plugin can also be configured during setting up Gerrit with
+the `init` command:
+
+```
+ java -jar gerrit.war init -d <site>
+ [...]
+
+ *** Cloud Foundry UAA OAuth 2.0 Authentication Provider
+ ***
+
+ UAA server URL [http://localhost:8080/uaa]: <serverUrl>
+ Client id [gerrit]: <clientId>
+ Client secret : <clientSecret>
+ confirm password : <clientSecret>
+ Verify token signatures [Y/n]? <verifySignatures>
+```
+