LCOV - code coverage report
Current view: top level - httpd - ProjectOAuthFilter.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 0 137 0.0 %
Date: 2022-11-19 15:00:39 Functions: 0 20 0.0 %

          Line data    Source code
       1             : // Copyright (C) 2015 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.google.gerrit.httpd;
      16             : 
      17             : import static com.google.gerrit.httpd.ProjectBasicAuthFilter.authenticationFailedMsg;
      18             : import static java.nio.charset.StandardCharsets.UTF_8;
      19             : import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
      20             : 
      21             : import com.google.common.base.MoreObjects;
      22             : import com.google.common.base.Strings;
      23             : import com.google.common.collect.Iterables;
      24             : import com.google.common.flogger.FluentLogger;
      25             : import com.google.common.io.BaseEncoding;
      26             : import com.google.gerrit.common.Nullable;
      27             : import com.google.gerrit.entities.Account;
      28             : import com.google.gerrit.extensions.auth.oauth.OAuthLoginProvider;
      29             : import com.google.gerrit.extensions.registration.DynamicItem;
      30             : import com.google.gerrit.extensions.registration.DynamicMap;
      31             : import com.google.gerrit.extensions.registration.Extension;
      32             : import com.google.gerrit.server.AccessPath;
      33             : import com.google.gerrit.server.account.AccountCache;
      34             : import com.google.gerrit.server.account.AccountException;
      35             : import com.google.gerrit.server.account.AccountManager;
      36             : import com.google.gerrit.server.account.AccountState;
      37             : import com.google.gerrit.server.account.AuthRequest;
      38             : import com.google.gerrit.server.account.AuthResult;
      39             : import com.google.gerrit.server.config.GerritServerConfig;
      40             : import com.google.inject.Inject;
      41             : import com.google.inject.Singleton;
      42             : import java.io.IOException;
      43             : import java.io.UnsupportedEncodingException;
      44             : import java.net.URLDecoder;
      45             : import java.util.Locale;
      46             : import java.util.NoSuchElementException;
      47             : import java.util.Optional;
      48             : import javax.servlet.Filter;
      49             : import javax.servlet.FilterChain;
      50             : import javax.servlet.FilterConfig;
      51             : import javax.servlet.ServletException;
      52             : import javax.servlet.ServletRequest;
      53             : import javax.servlet.ServletResponse;
      54             : import javax.servlet.http.Cookie;
      55             : import javax.servlet.http.HttpServletRequest;
      56             : import javax.servlet.http.HttpServletResponse;
      57             : import javax.servlet.http.HttpServletResponseWrapper;
      58             : import org.eclipse.jgit.lib.Config;
      59             : 
      60             : /**
      61             :  * Authenticates the current user with an OAuth2 server.
      62             :  *
      63             :  * @see <a href="https://tools.ietf.org/rfc/rfc6750.txt">RFC 6750</a>
      64             :  */
      65             : @Singleton
      66             : class ProjectOAuthFilter implements Filter {
      67           0 :   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
      68             : 
      69             :   private static final String REALM_NAME = "Gerrit Code Review";
      70             :   private static final String AUTHORIZATION = "Authorization";
      71             :   private static final String BASIC = "Basic ";
      72             :   private static final String GIT_COOKIE_PREFIX = "git-";
      73             : 
      74             :   private final DynamicItem<WebSession> session;
      75             :   private final DynamicMap<OAuthLoginProvider> loginProviders;
      76             :   private final AccountCache accountCache;
      77             :   private final AccountManager accountManager;
      78             :   private final String gitOAuthProvider;
      79             :   private final boolean userNameToLowerCase;
      80             :   private final AuthRequest.Factory authRequestFactory;
      81             : 
      82             :   private String defaultAuthPlugin;
      83             :   private String defaultAuthProvider;
      84             : 
      85             :   @Inject
      86             :   ProjectOAuthFilter(
      87             :       DynamicItem<WebSession> session,
      88             :       DynamicMap<OAuthLoginProvider> pluginsProvider,
      89             :       AccountCache accountCache,
      90             :       AccountManager accountManager,
      91             :       @GerritServerConfig Config gerritConfig,
      92           0 :       AuthRequest.Factory authRequestFactory) {
      93           0 :     this.session = session;
      94           0 :     this.loginProviders = pluginsProvider;
      95           0 :     this.accountCache = accountCache;
      96           0 :     this.accountManager = accountManager;
      97           0 :     this.gitOAuthProvider = gerritConfig.getString("auth", null, "gitOAuthProvider");
      98           0 :     this.userNameToLowerCase = gerritConfig.getBoolean("auth", null, "userNameToLowerCase", false);
      99           0 :     this.authRequestFactory = authRequestFactory;
     100           0 :   }
     101             : 
     102             :   @Override
     103             :   public void init(FilterConfig config) throws ServletException {
     104           0 :     if (Strings.isNullOrEmpty(gitOAuthProvider)) {
     105           0 :       pickOnlyProvider();
     106             :     } else {
     107           0 :       pickConfiguredProvider();
     108             :     }
     109           0 :   }
     110             : 
     111             :   @Override
     112           0 :   public void destroy() {}
     113             : 
     114             :   @Override
     115             :   public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
     116             :       throws IOException, ServletException {
     117           0 :     HttpServletRequest req = (HttpServletRequest) request;
     118           0 :     Response rsp = new Response((HttpServletResponse) response);
     119           0 :     if (verify(req, rsp)) {
     120           0 :       chain.doFilter(req, rsp);
     121             :     }
     122           0 :   }
     123             : 
     124             :   private boolean verify(HttpServletRequest req, Response rsp) throws IOException {
     125             :     AuthInfo authInfo;
     126             : 
     127             :     // first check if there is a BASIC authentication header
     128           0 :     String hdr = req.getHeader(AUTHORIZATION);
     129           0 :     if (hdr != null && hdr.startsWith(BASIC)) {
     130           0 :       authInfo = extractAuthInfo(hdr, encoding(req));
     131           0 :       if (authInfo == null) {
     132           0 :         rsp.sendError(SC_UNAUTHORIZED);
     133           0 :         return false;
     134             :       }
     135             :     } else {
     136             :       // if there is no BASIC authentication header, check if there is
     137             :       // a cookie starting with the prefix "git-"
     138           0 :       Cookie cookie = findGitCookie(req);
     139           0 :       if (cookie != null) {
     140           0 :         authInfo = extractAuthInfo(cookie);
     141           0 :         if (authInfo == null) {
     142           0 :           rsp.sendError(SC_UNAUTHORIZED);
     143           0 :           return false;
     144             :         }
     145             :       } else {
     146             :         // if there is no authentication information at all, it might be
     147             :         // an anonymous connection, or there might be a session cookie
     148           0 :         return true;
     149             :       }
     150             :     }
     151             : 
     152             :     // if there is authentication information but no secret => 401
     153           0 :     if (Strings.isNullOrEmpty(authInfo.tokenOrSecret)) {
     154           0 :       rsp.sendError(SC_UNAUTHORIZED);
     155           0 :       return false;
     156             :     }
     157             : 
     158           0 :     Optional<AccountState> who =
     159           0 :         accountCache.getByUsername(authInfo.username).filter(a -> a.account().isActive());
     160           0 :     if (!who.isPresent()) {
     161           0 :       logger.atWarning().log(
     162             :           "%s: account inactive or not provisioned in Gerrit",
     163           0 :           authenticationFailedMsg(authInfo.username, req));
     164           0 :       rsp.sendError(SC_UNAUTHORIZED);
     165           0 :       return false;
     166             :     }
     167             : 
     168           0 :     Account account = who.get().account();
     169           0 :     AuthRequest authRequest = authRequestFactory.createForExternalUser(authInfo.username);
     170           0 :     authRequest.setEmailAddress(account.preferredEmail());
     171           0 :     authRequest.setDisplayName(account.fullName());
     172           0 :     authRequest.setPassword(authInfo.tokenOrSecret);
     173           0 :     authRequest.setAuthPlugin(authInfo.pluginName);
     174           0 :     authRequest.setAuthProvider(authInfo.exportName);
     175             : 
     176             :     try {
     177           0 :       AuthResult authResult = accountManager.authenticate(authRequest);
     178           0 :       WebSession ws = session.get();
     179           0 :       ws.setUserAccountId(authResult.getAccountId());
     180           0 :       ws.setAccessPathOk(AccessPath.GIT, true);
     181           0 :       ws.setAccessPathOk(AccessPath.REST_API, true);
     182           0 :       return true;
     183           0 :     } catch (AccountException e) {
     184           0 :       logger.atWarning().withCause(e).log("%s", authenticationFailedMsg(authInfo.username, req));
     185           0 :       rsp.sendError(SC_UNAUTHORIZED);
     186           0 :       return false;
     187             :     }
     188             :   }
     189             : 
     190             :   /**
     191             :    * Picks the only installed OAuth provider. If there is a multiude of providers available, the
     192             :    * actual provider must be determined from the authentication request.
     193             :    *
     194             :    * @throws ServletException if there is no {@code OAuthLoginProvider} installed at all.
     195             :    */
     196             :   private void pickOnlyProvider() throws ServletException {
     197             :     try {
     198           0 :       Extension<OAuthLoginProvider> loginProvider = Iterables.getOnlyElement(loginProviders);
     199           0 :       defaultAuthPlugin = loginProvider.getPluginName();
     200           0 :       defaultAuthProvider = loginProvider.getExportName();
     201           0 :     } catch (NoSuchElementException e) {
     202           0 :       throw new ServletException("No OAuth login provider installed", e);
     203           0 :     } catch (IllegalArgumentException e) {
     204             :       // multiple providers found => do not pick any
     205           0 :     }
     206           0 :   }
     207             : 
     208             :   /**
     209             :    * Picks the {@code OAuthLoginProvider} configured with <tt>auth.gitOAuthProvider</tt>.
     210             :    *
     211             :    * @throws ServletException if the configured provider was not found.
     212             :    */
     213             :   private void pickConfiguredProvider() throws ServletException {
     214           0 :     int splitPos = gitOAuthProvider.lastIndexOf(':');
     215           0 :     if (splitPos < 1 || splitPos == gitOAuthProvider.length() - 1) {
     216             :       // no colon at all or leading/trailing colon: malformed providerId
     217           0 :       throw new ServletException(
     218             :           "OAuth login provider configuration is"
     219             :               + " invalid: Must be of the form pluginName:providerName");
     220             :     }
     221           0 :     defaultAuthPlugin = gitOAuthProvider.substring(0, splitPos);
     222           0 :     defaultAuthProvider = gitOAuthProvider.substring(splitPos + 1);
     223           0 :     OAuthLoginProvider provider = loginProviders.get(defaultAuthPlugin, defaultAuthProvider);
     224           0 :     if (provider == null) {
     225           0 :       throw new ServletException(
     226             :           "Configured OAuth login provider " + gitOAuthProvider + " wasn't installed");
     227             :     }
     228           0 :   }
     229             : 
     230             :   @Nullable
     231             :   private AuthInfo extractAuthInfo(String hdr, String encoding)
     232             :       throws UnsupportedEncodingException {
     233           0 :     byte[] decoded = BaseEncoding.base64().decode(hdr.substring(BASIC.length()));
     234           0 :     String usernamePassword = new String(decoded, encoding);
     235           0 :     int splitPos = usernamePassword.indexOf(':');
     236           0 :     if (splitPos < 1 || splitPos == usernamePassword.length() - 1) {
     237           0 :       return null;
     238             :     }
     239           0 :     return new AuthInfo(
     240           0 :         usernamePassword.substring(0, splitPos),
     241           0 :         usernamePassword.substring(splitPos + 1),
     242             :         defaultAuthPlugin,
     243             :         defaultAuthProvider);
     244             :   }
     245             : 
     246             :   @Nullable
     247             :   private AuthInfo extractAuthInfo(Cookie cookie) throws UnsupportedEncodingException {
     248           0 :     String username =
     249           0 :         URLDecoder.decode(cookie.getName().substring(GIT_COOKIE_PREFIX.length()), UTF_8.name());
     250           0 :     String value = cookie.getValue();
     251           0 :     int splitPos = value.lastIndexOf('@');
     252           0 :     if (splitPos < 1 || splitPos == value.length() - 1) {
     253             :       // no providerId in the cookie value => assume default provider
     254             :       // note: a leading/trailing at sign is considered to belong to
     255             :       // the access token rather than being a separator
     256           0 :       return new AuthInfo(username, cookie.getValue(), defaultAuthPlugin, defaultAuthProvider);
     257             :     }
     258           0 :     String token = value.substring(0, splitPos);
     259           0 :     String providerId = value.substring(splitPos + 1);
     260           0 :     splitPos = providerId.lastIndexOf(':');
     261           0 :     if (splitPos < 1 || splitPos == providerId.length() - 1) {
     262             :       // no colon at all or leading/trailing colon: malformed providerId
     263           0 :       return null;
     264             :     }
     265           0 :     String pluginName = providerId.substring(0, splitPos);
     266           0 :     String exportName = providerId.substring(splitPos + 1);
     267           0 :     OAuthLoginProvider provider = loginProviders.get(pluginName, exportName);
     268           0 :     if (provider == null) {
     269           0 :       return null;
     270             :     }
     271           0 :     return new AuthInfo(username, token, pluginName, exportName);
     272             :   }
     273             : 
     274             :   private static String encoding(HttpServletRequest req) {
     275           0 :     return MoreObjects.firstNonNull(req.getCharacterEncoding(), UTF_8.name());
     276             :   }
     277             : 
     278             :   @Nullable
     279             :   private static Cookie findGitCookie(HttpServletRequest req) {
     280           0 :     Cookie[] cookies = req.getCookies();
     281           0 :     if (cookies != null) {
     282           0 :       for (Cookie cookie : cookies) {
     283           0 :         if (cookie.getName().startsWith(GIT_COOKIE_PREFIX)) {
     284           0 :           return cookie;
     285             :         }
     286             :       }
     287             :     }
     288           0 :     return null;
     289             :   }
     290             : 
     291             :   private class AuthInfo {
     292             :     private final String username;
     293             :     private final String tokenOrSecret;
     294             :     private final String pluginName;
     295             :     private final String exportName;
     296             : 
     297           0 :     private AuthInfo(String username, String tokenOrSecret, String pluginName, String exportName) {
     298           0 :       this.username = userNameToLowerCase ? username.toLowerCase(Locale.US) : username;
     299           0 :       this.tokenOrSecret = tokenOrSecret;
     300           0 :       this.pluginName = pluginName;
     301           0 :       this.exportName = exportName;
     302           0 :     }
     303             :   }
     304             : 
     305             :   private static class Response extends HttpServletResponseWrapper {
     306             :     private static final String WWW_AUTHENTICATE = "WWW-Authenticate";
     307             : 
     308             :     Response(HttpServletResponse rsp) {
     309           0 :       super(rsp);
     310           0 :     }
     311             : 
     312             :     private void status(int sc) {
     313           0 :       if (sc == SC_UNAUTHORIZED) {
     314           0 :         StringBuilder v = new StringBuilder();
     315           0 :         v.append(BASIC);
     316           0 :         v.append("realm=\"").append(REALM_NAME).append("\"");
     317           0 :         setHeader(WWW_AUTHENTICATE, v.toString());
     318           0 :       } else if (containsHeader(WWW_AUTHENTICATE)) {
     319           0 :         setHeader(WWW_AUTHENTICATE, null);
     320             :       }
     321           0 :     }
     322             : 
     323             :     @Override
     324             :     public void sendError(int sc, String msg) throws IOException {
     325           0 :       status(sc);
     326           0 :       super.sendError(sc, msg);
     327           0 :     }
     328             : 
     329             :     @Override
     330             :     public void sendError(int sc) throws IOException {
     331           0 :       status(sc);
     332           0 :       super.sendError(sc);
     333           0 :     }
     334             : 
     335             :     @Override
     336             :     @Deprecated
     337             :     public void setStatus(int sc, String sm) {
     338           0 :       status(sc);
     339           0 :       super.setStatus(sc, sm);
     340           0 :     }
     341             : 
     342             :     @Override
     343             :     public void setStatus(int sc) {
     344           0 :       status(sc);
     345           0 :       super.setStatus(sc);
     346           0 :     }
     347             :   }
     348             : }

Generated by: LCOV version 1.16+git.20220603.dfeb750