OAuth2 authentication for Git-over-HTTP

OAuth2 support was only implemented for the web UI so far
but not for Git-over-HTTP communication. This patch adds
a mechanism similiar to that supported by Github,
where Git clients may send OAuth2 access tokens instead
of passwords in a Basic authentication header [1].

Received access tokens will be verified by means of an
OAuthLoginProvider, which is a new extension point.
The OAuth2 protocol does not specify a mechanism for how to
verify access tokens, so there is no default implementation
for the OAuthLoginProvider interface, but a plugin must
provide a suitable implementation.

In order to enable OAuth2 authentication for Git-over-HTTP
the configuration option auth.type must be set to OAUTH
and auth.gitBasicAuth must be set to true. The parameter
auth.gitOAuthProvider defines the default OAuthLoginProvider
to use in case multiple OAuthLoginProvider implementations
are installed and it cannot be deduced from the request,
which OAuth provider to address.

An OAuthLoginProvider implementation may also support
HTTP Basic authentication with passwords instead of access
tokens, if the OAuth2 backend supports the Resource Owner
Password Credentials Grant authentication flow [2] or some
other API for verifying password credentials. For that reason
the second parameter of the OAuthLoginProvider interface is
called "secret" instead of "accessToken".

An example implementation for the OAuthLoginProvider
extension point will be contributed to the cfoauth plugin.

[1] https://developer.github.com/v3/auth/#basic-authentication
[2] https://tools.ietf.org/html/rfc6749#section-4.3

Change-Id: I0f00599dce38a806fd3e21758ea9e2cab49ce57f
Signed-off-by: Michael Ochmann <michael.ochmann@sap.com>
This commit is contained in:
Michael Ochmann
2015-10-20 15:34:29 +02:00
parent 386ff83c94
commit e9e046a4b3
8 changed files with 516 additions and 5 deletions

View File

@@ -0,0 +1,121 @@
// 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.google.gerrit.server.auth.oauth;
import com.google.common.base.Strings;
import com.google.gerrit.extensions.auth.oauth.OAuthLoginProvider;
import com.google.gerrit.extensions.auth.oauth.OAuthUserInfo;
import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Account.FieldName;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.account.AbstractRealm;
import com.google.gerrit.server.account.AccountException;
import com.google.gerrit.server.account.AccountManager;
import com.google.gerrit.server.account.AuthRequest;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.io.IOException;
@Singleton
public class OAuthRealm extends AbstractRealm {
private final DynamicMap<OAuthLoginProvider> loginProviders;
@Inject
OAuthRealm(DynamicMap<OAuthLoginProvider> loginProviders) {
this.loginProviders = loginProviders;
}
@Override
public boolean allowsEdit(FieldName field) {
return false;
}
/**
* Authenticates with the {@link OAuthLoginProvider} specified
* in the authentication request.
*
* {@link AccountManager} calls this method without password
* if authenticity of the user has already been established.
* In that case the {@link AuthRequest} is supposed to contain
* a resolved email address and we can skip the authentication
* request to the {@code OAuthLoginService}.
*
* @param who the authentication request.
*
* @return the authentication request with resolved email address
* and display name in case the authenticity of the user could
* be established; otherwise {@code who} is returned unchanged.
*
* @throws AccountException if the authentication request with
* the OAuth2 server failed or no {@code OAuthLoginProvider} was
* available to handle the request.
*/
@Override
public AuthRequest authenticate(AuthRequest who) throws AccountException {
if (Strings.isNullOrEmpty(who.getPassword()) &&
!Strings.isNullOrEmpty(who.getEmailAddress())) {
return who;
}
if (Strings.isNullOrEmpty(who.getAuthPlugin())
|| Strings.isNullOrEmpty(who.getAuthProvider())) {
throw new AccountException("Cannot authenticate");
}
OAuthLoginProvider loginProvider =
loginProviders.get(who.getAuthPlugin(), who.getAuthProvider());
if (loginProvider == null) {
throw new AccountException("Cannot authenticate");
}
OAuthUserInfo userInfo;
try {
userInfo = loginProvider.login(who.getUserName(), who.getPassword());
} catch (IOException e) {
throw new AccountException("Cannot authenticate", e);
}
if (userInfo == null) {
throw new AccountException("Cannot authenticate");
}
if (!Strings.isNullOrEmpty(userInfo.getEmailAddress())) {
who.setEmailAddress(userInfo.getEmailAddress());
}
if (!Strings.isNullOrEmpty(userInfo.getDisplayName())) {
who.setDisplayName(userInfo.getDisplayName());
}
return who;
}
@Override
public AuthRequest link(ReviewDb db, Account.Id to, AuthRequest who) {
return who;
}
@Override
public AuthRequest unlink(ReviewDb db, Account.Id to, AuthRequest who)
throws AccountException {
return who;
}
@Override
public void onCreateAccount(AuthRequest who, Account account) {
}
@Override
public Account.Id lookup(String accountName) {
return null;
}
}