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

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