chromium/chrome/credential_provider/gaiacp/associated_user_validator.cc

// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/credential_provider/gaiacp/associated_user_validator.h"

#include <ntstatus.h>
#include <process.h>

#include <string>
#include <string_view>

#include "base/json/json_reader.h"
#include "base/logging.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/values.h"
#include "base/win/win_util.h"
#include "chrome/credential_provider/common/gcp_strings.h"
#include "chrome/credential_provider/gaiacp/gaia_credential_provider.h"
#include "chrome/credential_provider/gaiacp/gcp_utils.h"
#include "chrome/credential_provider/gaiacp/gcpw_strings.h"
#include "chrome/credential_provider/gaiacp/internet_availability_checker.h"
#include "chrome/credential_provider/gaiacp/logging.h"
#include "chrome/credential_provider/gaiacp/mdm_utils.h"
#include "chrome/credential_provider/gaiacp/os_user_manager.h"
#include "chrome/credential_provider/gaiacp/reg_utils.h"
#include "chrome/credential_provider/gaiacp/user_policies_manager.h"
#include "chrome/credential_provider/gaiacp/win_http_url_fetcher.h"

namespace credential_provider {

const base::TimeDelta
    AssociatedUserValidator::kDefaultTokenHandleValidationTimeout =
        base::Milliseconds(3000);

const base::TimeDelta AssociatedUserValidator::kTokenHandleValidityLifetime =
    base::Seconds(60);

const char AssociatedUserValidator::kTokenInfoUrl[] =
    "https://www.googleapis.com/oauth2/v2/tokeninfo";

constexpr long kDayInMillis = 86400000;

namespace {

struct CheckReauthParams {
  std::wstring sid;
  std::wstring token_handle;
  std::unique_ptr<WinHttpUrlFetcher> fetcher;
};

// Queries google to see whether the user's token handle is no longer valid or
// is expired. Returns 1 if it is still valid, 0 if the user needs to reauth.
unsigned __stdcall CheckReauthStatus(void* param) {
  DCHECK(param);
  std::unique_ptr<CheckReauthParams> reauth_info(
      reinterpret_cast<CheckReauthParams*>(param));

  if (reauth_info->fetcher) {
    reauth_info->fetcher->SetRequestHeader("Content-Type",
                                           "application/x-www-form-urlencoded");

    std::string body = base::StringPrintf("token_handle=%ls",
                                          reauth_info->token_handle.c_str());
    HRESULT hr = reauth_info->fetcher->SetRequestBody(body.c_str());
    if (FAILED(hr)) {
      LOGFN(ERROR) << "fetcher.SetRequestBody sid=" << reauth_info->sid
                   << " hr=" << putHR(hr);
      return 1;
    }

    std::vector<char> response;
    hr = reauth_info->fetcher->Fetch(&response);
    if (FAILED(hr)) {
      LOGFN(ERROR) << "fetcher.Fetch sid=" << reauth_info->sid
                   << " hr=" << putHR(hr);
      return 1;
    }

    std::string_view response_string(response.data(), response.size());
    std::optional<base::Value> properties_val = base::JSONReader::Read(
        response_string, base::JSON_ALLOW_TRAILING_COMMAS);
    if (!properties_val || !properties_val->is_dict()) {
      LOGFN(ERROR) << "base::JSONReader::Read failed forcing reauth";
      return 0;
    }

    const auto& properties = properties_val->GetDict();
    std::optional<int> expires_in = properties.FindInt("expires_in");
    if (properties.contains("error") || !expires_in || expires_in.value() < 0) {
      LOGFN(VERBOSE) << "Needs reauth sid=" << reauth_info->sid;
      return 0;
    }
  }

  return 1;
}

bool TokenHandleNeedsUpdate(const base::Time& last_refresh) {
  return (base::Time::Now() - last_refresh) >
         AssociatedUserValidator::kTokenHandleValidityLifetime;
}

bool WaitForQueryResult(const base::win::ScopedHandle& thread_handle,
                        const base::Time& until) {
  if (!thread_handle.IsValid())
    return true;

  DWORD time_left = std::max<DWORD>(
      static_cast<DWORD>((until - base::Time::Now()).InMilliseconds()), 0);

  // See if a response to the token info can be fetched in a reasonable
  // amount of time. If not, assume there is no internet and that the handle
  // is still valid.
  HRESULT hr = ::WaitForSingleObject(thread_handle.Get(), time_left);

  bool token_handle_validity = false;
  if (hr == WAIT_OBJECT_0) {
    DWORD exit_code;
    token_handle_validity =
        !::GetExitCodeThread(thread_handle.Get(), &exit_code) || exit_code == 1;
  } else if (hr == WAIT_TIMEOUT) {
    token_handle_validity = true;
  }

  return token_handle_validity;
}

HRESULT ModifyUserAccess(const std::unique_ptr<ScopedLsaPolicy>& policy,
                         const std::wstring& sid,
                         bool allow) {
  OSUserManager* manager = OSUserManager::Get();
  wchar_t username[kWindowsUsernameBufferLength];
  wchar_t domain[kWindowsDomainBufferLength];

  HRESULT hr = manager->FindUserBySID(
      sid.c_str(), username, std::size(username), domain, std::size(domain));

  if (FAILED(hr)) {
    LOGFN(ERROR) << "FindUserBySID sid=" << sid << " hr=" << putHR(hr);
    return hr;
  }

  PSID psid = nullptr;
  if (!::ConvertStringSidToSidW(sid.c_str(), &psid)) {
    hr = HRESULT_FROM_WIN32(::GetLastError());
    LOGFN(ERROR) << "ConvertStringSidToSidW sid=" << sid << " hr=" << putHR(hr);
    return hr;
  }

  std::vector<std::wstring> account_rights{
      SE_DENY_INTERACTIVE_LOGON_NAME, SE_DENY_NETWORK_LOGON_NAME,
      SE_DENY_REMOTE_INTERACTIVE_LOGON_NAME};
  HRESULT status;
  if (!allow) {
    status = policy->AddAccountRights(psid, account_rights);
  } else {
    // Note: We are still going to keep this time restrictions flow to avoid
    // any cornercase scenario where user is blocked on login UI because
    // time restrictions were set but were never added back.
    hr = manager->ModifyUserAccessWithLogonHours(domain, username, allow);
    if (FAILED(hr))
      LOGFN(ERROR) << "Failed to remove time restrictions for sid : " << sid;

    status = policy->RemoveAccountRights(psid, account_rights);
  }
  ::LocalFree(psid);
  return status;
}

}  // namespace

AssociatedUserValidator::TokenHandleInfo::TokenHandleInfo() = default;
AssociatedUserValidator::TokenHandleInfo::~TokenHandleInfo() = default;

AssociatedUserValidator::TokenHandleInfo::TokenHandleInfo(
    const std::wstring& token_handle)
    : queried_token_handle(token_handle), last_update(base::Time::Now()) {}

AssociatedUserValidator::TokenHandleInfo::TokenHandleInfo(
    const std::wstring& token_handle,
    base::Time update_time,
    base::win::ScopedHandle::Handle thread_handle)
    : queried_token_handle(token_handle),
      last_update(update_time),
      pending_query_thread(thread_handle) {}

AssociatedUserValidator::ScopedBlockDenyAccessUpdate::
    ScopedBlockDenyAccessUpdate(AssociatedUserValidator* validator)
    : validator_(validator) {
  DCHECK(validator_);
  validator_->BlockDenyAccessUpdate();
}

AssociatedUserValidator::ScopedBlockDenyAccessUpdate::
    ~ScopedBlockDenyAccessUpdate() {
  validator_->UnblockDenyAccessUpdate();
}

// static
AssociatedUserValidator* AssociatedUserValidator::Get() {
  return *GetInstanceStorage();
}

// static
AssociatedUserValidator** AssociatedUserValidator::GetInstanceStorage() {
  static AssociatedUserValidator instance(kDefaultTokenHandleValidationTimeout);
  static AssociatedUserValidator* instance_storage = &instance;

  return &instance_storage;
}

AssociatedUserValidator::AssociatedUserValidator(
    base::TimeDelta validation_timeout)
    : validation_timeout_(validation_timeout) {}

AssociatedUserValidator::~AssociatedUserValidator() = default;

bool AssociatedUserValidator::IsOnlineLoginStale(
    const std::wstring& sid) const {
  wchar_t last_token_valid_millis[512];
  ULONG last_token_valid_size = std::size(last_token_valid_millis);
  HRESULT hr = GetUserProperty(sid, base::UTF8ToWide(kKeyLastTokenValid),
                               last_token_valid_millis, &last_token_valid_size);

  if (FAILED(hr)) {
    LOGFN(VERBOSE) << "GetUserProperty for " << kKeyLastTokenValid
                   << " failed. hr=" << putHR(hr);
    // DEPRECATED FLOW. Keeping it for backward compatibility.
    hr = GetUserProperty(sid,
                         base::UTF8ToWide(kKeyLastSuccessfulOnlineLoginMillis),
                         last_token_valid_millis, &last_token_valid_size);

    if (FAILED(hr)) {
      LOGFN(VERBOSE) << "GetUserProperty for "
                     << kKeyLastSuccessfulOnlineLoginMillis
                     << " failed. hr=" << putHR(hr);
      // Fallback to the less obstructive option to not enforce
      // login via google when fetching the registry entry fails.
      return false;
    }
  }

  int64_t last_token_valid_millis_int64;
  base::StringToInt64(last_token_valid_millis, &last_token_valid_millis_int64);

  DWORD validity_period_days;
  if (UserPoliciesManager::Get()->CloudPoliciesEnabled()) {
    UserPolicies user_policies;
    UserPoliciesManager::Get()->GetUserPolicies(sid, &user_policies);
    validity_period_days = user_policies.validity_period_days;
  } else {
    hr = GetGlobalFlag(base::UTF8ToWide(kKeyValidityPeriodInDays),
                       &validity_period_days);
    if (FAILED(hr)) {
      LOGFN(VERBOSE) << "GetGlobalFlag for " << kKeyValidityPeriodInDays
                     << " failed. hr=" << putHR(hr);
      // Fallback to the less obstructive option to not enforce login via google
      // when fetching the registry entry fails.
      return false;
    }
  }

  int64_t validity_period_in_millis =
      kDayInMillis * static_cast<int64_t>(validity_period_days);
  int64_t time_delta_from_last_login =
      base::Time::Now().ToDeltaSinceWindowsEpoch().InMilliseconds() -
      last_token_valid_millis_int64;
  return time_delta_from_last_login >= validity_period_in_millis;
}

bool AssociatedUserValidator::HasInternetConnection() const {
  return InternetAvailabilityChecker::Get()->HasInternetConnection();
}

bool AssociatedUserValidator::HasInvokedUpdateAssociatedSids() {
  return has_invoked_update_associated_sids_;
}

HRESULT AssociatedUserValidator::UpdateAssociatedSids(
    std::map<std::wstring, std::wstring>* sid_to_handle) {
  has_invoked_update_associated_sids_ = true;

  std::map<std::wstring, UserTokenHandleInfo> sids_to_handle_info;

  HRESULT hr = GetUserTokenHandles(&sids_to_handle_info);
  if (FAILED(hr)) {
    LOGFN(ERROR) << "GetUserTokenHandles hr=" << putHR(hr);
    return hr;
  }

  std::set<std::wstring> users_to_delete;
  OSUserManager* manager = OSUserManager::Get();
  for (const auto& sid_to_association : sids_to_handle_info) {
    const std::wstring& sid = sid_to_association.first;
    const UserTokenHandleInfo& info = sid_to_association.second;

    // If both gaia id and email address are empty. Then remove the
    // User Properties as it is invalid.
    if (info.gaia_id.empty() && info.email_address.empty()) {
      users_to_delete.insert(sid_to_association.first);
      continue;
    }
    hr = manager->FindUserBySID(sid.c_str(), nullptr, 0, nullptr, 0);
    if (hr == HRESULT_FROM_WIN32(ERROR_NONE_MAPPED)) {
      users_to_delete.insert(sid_to_association.first);
      continue;
    } else if (FAILED(hr)) {
      LOGFN(ERROR) << "manager->FindUserBySID hr=" << putHR(hr);
    }

    if (sid_to_handle)
      sid_to_handle->emplace(sid, info.token_handle);
  }

  for (const auto& to_delete : users_to_delete) {
    user_to_token_handle_info_.erase(to_delete);
    RemoveAllUserProperties(to_delete);
  }

  return S_OK;
}

size_t AssociatedUserValidator::GetAssociatedUsersCount() {
  base::AutoLock locker(validator_lock_);

  UpdateAssociatedSids(nullptr);

  return user_to_token_handle_info_.size();
}

bool AssociatedUserValidator::IsUserAccessBlockingEnforced(
    CREDENTIAL_PROVIDER_USAGE_SCENARIO cpus) const {
  if (!CGaiaCredentialProvider::IsUsageScenarioSupported(cpus))
    return false;

  return true;
}

bool AssociatedUserValidator::DenySigninForUsersWithInvalidTokenHandles(
    CREDENTIAL_PROVIDER_USAGE_SCENARIO cpus,
    const std::vector<std::wstring>& reauth_sids) {
  base::AutoLock locker(validator_lock_);

  if (block_deny_access_update_) {
    LOGFN(VERBOSE) << "Block the deny access update";
    return false;
  }

  if (!IsUserAccessBlockingEnforced(cpus)) {
    LOGFN(VERBOSE) << "User Access Blocking not enforced.";
    return false;
  }

  HRESULT hr = UpdateAssociatedSids(nullptr);
  if (FAILED(hr)) {
    LOGFN(ERROR) << "UpdateAssociatedSids hr=" << putHR(hr);
    return false;
  }

  auto policy = ScopedLsaPolicy::Create(POLICY_ALL_ACCESS);

  bool user_denied_signin = false;
  OSUserManager* manager = OSUserManager::Get();
  for (const auto& sid : reauth_sids) {
    if (locked_user_sids_.find(sid) != locked_user_sids_.end())
      continue;

    // Note that logon hours cannot be changed on domain joined AD user account.
    if (GetAuthEnforceReason(sid) != EnforceAuthReason::NOT_ENFORCED &&
        !manager->IsUserDomainJoined(sid)) {
      LOGFN(VERBOSE) << "Revoking access for sid=" << sid;
      hr = ModifyUserAccess(policy, sid, false);
      if (FAILED(hr)) {
        LOGFN(ERROR) << "ModifyUserAccess sid=" << sid << " hr=" << putHR(hr);
      } else {
        locked_user_sids_.insert(sid);
        user_denied_signin = true;
      }
    } else if (manager->IsUserDomainJoined(sid)) {
      // TODO(crbug.com/40631676): Description provided in the bug.
      LOGFN(VERBOSE) << "Not denying signin for AD user accounts.";
    }
  }

  return user_denied_signin;
}

HRESULT AssociatedUserValidator::RestoreUserAccess(const std::wstring& sid) {
  base::AutoLock locker(validator_lock_);

  if (locked_user_sids_.erase(sid)) {
    auto policy = ScopedLsaPolicy::Create(POLICY_ALL_ACCESS);
    return ModifyUserAccess(policy, sid, true);
  }

  return S_OK;
}

void AssociatedUserValidator::AllowSigninForAllAssociatedUsers(
    CREDENTIAL_PROVIDER_USAGE_SCENARIO cpus) {
  base::AutoLock locker(validator_lock_);

  if (!CGaiaCredentialProvider::IsUsageScenarioSupported(cpus))
    return;

  std::map<std::wstring, std::wstring> sids_to_handle;
  HRESULT hr = UpdateAssociatedSids(&sids_to_handle);
  if (FAILED(hr)) {
    LOGFN(ERROR) << "UpdateAssociatedSids hr=" << putHR(hr);
    return;
  }

  auto policy = ScopedLsaPolicy::Create(POLICY_ALL_ACCESS);
  for (const auto& sid_to_handle : sids_to_handle)
    ModifyUserAccess(policy, sid_to_handle.first, true);

  locked_user_sids_.clear();
}

void AssociatedUserValidator::AllowSigninForUsersWithInvalidTokenHandles() {
  base::AutoLock locker(validator_lock_);

  LOGFN(VERBOSE);
  auto policy = ScopedLsaPolicy::Create(POLICY_ALL_ACCESS);
  for (auto& sid : locked_user_sids_) {
    HRESULT hr = ModifyUserAccess(policy, sid, true);
    if (FAILED(hr))
      LOGFN(ERROR) << "ModifyUserAccess sid=" << sid << " hr=" << putHR(hr);
  }
  locked_user_sids_.clear();
}

void AssociatedUserValidator::StartRefreshingTokenHandleValidity() {
  base::AutoLock locker(validator_lock_);

  std::map<std::wstring, std::wstring> sid_to_handle;
  HRESULT hr = UpdateAssociatedSids(&sid_to_handle);

  if (FAILED(hr)) {
    LOGFN(ERROR) << "UpdateAssociatedSids hr=" << putHR(hr);
    return;
  }

  // Fire off the threads that will query the token handles but do not wait for
  // them to complete. Later queries will do the wait.
  CheckTokenHandleValidity(sid_to_handle);
}

void AssociatedUserValidator::CheckTokenHandleValidity(
    const std::map<std::wstring, std::wstring>& handles_to_verify) {
  for (auto it = handles_to_verify.cbegin(); it != handles_to_verify.cend();
       ++it) {
    // Make sure the user actually exists.
    if (FAILED(OSUserManager::Get()->FindUserBySID(it->first.c_str(), nullptr,
                                                   0, nullptr, 0))) {
      continue;
    }

    // User exists, has a gaia id or email address, but no token handle.
    // Consider this an invalid token handle and the user needs to sign in
    // with Gaia to get a new one.
    if (it->second.empty()) {
      user_to_token_handle_info_[it->first] =
          std::make_unique<TokenHandleInfo>(std::wstring());
      continue;
    }

    // If there is already token handle info for the current user and it
    // 1. Is NOT valid.
    // AND
    // 2. There is no query still pending on it.
    // Then this means that the user has an invalid token handle.
    // The state of this handle will never change during the execution of the
    // sign in process (since a new token handle will only be written on
    // successful sign in) so it does not need to update and the current state
    // information is valid.
    auto existing_validity_it = user_to_token_handle_info_.find(it->first);
    if (existing_validity_it != user_to_token_handle_info_.end() &&
        !existing_validity_it->second->is_valid &&
        !existing_validity_it->second->pending_query_thread.IsValid()) {
      continue;
    }

    // Start a new token handle query if:
    // 1. No token info entry yet exists for this token handle.
    // OR
    // 2. Token info exists but it is stale (either because it is a query that
    // was started a while ago but for which we never tried to query the result
    // because the last query result is from a while ago or because the last
    // query result is older than |kTokenHandleValidityLifetime|).
    if (existing_validity_it == user_to_token_handle_info_.end() ||
        TokenHandleNeedsUpdate(existing_validity_it->second->last_update)) {
      StartTokenValidityQuery(it->first, it->second, validation_timeout_);
    }
  }
}

void AssociatedUserValidator::StartTokenValidityQuery(
    const std::wstring& sid,
    const std::wstring& token_handle,
    base::TimeDelta timeout) {
  base::Time max_end_time = base::Time::Now() + timeout;

  // Fire off a thread to check with Gaia if a re-auth is required. The thread
  // does not reference this object nor does this object have any reference
  // directly on the thread. This object only checks for the return code of the
  // thread within a given timeout. If no return code is given in that timeout
  // then assume that the token handle is valid. The running thread can continue
  // running and finish its execution without worrying about notifying anything
  // about the result.
  unsigned wait_thread_id;
  CheckReauthParams* params = new CheckReauthParams{
      sid, token_handle,
      WinHttpUrlFetcher::Create(GURL(AssociatedUserValidator::kTokenInfoUrl))};
  uintptr_t wait_thread =
      _beginthreadex(nullptr, 0, CheckReauthStatus,
                     reinterpret_cast<void*>(params), 0, &wait_thread_id);
  if (wait_thread == 0) {
    user_to_token_handle_info_[sid] =
        std::make_unique<TokenHandleInfo>(token_handle);
    delete params;
    return;
  }

  user_to_token_handle_info_[sid] = std::make_unique<TokenHandleInfo>(
      token_handle, max_end_time, reinterpret_cast<HANDLE>(wait_thread));
}

bool AssociatedUserValidator::IsAuthEnforcedForUser(const std::wstring& sid) {
  base::AutoLock locker(validator_lock_);
  return GetAuthEnforceReason(sid) !=
         AssociatedUserValidator::EnforceAuthReason::NOT_ENFORCED;
}

AssociatedUserValidator::EnforceAuthReason
AssociatedUserValidator::GetAuthEnforceReason(const std::wstring& sid) {
  LOGFN(VERBOSE);

  // Is user not associated, then we shouldn't have any auth enforcement.
  if (!IsUserAssociated(sid)) {
    LOGFN(VERBOSE) << "IsUserAssociated is false, not forcing auth";
    return AssociatedUserValidator::EnforceAuthReason::NOT_ENFORCED;
  }

  // Check if online sign in is enforced.
  if (IsOnlineLoginEnforced(sid)) {
    LOGFN(VERBOSE) << "IsOnlineLoginEnforced is true, forcing auth";
    return AssociatedUserValidator::EnforceAuthReason::ONLINE_LOGIN_ENFORCED;
  }

  // All token handles are valid when no internet connection is available.
  if (!HasInternetConnection()) {
    if (!IsOnlineLoginStale(sid)) {
      LOGFN(VERBOSE) << "HasInternetConnectionis false and IsOnlineLoginStale "
                        "is false - not forcing auth";
      return AssociatedUserValidator::EnforceAuthReason::NOT_ENFORCED;
    }
    LOGFN(VERBOSE) << "HasInternetConnectionis false and IsOnlineLoginStale is "
                      "true - forcing auth";
    return AssociatedUserValidator::EnforceAuthReason::ONLINE_LOGIN_STALE;
  }

  // Force user to login when policies are missing or stale. This check should
  // be done before MDM enrollment to have the correct MDM enrollment policy for
  // user.
  if (UserPoliciesManager::Get()->CloudPoliciesEnabled() &&
      UserPoliciesManager::Get()->IsUserPolicyStaleOrMissing(sid)) {
    LOGFN(VERBOSE) << "CloudPolicies enabled and  >IsUserPolicyStaleOrMissing "
                      "is true - forcing auth";
    return AssociatedUserValidator::EnforceAuthReason::
        MISSING_OR_STALE_USER_POLICIES;
  }

  // Force a reauth only for this user if mdm enrollment is needed, so that they
  // enroll.
  if (NeedsToEnrollWithMdm(sid)) {
    LOGFN(VERBOSE) << "NeedsToEnrollWithMdm is true, forcing auth";
    return AssociatedUserValidator::EnforceAuthReason::NOT_ENROLLED_WITH_MDM;
  }

  if (PasswordRecoveryEnabled()) {
    std::wstring store_key = GetUserPasswordLsaStoreKey(sid);
    auto policy = ScopedLsaPolicy::Create(POLICY_ALL_ACCESS);
    if (!policy->PrivateDataExists(store_key.c_str())) {
      LOGFN(VERBOSE) << "Enforcing re-auth due to missing password lsa store "
                        "data for user "
                     << sid;
      return AssociatedUserValidator::EnforceAuthReason::
          MISSING_PASSWORD_RECOVERY_INFO;
    }
  }

  if (!IsTokenHandleValidForUser(sid)) {
    LOGFN(VERBOSE) << "IsTokenHandleValidForUser is false, forcing auth";
    return AssociatedUserValidator::EnforceAuthReason::INVALID_TOKEN_HANDLE;
  }

  if (UploadDeviceDetailsNeeded(sid)) {
    LOGFN(VERBOSE) << "UploadDeviceDetailsNeeded is true, forcing auth";
    return AssociatedUserValidator::EnforceAuthReason::
        UPLOAD_DEVICE_DETAILS_FAILED;
  }

  return AssociatedUserValidator::EnforceAuthReason::NOT_ENFORCED;
}

bool AssociatedUserValidator::IsUserAssociated(const std::wstring& sid) {
  // If at this point there is no token info entry for this user, assume the
  // user is not associated and does not need a token handle and is thus always
  // valid. Between the first creation of all the token infos
  // and the eventual successful sign in, there should be no new token handles
  // created so we can immediately assign that an absence of token handle info
  // for this user means that they are not associated and do not need to
  // validate any token handles.
  auto validity_it = user_to_token_handle_info_.find(sid);
  return validity_it != user_to_token_handle_info_.end();
}

bool AssociatedUserValidator::IsTokenHandleValidForUser(
    const std::wstring& sid) {
  // Make sure sid mapping in registry is always up to date before checking
  // for token validity.
  if (!HasInvokedUpdateAssociatedSids())
    UpdateAssociatedSids(nullptr);

  auto validity_it = user_to_token_handle_info_.find(sid);
  if (validity_it == user_to_token_handle_info_.end())
    return true;

  // This function will start a new query if the current info for the token
  // handle is stale or has not yet been queried. At the end of this function,
  // either we will already have the validity of the token handle or we have a
  // handle to a pending query that we wait to complete before finally having
  // the validity.
  CheckTokenHandleValidity({{sid, validity_it->second->queried_token_handle}});

  // If a query is still pending, wait for it and update the validity.
  if (validity_it->second->pending_query_thread.IsValid()) {
    validity_it->second->is_valid =
        WaitForQueryResult(validity_it->second->pending_query_thread,
                           validity_it->second->last_update);
    if (!validity_it->second->is_valid) {
      // Clear the token handle to delete it. At this point we know it is
      // definetely invalid so if we remove the handle completely we will
      // no longer need to query it and can just assume immediately that
      // the user needs to be reauthorized.
      HRESULT hr = SetUserProperty(sid, kUserTokenHandle, L"");
      if (FAILED(hr))
        LOGFN(ERROR) << "SetUserProperty hr=" << putHR(hr);
    }
    validity_it->second->pending_query_thread.Close();
    base::Time now = base::Time::Now();
    // NOTE: Don't always update |last_update| because the result of this query
    // may still be old. E.g.:
    // 1. At time X thread is started to query. The maximum end time of this
    //    query is X + timeout
    // 2. At time Y (X + timeout + lifetime > Y >> X + timeout) we ask for
    //    the validity of the token handle. The time Y might still be valid
    //    when considering the lifetime but may still be relatively old
    //    depending on how long after time X the request is made. So keep the
    //    original end time of the query as the last update (if the end time
    //    occurs before time Y) so that the token handle is updated earlier on
    //    the next query.
    validity_it->second->last_update = now > validity_it->second->last_update
                                           ? validity_it->second->last_update
                                           : now;
  }

  if (validity_it->second->is_valid) {
    // Update the last token valid timestamp.
    int64_t current_time = static_cast<int64_t>(
        base::Time::Now().ToDeltaSinceWindowsEpoch().InMilliseconds());
    SetUserProperty(sid, base::UTF8ToWide(kKeyLastTokenValid),
                    base::NumberToWString(current_time));
  }

  return validity_it->second->is_valid;
}

void AssociatedUserValidator::BlockDenyAccessUpdate() {
  base::AutoLock locker(validator_lock_);
  ++block_deny_access_update_;
}

void AssociatedUserValidator::UnblockDenyAccessUpdate() {
  base::AutoLock locker(validator_lock_);
  DCHECK(block_deny_access_update_ > 0);
  --block_deny_access_update_;
}

bool AssociatedUserValidator::IsDenyAccessUpdateBlocked() const {
  base::AutoLock locker(validator_lock_);
  return block_deny_access_update_ > 0;
}

bool AssociatedUserValidator::IsUserAccessBlockedForTesting(
    const std::wstring& sid) const {
  base::AutoLock locker(validator_lock_);
  return locked_user_sids_.find(sid) != locked_user_sids_.end();
}

void AssociatedUserValidator::ForceRefreshTokenHandlesForTesting() {
  base::AutoLock locker(validator_lock_);
  for (const auto& user_info : user_to_token_handle_info_) {
    // Make the last update time outside the validity lifetime of the token
    // handle.
    user_info.second->last_update =
        base::Time::Now() - kTokenHandleValidityLifetime;
  }
}

}  // namespace credential_provider