LCOV - code coverage report
Current view: top level - httpd/auth/oauth - OAuthSession.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 0 111 0.0 %
Date: 2022-11-19 15:00:39 Functions: 0 17 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.auth.oauth;
      16             : 
      17             : import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
      18             : 
      19             : import com.google.common.base.CharMatcher;
      20             : import com.google.common.base.Strings;
      21             : import com.google.common.flogger.FluentLogger;
      22             : import com.google.common.io.BaseEncoding;
      23             : import com.google.gerrit.auth.oauth.OAuthTokenCache;
      24             : import com.google.gerrit.entities.Account;
      25             : import com.google.gerrit.extensions.auth.oauth.OAuthServiceProvider;
      26             : import com.google.gerrit.extensions.auth.oauth.OAuthToken;
      27             : import com.google.gerrit.extensions.auth.oauth.OAuthUserInfo;
      28             : import com.google.gerrit.extensions.auth.oauth.OAuthVerifier;
      29             : import com.google.gerrit.extensions.registration.DynamicItem;
      30             : import com.google.gerrit.extensions.restapi.Url;
      31             : import com.google.gerrit.httpd.CanonicalWebUrl;
      32             : import com.google.gerrit.httpd.WebSession;
      33             : import com.google.gerrit.server.IdentifiedUser;
      34             : import com.google.gerrit.server.account.AccountException;
      35             : import com.google.gerrit.server.account.AccountManager;
      36             : import com.google.gerrit.server.account.AuthRequest;
      37             : import com.google.gerrit.server.account.AuthResult;
      38             : import com.google.gerrit.server.account.externalids.ExternalIdKeyFactory;
      39             : import com.google.inject.Inject;
      40             : import com.google.inject.Provider;
      41             : import com.google.inject.servlet.SessionScoped;
      42             : import java.io.IOException;
      43             : import java.security.NoSuchAlgorithmException;
      44             : import java.security.SecureRandom;
      45             : import java.util.Optional;
      46             : import javax.servlet.ServletRequest;
      47             : import javax.servlet.http.HttpServletRequest;
      48             : import javax.servlet.http.HttpServletResponse;
      49             : import org.eclipse.jgit.errors.ConfigInvalidException;
      50             : 
      51             : @SessionScoped
      52             : /* OAuth protocol implementation */
      53             : class OAuthSession {
      54           0 :   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
      55             : 
      56           0 :   private static final SecureRandom randomState = newRandomGenerator();
      57             :   private final String state;
      58             :   private final DynamicItem<WebSession> webSession;
      59             :   private final Provider<IdentifiedUser> identifiedUser;
      60             :   private final AccountManager accountManager;
      61             :   private final CanonicalWebUrl urlProvider;
      62             :   private final OAuthTokenCache tokenCache;
      63             :   private OAuthServiceProvider serviceProvider;
      64             :   private OAuthUserInfo user;
      65             :   private Account.Id accountId;
      66             :   private String redirectToken;
      67             :   private boolean linkMode;
      68             :   private final ExternalIdKeyFactory externalIdKeyFactory;
      69             :   private final AuthRequest.Factory authRequestFactory;
      70             : 
      71             :   @Inject
      72             :   OAuthSession(
      73             :       DynamicItem<WebSession> webSession,
      74             :       Provider<IdentifiedUser> identifiedUser,
      75             :       AccountManager accountManager,
      76             :       CanonicalWebUrl urlProvider,
      77             :       OAuthTokenCache tokenCache,
      78             :       ExternalIdKeyFactory externalIdKeyFactory,
      79           0 :       AuthRequest.Factory authRequestFactory) {
      80           0 :     this.state = generateRandomState();
      81           0 :     this.identifiedUser = identifiedUser;
      82           0 :     this.webSession = webSession;
      83           0 :     this.accountManager = accountManager;
      84           0 :     this.urlProvider = urlProvider;
      85           0 :     this.tokenCache = tokenCache;
      86           0 :     this.externalIdKeyFactory = externalIdKeyFactory;
      87           0 :     this.authRequestFactory = authRequestFactory;
      88           0 :   }
      89             : 
      90             :   boolean isLoggedIn() {
      91           0 :     return user != null;
      92             :   }
      93             : 
      94             :   boolean isOAuthFinal(HttpServletRequest request) {
      95           0 :     return Strings.emptyToNull(request.getParameter("code")) != null;
      96             :   }
      97             : 
      98             :   boolean login(
      99             :       HttpServletRequest request, HttpServletResponse response, OAuthServiceProvider oauth)
     100             :       throws IOException {
     101           0 :     logger.atFine().log("Login %s", this);
     102             : 
     103           0 :     if (isOAuthFinal(request)) {
     104           0 :       if (!checkState(request)) {
     105           0 :         response.sendError(HttpServletResponse.SC_NOT_FOUND);
     106           0 :         return false;
     107             :       }
     108             : 
     109           0 :       logger.atFine().log("Login-Retrieve-User %s", this);
     110           0 :       OAuthToken token = oauth.getAccessToken(new OAuthVerifier(request.getParameter("code")));
     111           0 :       user = oauth.getUserInfo(token);
     112             : 
     113           0 :       if (isLoggedIn()) {
     114           0 :         logger.atFine().log("Login-SUCCESS %s", this);
     115           0 :         authenticateAndRedirect(request, response, token);
     116           0 :         return true;
     117             :       }
     118           0 :       response.sendError(SC_UNAUTHORIZED);
     119           0 :       return false;
     120             :     }
     121           0 :     logger.atFine().log("Login-PHASE1 %s", this);
     122           0 :     redirectToken = request.getRequestURI();
     123             :     // We are here in content of filter.
     124             :     // Due to this Jetty limitation:
     125             :     // https://bz.apache.org/bugzilla/show_bug.cgi?id=28323
     126             :     // we cannot use LoginUrlToken.getToken() method,
     127             :     // because it relies on getPathInfo() and it is always null here.
     128           0 :     redirectToken = redirectToken.substring(request.getContextPath().length());
     129           0 :     response.sendRedirect(oauth.getAuthorizationUrl() + "&state=" + state);
     130           0 :     return false;
     131             :   }
     132             : 
     133             :   private void authenticateAndRedirect(
     134             :       HttpServletRequest req, HttpServletResponse rsp, OAuthToken token) throws IOException {
     135           0 :     AuthRequest areq = authRequestFactory.create(externalIdKeyFactory.parse(user.getExternalId()));
     136             :     AuthResult arsp;
     137             :     try {
     138           0 :       String claimedIdentifier = user.getClaimedIdentity();
     139           0 :       if (!Strings.isNullOrEmpty(claimedIdentifier)) {
     140           0 :         if (!authenticateWithIdentityClaimedDuringHandshake(areq, rsp, claimedIdentifier)) {
     141           0 :           return;
     142             :         }
     143           0 :       } else if (linkMode) {
     144           0 :         if (!authenticateWithLinkedIdentity(areq, rsp)) {
     145           0 :           return;
     146             :         }
     147             :       }
     148           0 :       areq.setUserName(user.getUserName());
     149           0 :       areq.setEmailAddress(user.getEmailAddress());
     150           0 :       areq.setDisplayName(user.getDisplayName());
     151           0 :       arsp = accountManager.authenticate(areq);
     152             : 
     153           0 :       accountId = arsp.getAccountId();
     154           0 :       tokenCache.put(accountId, token);
     155           0 :     } catch (AccountException e) {
     156           0 :       logger.atSevere().withCause(e).log("Unable to authenticate user \"%s\"", user);
     157           0 :       rsp.sendError(HttpServletResponse.SC_FORBIDDEN);
     158           0 :       return;
     159           0 :     }
     160             : 
     161           0 :     webSession.get().login(arsp, true);
     162           0 :     String suffix = redirectToken.substring(OAuthWebFilter.GERRIT_LOGIN.length() + 1);
     163           0 :     suffix = CharMatcher.anyOf("/").trimLeadingFrom(Url.decode(suffix));
     164           0 :     StringBuilder rdr = new StringBuilder(urlProvider.get(req));
     165           0 :     rdr.append(suffix);
     166           0 :     rsp.sendRedirect(rdr.toString());
     167           0 :   }
     168             : 
     169             :   private boolean authenticateWithIdentityClaimedDuringHandshake(
     170             :       AuthRequest req, HttpServletResponse rsp, String claimedIdentifier)
     171             :       throws AccountException, IOException {
     172           0 :     Optional<Account.Id> claimedId = accountManager.lookup(claimedIdentifier);
     173           0 :     Optional<Account.Id> actualId = accountManager.lookup(user.getExternalId());
     174           0 :     if (claimedId.isPresent() && actualId.isPresent()) {
     175           0 :       if (claimedId.get().equals(actualId.get())) {
     176             :         // Both link to the same account, that's what we expected.
     177           0 :         logger.atFine().log("OAuth2: claimed identity equals current id");
     178             :       } else {
     179             :         // This is (for now) a fatal error. There are two records
     180             :         // for what might be the same user.
     181             :         //
     182           0 :         logger.atSevere().log(
     183             :             "OAuth accounts disagree over user identity:\n"
     184             :                 + "  Claimed ID: %s is %s\n"
     185             :                 + "  Delgate ID: %s is %s",
     186           0 :             claimedId.get(), claimedIdentifier, actualId.get(), user.getExternalId());
     187           0 :         rsp.sendError(HttpServletResponse.SC_FORBIDDEN);
     188           0 :         return false;
     189             :       }
     190           0 :     } else if (claimedId.isPresent() && !actualId.isPresent()) {
     191             :       // Claimed account already exists: link to it.
     192             :       //
     193           0 :       logger.atInfo().log("OAuth2: linking claimed identity to %s", claimedId.get());
     194             :       try {
     195           0 :         accountManager.link(claimedId.get(), req);
     196           0 :       } catch (ConfigInvalidException e) {
     197           0 :         logger.atSevere().log(
     198             :             "Cannot link: %s to user identity:\n  Claimed ID: %s is %s",
     199           0 :             user.getExternalId(), claimedId.get(), claimedIdentifier);
     200           0 :         rsp.sendError(HttpServletResponse.SC_FORBIDDEN);
     201           0 :         return false;
     202           0 :       }
     203             :     }
     204           0 :     return true;
     205             :   }
     206             : 
     207             :   private boolean authenticateWithLinkedIdentity(AuthRequest areq, HttpServletResponse rsp)
     208             :       throws AccountException, IOException {
     209             :     try {
     210           0 :       accountManager.link(identifiedUser.get().getAccountId(), areq);
     211           0 :     } catch (ConfigInvalidException e) {
     212           0 :       logger.atSevere().log(
     213             :           "Cannot link: %s to user identity: %s",
     214           0 :           user.getExternalId(), identifiedUser.get().getAccountId());
     215           0 :       rsp.sendError(HttpServletResponse.SC_FORBIDDEN);
     216           0 :       return false;
     217             :     } finally {
     218           0 :       linkMode = false;
     219             :     }
     220           0 :     return true;
     221             :   }
     222             : 
     223             :   void logout() {
     224           0 :     if (accountId != null) {
     225           0 :       tokenCache.remove(accountId);
     226           0 :       accountId = null;
     227             :     }
     228           0 :     user = null;
     229           0 :     redirectToken = null;
     230           0 :     serviceProvider = null;
     231           0 :   }
     232             : 
     233             :   private boolean checkState(ServletRequest request) {
     234           0 :     String s = Strings.nullToEmpty(request.getParameter("state"));
     235           0 :     if (!s.equals(state)) {
     236           0 :       logger.atSevere().log("Illegal request state '%s' on OAuthProtocol %s", s, this);
     237           0 :       return false;
     238             :     }
     239           0 :     return true;
     240             :   }
     241             : 
     242             :   private static SecureRandom newRandomGenerator() {
     243             :     try {
     244           0 :       return SecureRandom.getInstance("SHA1PRNG");
     245           0 :     } catch (NoSuchAlgorithmException e) {
     246           0 :       throw new IllegalStateException("No SecureRandom available for GitHub authentication", e);
     247             :     }
     248             :   }
     249             : 
     250             :   private static String generateRandomState() {
     251           0 :     byte[] state = new byte[32];
     252           0 :     randomState.nextBytes(state);
     253           0 :     return BaseEncoding.base64Url().encode(state);
     254             :   }
     255             : 
     256             :   @Override
     257             :   public String toString() {
     258           0 :     return "OAuthSession [token=" + tokenCache.get(accountId) + ", user=" + user + "]";
     259             :   }
     260             : 
     261             :   public void setServiceProvider(OAuthServiceProvider provider) {
     262           0 :     this.serviceProvider = provider;
     263           0 :   }
     264             : 
     265             :   public OAuthServiceProvider getServiceProvider() {
     266           0 :     return serviceProvider;
     267             :   }
     268             : 
     269             :   public void setLinkMode(boolean linkMode) {
     270           0 :     this.linkMode = linkMode;
     271           0 :   }
     272             : 
     273             :   public boolean isLinkMode() {
     274           0 :     return linkMode;
     275             :   }
     276             : }

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