blob: 4b5742d06a4a2d176f1cf66cc10ca5ccc0811baf [file] [log] [blame]
// 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.httpd;
import static com.google.gerrit.httpd.ProjectBasicAuthFilter.authenticationFailedMsg;
import static java.nio.charset.StandardCharsets.UTF_8;
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.Iterables;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.entities.Account;
import com.google.gerrit.extensions.auth.oauth.OAuthLoginProvider;
import com.google.gerrit.extensions.registration.DynamicItem;
import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.extensions.registration.Extension;
import com.google.gerrit.server.AccessPath;
import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.account.AccountException;
import com.google.gerrit.server.account.AccountManager;
import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.account.AuthRequest;
import com.google.gerrit.server.account.AuthResult;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.Locale;
import java.util.NoSuchElementException;
import java.util.Optional;
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.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import org.apache.commons.codec.binary.Base64;
import org.eclipse.jgit.lib.Config;
/**
* Authenticates the current user with an OAuth2 server.
*
* @see <a href="https://tools.ietf.org/rfc/rfc6750.txt">RFC 6750</a>
*/
@Singleton
class ProjectOAuthFilter implements Filter {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private static final String REALM_NAME = "Gerrit Code Review";
private static final String AUTHORIZATION = "Authorization";
private static final String BASIC = "Basic ";
private static final String GIT_COOKIE_PREFIX = "git-";
private final DynamicItem<WebSession> session;
private final DynamicMap<OAuthLoginProvider> loginProviders;
private final AccountCache accountCache;
private final AccountManager accountManager;
private final String gitOAuthProvider;
private final boolean userNameToLowerCase;
private String defaultAuthPlugin;
private String defaultAuthProvider;
@Inject
ProjectOAuthFilter(
DynamicItem<WebSession> session,
DynamicMap<OAuthLoginProvider> pluginsProvider,
AccountCache accountCache,
AccountManager accountManager,
@GerritServerConfig Config gerritConfig) {
this.session = session;
this.loginProviders = pluginsProvider;
this.accountCache = accountCache;
this.accountManager = accountManager;
this.gitOAuthProvider = gerritConfig.getString("auth", null, "gitOAuthProvider");
this.userNameToLowerCase = gerritConfig.getBoolean("auth", null, "userNameToLowerCase", false);
}
@Override
public void init(FilterConfig config) throws ServletException {
if (Strings.isNullOrEmpty(gitOAuthProvider)) {
pickOnlyProvider();
} else {
pickConfiguredProvider();
}
}
@Override
public void destroy() {}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
Response rsp = new Response((HttpServletResponse) response);
if (verify(req, rsp)) {
chain.doFilter(req, rsp);
}
}
private boolean verify(HttpServletRequest req, Response rsp) throws IOException {
AuthInfo authInfo;
// first check if there is a BASIC authentication header
String hdr = req.getHeader(AUTHORIZATION);
if (hdr != null && hdr.startsWith(BASIC)) {
authInfo = extractAuthInfo(hdr, encoding(req));
if (authInfo == null) {
rsp.sendError(SC_UNAUTHORIZED);
return false;
}
} else {
// if there is no BASIC authentication header, check if there is
// a cookie starting with the prefix "git-"
Cookie cookie = findGitCookie(req);
if (cookie != null) {
authInfo = extractAuthInfo(cookie);
if (authInfo == null) {
rsp.sendError(SC_UNAUTHORIZED);
return false;
}
} else {
// if there is no authentication information at all, it might be
// an anonymous connection, or there might be a session cookie
return true;
}
}
// if there is authentication information but no secret => 401
if (Strings.isNullOrEmpty(authInfo.tokenOrSecret)) {
rsp.sendError(SC_UNAUTHORIZED);
return false;
}
Optional<AccountState> who =
accountCache.getByUsername(authInfo.username).filter(a -> a.account().isActive());
if (!who.isPresent()) {
logger.atWarning().log(
authenticationFailedMsg(authInfo.username, req)
+ ": account inactive or not provisioned in Gerrit");
rsp.sendError(SC_UNAUTHORIZED);
return false;
}
Account account = who.get().account();
AuthRequest authRequest = AuthRequest.forExternalUser(authInfo.username);
authRequest.setEmailAddress(account.preferredEmail());
authRequest.setDisplayName(account.fullName());
authRequest.setPassword(authInfo.tokenOrSecret);
authRequest.setAuthPlugin(authInfo.pluginName);
authRequest.setAuthProvider(authInfo.exportName);
try {
AuthResult authResult = accountManager.authenticate(authRequest);
WebSession ws = session.get();
ws.setUserAccountId(authResult.getAccountId());
ws.setAccessPathOk(AccessPath.GIT, true);
ws.setAccessPathOk(AccessPath.REST_API, true);
return true;
} catch (AccountException e) {
logger.atWarning().withCause(e).log(authenticationFailedMsg(authInfo.username, req));
rsp.sendError(SC_UNAUTHORIZED);
return false;
}
}
/**
* Picks the only installed OAuth provider. If there is a multiude of providers available, the
* actual provider must be determined from the authentication request.
*
* @throws ServletException if there is no {@code OAuthLoginProvider} installed at all.
*/
private void pickOnlyProvider() throws ServletException {
try {
Extension<OAuthLoginProvider> loginProvider = Iterables.getOnlyElement(loginProviders);
defaultAuthPlugin = loginProvider.getPluginName();
defaultAuthProvider = loginProvider.getExportName();
} catch (NoSuchElementException e) {
throw new ServletException("No OAuth login provider installed");
} catch (IllegalArgumentException e) {
// multiple providers found => do not pick any
}
}
/**
* Picks the {@code OAuthLoginProvider} configured with <tt>auth.gitOAuthProvider</tt>.
*
* @throws ServletException if the configured provider was not found.
*/
private void pickConfiguredProvider() throws ServletException {
int splitPos = gitOAuthProvider.lastIndexOf(':');
if (splitPos < 1 || splitPos == gitOAuthProvider.length() - 1) {
// no colon at all or leading/trailing colon: malformed providerId
throw new ServletException(
"OAuth login provider configuration is"
+ " invalid: Must be of the form pluginName:providerName");
}
defaultAuthPlugin = gitOAuthProvider.substring(0, splitPos);
defaultAuthProvider = gitOAuthProvider.substring(splitPos + 1);
OAuthLoginProvider provider = loginProviders.get(defaultAuthPlugin, defaultAuthProvider);
if (provider == null) {
throw new ServletException(
"Configured OAuth login provider " + gitOAuthProvider + " wasn't installed");
}
}
private AuthInfo extractAuthInfo(String hdr, String encoding)
throws UnsupportedEncodingException {
byte[] decoded = Base64.decodeBase64(hdr.substring(BASIC.length()));
String usernamePassword = new String(decoded, encoding);
int splitPos = usernamePassword.indexOf(':');
if (splitPos < 1 || splitPos == usernamePassword.length() - 1) {
return null;
}
return new AuthInfo(
usernamePassword.substring(0, splitPos),
usernamePassword.substring(splitPos + 1),
defaultAuthPlugin,
defaultAuthProvider);
}
private AuthInfo extractAuthInfo(Cookie cookie) throws UnsupportedEncodingException {
String username =
URLDecoder.decode(cookie.getName().substring(GIT_COOKIE_PREFIX.length()), UTF_8.name());
String value = cookie.getValue();
int splitPos = value.lastIndexOf('@');
if (splitPos < 1 || splitPos == value.length() - 1) {
// no providerId in the cookie value => assume default provider
// note: a leading/trailing at sign is considered to belong to
// the access token rather than being a separator
return new AuthInfo(username, cookie.getValue(), defaultAuthPlugin, defaultAuthProvider);
}
String token = value.substring(0, splitPos);
String providerId = value.substring(splitPos + 1);
splitPos = providerId.lastIndexOf(':');
if (splitPos < 1 || splitPos == providerId.length() - 1) {
// no colon at all or leading/trailing colon: malformed providerId
return null;
}
String pluginName = providerId.substring(0, splitPos);
String exportName = providerId.substring(splitPos + 1);
OAuthLoginProvider provider = loginProviders.get(pluginName, exportName);
if (provider == null) {
return null;
}
return new AuthInfo(username, token, pluginName, exportName);
}
private static String encoding(HttpServletRequest req) {
return MoreObjects.firstNonNull(req.getCharacterEncoding(), UTF_8.name());
}
private static Cookie findGitCookie(HttpServletRequest req) {
Cookie[] cookies = req.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if (cookie.getName().startsWith(GIT_COOKIE_PREFIX)) {
return cookie;
}
}
}
return null;
}
private class AuthInfo {
private final String username;
private final String tokenOrSecret;
private final String pluginName;
private final String exportName;
private AuthInfo(String username, String tokenOrSecret, String pluginName, String exportName) {
this.username = userNameToLowerCase ? username.toLowerCase(Locale.US) : username;
this.tokenOrSecret = tokenOrSecret;
this.pluginName = pluginName;
this.exportName = exportName;
}
}
private static class Response extends HttpServletResponseWrapper {
private static final String WWW_AUTHENTICATE = "WWW-Authenticate";
Response(HttpServletResponse rsp) {
super(rsp);
}
private void status(int sc) {
if (sc == SC_UNAUTHORIZED) {
StringBuilder v = new StringBuilder();
v.append(BASIC);
v.append("realm=\"").append(REALM_NAME).append("\"");
setHeader(WWW_AUTHENTICATE, v.toString());
} else if (containsHeader(WWW_AUTHENTICATE)) {
setHeader(WWW_AUTHENTICATE, null);
}
}
@Override
public void sendError(int sc, String msg) throws IOException {
status(sc);
super.sendError(sc, msg);
}
@Override
public void sendError(int sc) throws IOException {
status(sc);
super.sendError(sc);
}
@Override
@Deprecated
public void setStatus(int sc, String sm) {
status(sc);
super.setStatus(sc, sm);
}
@Override
public void setStatus(int sc) {
status(sc);
super.setStatus(sc);
}
}
}