blob: 02d60206316594caeaa127cf625711aaa2a8d75b [file] [log] [blame]
// Copyright (C) 2013 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.github.oauth;
import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN;
import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
import com.google.common.base.MoreObjects;
import com.google.common.base.Strings;
import com.google.common.collect.Iterators;
import com.google.gerrit.httpd.XGerritAuth;
import com.google.gerrit.server.account.AccountCache;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import com.googlesource.gerrit.plugins.github.oauth.OAuthProtocol.AccessToken;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang.StringUtils;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.concurrent.ExecutionException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
@Singleton
public class OAuthGitFilter implements Filter {
private static final String GITHUB_X_OAUTH_BASIC = "x-oauth-basic";
private static final org.slf4j.Logger log = LoggerFactory
.getLogger(OAuthGitFilter.class);
public static final String GIT_REALM_NAME =
"GitHub authentication for Gerrit Code Review";
private static final String GIT_AUTHORIZATION_HEADER = "Authorization";
private static final String GIT_AUTHENTICATION_BASIC = "Basic ";
private final OAuthCache oauthCache;
private final AccountCache accountCache;
private final Provider<HttpClient> httpClientProvider;
private final GitHubOAuthConfig config;
private final XGerritAuth xGerritAuth;
private ScopedProvider<GitHubLogin> ghLoginProvider;
public static class BasicAuthHttpRequest extends HttpServletRequestWrapper {
private HashMap<String, String> headers = new HashMap<>();
public BasicAuthHttpRequest(HttpServletRequest request, String username,
String password) {
super(request);
try {
headers.put(
GIT_AUTHORIZATION_HEADER,
GIT_AUTHENTICATION_BASIC
+ Base64.encodeBase64String((username + ":" + password)
.getBytes(OAuthGitFilter.encoding(request))));
} catch (UnsupportedEncodingException e) {
// This cannot really happen as we have already used the encoding for
// decoding the request
}
}
@Override
public Enumeration<String> getHeaderNames() {
final Enumeration<String> wrappedHeaderNames = super.getHeaderNames();
HashSet<String> headerNames = new HashSet<>(headers.keySet());
while (wrappedHeaderNames.hasMoreElements()) {
headerNames.add(wrappedHeaderNames.nextElement());
}
return Iterators.asEnumeration(headerNames.iterator());
}
@Override
public String getHeader(String name) {
String headerValue = headers.get(name);
if (headerValue != null) {
return headerValue;
}
return super.getHeader(name);
}
}
@Inject
public OAuthGitFilter(OAuthCache oauthCache, AccountCache accountCache,
PooledHttpClientProvider httpClientProvider, GitHubOAuthConfig config,
XGerritAuth xGerritAuth, GitHubLogin.Provider ghLoginProvider) {
this.oauthCache = oauthCache;
this.accountCache = accountCache;
this.httpClientProvider = httpClientProvider;
this.config = config;
this.xGerritAuth = xGerritAuth;
this.ghLoginProvider = ghLoginProvider;
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse =
new OAuthGitWrappedResponse((HttpServletResponse) response);
GitHubLogin ghLogin = ghLoginProvider.get(httpRequest);
log.debug("OAuthGitFilter(" + httpRequest.getRequestURL() + ") ghLogin="
+ ghLogin);
String username =
getAuthenticatedUserFromGitRequestUsingOAuthToken(httpRequest,
httpResponse);
if (username != null) {
String gerritPassword =
accountCache.getByUsername(username).getPassword(username);
if (gerritPassword == null) {
gerritPassword =
generateRandomGerritPassword(username, httpRequest, httpResponse,
chain);
if (Strings.isNullOrEmpty(gerritPassword)) {
httpResponse.sendError(SC_FORBIDDEN,
"Unable to generate Gerrit password for Git Basic-Auth");
} else {
httpResponse.sendRedirect(getRequestPathWithQueryString(httpRequest));
}
return;
}
httpRequest =
new BasicAuthHttpRequest(httpRequest, username, gerritPassword);
}
chain.doFilter(httpRequest, httpResponse);
}
private String getRequestPathWithQueryString(HttpServletRequest httpRequest) {
String requestPathWithQueryString =
httpRequest.getContextPath() + httpRequest.getServletPath()
+ Strings.nullToEmpty(httpRequest.getPathInfo()) + "?"
+ httpRequest.getQueryString();
return requestPathWithQueryString;
}
private String generateRandomGerritPassword(String username,
HttpServletRequest httpRequest, HttpServletResponse httpResponse,
FilterChain chain) throws IOException, ServletException {
log.warn("User " + username + " has not a Gerrit HTTP password: "
+ "generating a random one in order to be able to use Git over HTTP");
Cookie gerritCookie =
getGerritLoginCookie(username, httpRequest, httpResponse, chain);
String xGerritAuthValue = xGerritAuth.getAuthValue(gerritCookie);
HttpPut putRequest =
new HttpPut(getRequestUrlWithAlternatePath(httpRequest,
"/accounts/self/password.http"));
putRequest.setHeader("Cookie",
gerritCookie.getName() + "=" + gerritCookie.getValue());
putRequest.setHeader(XGerritAuth.X_GERRIT_AUTH, xGerritAuthValue);
putRequest.setEntity(new StringEntity("{\"generate\":true}",
ContentType.APPLICATION_JSON));
HttpResponse putResponse = httpClientProvider.get().execute(putRequest);
if (putResponse.getStatusLine().getStatusCode() != HttpStatus.SC_OK) {
throw new ServletException(
"Cannot generate HTTP password for authenticating user " + username);
}
return accountCache.getByUsername(username).getPassword(username);
}
private URI getRequestUrlWithAlternatePath(HttpServletRequest httpRequest,
String alternatePath) throws MalformedURLException {
URL originalUrl = new URL(httpRequest.getRequestURL().toString());
String contextPath = httpRequest.getContextPath();
return URI.create(originalUrl.getProtocol() + "://" + originalUrl.getHost()
+ ":" + getPort(originalUrl) + contextPath + alternatePath);
}
private int getPort(URL originalUrl) {
String protocol = originalUrl.getProtocol().toLowerCase();
int port = originalUrl.getPort();
if (port == -1) {
return protocol.equals("https") ? 443 : 80;
}
return port;
}
private Cookie getGerritLoginCookie(String username,
HttpServletRequest httpRequest, HttpServletResponse httpResponse,
FilterChain chain) throws IOException, ServletException {
AuthenticatedPathHttpRequest loginRequest =
new AuthenticatedLoginHttpRequest(httpRequest, config.httpHeader,
username);
AuthenticatedLoginHttpResponse loginResponse =
new AuthenticatedLoginHttpResponse(httpResponse);
chain.doFilter(loginRequest, loginResponse);
return loginResponse.getGerritCookie();
}
private String getAuthenticatedUserFromGitRequestUsingOAuthToken(
HttpServletRequest req, HttpServletResponse rsp) throws IOException {
final String httpBasicAuth = getHttpBasicAuthenticationHeader(req);
if (httpBasicAuth == null) {
return null;
}
if (isInvalidHttpAuthenticationHeader(httpBasicAuth)) {
rsp.sendError(SC_UNAUTHORIZED);
return null;
}
String oauthToken = StringUtils.substringBefore(httpBasicAuth, ":");
String oauthKeyword = StringUtils.substringAfter(httpBasicAuth, ":");
if (Strings.isNullOrEmpty(oauthToken)
|| Strings.isNullOrEmpty(oauthKeyword)) {
rsp.sendError(SC_UNAUTHORIZED);
return null;
}
if (!oauthKeyword.equalsIgnoreCase(GITHUB_X_OAUTH_BASIC)) {
return null;
}
boolean loginSuccessful = false;
String oauthLogin = null;
try {
oauthLogin =
oauthCache.getLoginByAccessToken(new AccessToken(oauthToken));
loginSuccessful = !Strings.isNullOrEmpty(oauthLogin);
} catch (ExecutionException e) {
log.warn("Login failed for OAuth token " + oauthToken, e);
loginSuccessful = false;
}
if (!loginSuccessful) {
rsp.sendError(SC_FORBIDDEN);
return null;
}
return oauthLogin;
}
private boolean isInvalidHttpAuthenticationHeader(String usernamePassword) {
return usernamePassword.indexOf(':') < 1;
}
static String encoding(HttpServletRequest req) {
return MoreObjects.firstNonNull(req.getCharacterEncoding(), "UTF-8");
}
private String getHttpBasicAuthenticationHeader(final HttpServletRequest req)
throws UnsupportedEncodingException {
String hdr = req.getHeader(GIT_AUTHORIZATION_HEADER);
if (hdr == null || !hdr.startsWith(GIT_AUTHENTICATION_BASIC)) {
return null;
}
return new String(Base64.decodeBase64(hdr
.substring(GIT_AUTHENTICATION_BASIC.length())), encoding(req));
}
@Override
public void destroy() {
log.info("Destroy");
}
}