// Copyright 2024 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/browser/ash/login/lock/online_reauth/lock_screen_reauth_manager.h"

#include <optional>
#include <utility>

#include "ash/constants/ash_features.h"
#include "ash/constants/ash_login_pref_names.h"
#include "ash/constants/ash_pref_names.h"
#include "ash/login/login_screen_controller.h"
#include "ash/public/cpp/reauth_reason.h"
#include "ash/shell.h"
#include "base/check.h"
#include "base/check_deref.h"
#include "base/check_is_test.h"
#include "base/functional/callback_helpers.h"
#include "base/logging.h"
#include "base/metrics/histogram_functions.h"
#include "base/syslog_logging.h"
#include "base/time/default_clock.h"
#include "base/trace_event/trace_event.h"
#include "chrome/browser/ash/login/auth/chrome_safe_mode_delegate.h"
#include "chrome/browser/ash/login/helper.h"
#include "chrome/browser/ash/login/profile_auth_data.h"
#include "chrome/browser/ash/login/reauth_stats.h"
#include "chrome/browser/ash/login/saml/in_session_password_sync_manager.h"
#include "chrome/browser/ash/profiles/profile_helper.h"
#include "chrome/browser/ui/webui/ash/lock_screen_reauth/lock_screen_reauth_dialogs.h"
#include "chromeos/ash/components/login/auth/auth_session_authenticator.h"
#include "chromeos/ash/components/login/auth/password_update_flow.h"
#include "chromeos/ash/components/login/auth/public/authentication_error.h"
#include "chromeos/ash/components/login/auth/public/user_context.h"
#include "chromeos/ash/components/osauth/public/auth_policy_connector.h"
#include "chromeos/ash/components/osauth/public/common_types.h"
#include "chromeos/ash/components/proximity_auth/screenlock_bridge.h"
#include "chromeos/ash/components/settings/cros_settings.h"
#include "chromeos/ash/services/auth_factor_config/auth_factor_config_utils.h"
#include "components/prefs/pref_service.h"
#include "components/session_manager/core/session_manager.h"
#include "components/user_manager/known_user.h"
#include "components/user_manager/user_manager.h"
#include "content/public/browser/storage_partition.h"

namespace ash {
namespace {
constexpr char kLockScreenReauthHistogram[] =
    "ChromeOS.LockScreenReauth.LockScreenReauthReason";

bool IsScreenLocked() {
  return session_manager::SessionManager::Get()->IsScreenLocked();
}

void RunAuthConfigExitIfNotNull(
    OnGetAuthFactorsConfigurationExitCallback callback) {
  if (callback) {
    callback.Run();
  }
}

}  // namespace

LockScreenReauthManager::LockScreenReauthManager(PrefService* local_state,
                                                 Profile* primary_profile)
    : local_state_(CHECK_DEREF(local_state)),
      primary_profile_(primary_profile),
      primary_user_(ProfileHelper::Get()->GetUserByProfile(primary_profile)),
      clock_(base::DefaultClock::GetInstance()),
      in_session_password_sync_manager_(
          InSessionPasswordSyncManager(local_state, primary_profile_)) {
  CHECK(primary_user_);
  auto* session_manager = session_manager::SessionManager::Get();
  // Extra check as SessionManager may be not initialized in some unit
  // tests
  if (session_manager) {
    session_manager->AddObserver(this);
  }

  screenlock_bridge_ = proximity_auth::ScreenlockBridge::Get();
  CHECK(screenlock_bridge_);
}

LockScreenReauthManager::~LockScreenReauthManager() {
  auto* session_manager = session_manager::SessionManager::Get();
  if (session_manager) {
    session_manager->RemoveObserver(this);
  }
}

bool LockScreenReauthManager::ShouldPasswordSyncTriggerReauth() {
  return primary_profile_->GetPrefs()->GetBoolean(
      ash::prefs::kLockScreenReauthenticationEnabled);
}

void LockScreenReauthManager::MaybeForceReauthOnLockScreen(
    ReauthReason reauth_reason) {
  if (features::IsManagedLocalPinAndPasswordEnabled()) {
    if (!auth_factor_editor_) {
      auth_factor_editor_ =
          std::make_unique<AuthFactorEditor>(UserDataAuthClient::Get());
    }
    // We need to determine whether the user has a local factor setup or not to
    // decide whether to show online reauth on lockscreen.
    auto user_context = std::make_unique<UserContext>();
    user_context->SetAccountId(primary_user_->GetAccountId());
    auth_factor_editor_->GetAuthFactorsConfiguration(
        std::move(user_context),
        base::BindOnce(&LockScreenReauthManager::OnGetAuthFactorsConfiguration,
                       weak_factory_.GetWeakPtr(), reauth_reason));
  } else {
    MaybeForceReauthOnLockScreenInternal(reauth_reason);
  }
}

void LockScreenReauthManager::MaybeForceReauthOnLockScreenInternal(
    ReauthReason reauth_reason) {
  if (reauth_reason == ReauthReason::kSamlPasswordSyncTokenValidationFailed &&
      !ShouldPasswordSyncTriggerReauth()) {
    // Reauth on lock for token dismatch is disabled by a policy.
    return;
  }

  // Record the reauth reason in case the user signed out without going through
  // lock screen online flow.
  RecordReauthReason(local_state_.get(), primary_user_->GetAccountId(),
                     reauth_reason);

  if (reauth_reason == ReauthReason::kSamlPasswordSyncTokenValidationFailed) {
    is_reauth_required_by_saml_token_mismatch_ = true;
  } else if (reauth_reason == ReauthReason::kSamlLockScreenReauthPolicy) {
    is_reauth_required_by_saml_time_limit_policy_ = true;
  } else if (reauth_reason == ReauthReason::kGaiaLockScreenReauthPolicy) {
    is_reauth_required_by_gaia_time_limit_policy_ = true;
  }

  if (IsScreenLocked()) {
    // On the lock screen: need to update the UI.
    ForceOnlineReauth();
  }
}

void LockScreenReauthManager::OnGetAuthFactorsConfiguration(
    ReauthReason reauth_reason,
    std::unique_ptr<UserContext> user_context,
    std::optional<AuthenticationError> error) {
  CHECK(user_context);
  if (error.has_value()) {
    LOG(WARNING) << "Failed to get auth factors configuration, code "
                 << error->get_cryptohome_error() << ", skip reauth request";
    RunAuthConfigExitIfNotNull(
        auth_factors_configuration_exit_callback_for_testing_);
    return;
  }
  const auto& config = user_context->GetAuthFactorsConfiguration();
  auto* password_factor =
      config.FindFactorByType(cryptohome::AuthFactorType::kPassword);
  bool has_online_password =
      password_factor && auth::IsGaiaPassword(*password_factor);

  // Skip the lock screen reauth attempt if the user does not have an online
  // password configured.
  //
  // This includes cases like:
  //     a. Only a PIN is set up.
  //     b. Only a local password is set up.
  //     c. NO factors are set up at all (e.g., during initial user creation
  //     before the first factor is added). Skipping is fine here, as this will
  //     be re-evaluated on the next time user locks the screen.
  //
  if (!has_online_password) {
    LOG(WARNING)
        << "Skipping lock screen reauth based on the fact that the user "
           "does not have an online password configured";
    RunAuthConfigExitIfNotNull(
        auth_factors_configuration_exit_callback_for_testing_);
    return;
  }

  MaybeForceReauthOnLockScreenInternal(reauth_reason);
  RunAuthConfigExitIfNotNull(
      auth_factors_configuration_exit_callback_for_testing_);
}

void LockScreenReauthManager::SetClockForTesting(const base::Clock* clock) {
  clock_ = clock;
}

void LockScreenReauthManager::Shutdown() {}

void LockScreenReauthManager::OnSessionStateChanged() {
  TRACE_EVENT0("login", "LockScreenReauthManager::OnSessionStateChanged");
  if (!IsScreenLocked()) {
    // We are unlocking the session, no further action required.
    return;
  }

  if (!is_reauth_required_by_gaia_time_limit_policy_ &&
      !is_reauth_required_by_saml_time_limit_policy_ &&
      !is_reauth_required_by_saml_token_mismatch_) {
    // locking the session but no re-auth flag set - show standard UI.
    return;
  }

  // Request re-auth immediately after locking the screen.
  ForceOnlineReauth();
}

void LockScreenReauthManager::ForceOnlineReauth() {
  SYSLOG(INFO) << "(LOGIN) LoginScreenReauthManager::ForceOnlineReauth";
  const auto account_id = primary_user_->GetAccountId();
  screenlock_bridge_->lock_handler()->SetAuthType(
      account_id, proximity_auth::mojom::AuthType::ONLINE_SIGN_IN, u"");

  const bool auto_start_reauth = primary_profile_->GetPrefs()->GetBoolean(
      ash::prefs::kLockScreenAutoStartOnlineReauth);
  if (auto_start_reauth) {
    SYSLOG(INFO) << "(LOGIN) LoginScreenReauthManager::ForceOnlineReauth "
                    "ShowGaiaSignin()";
    Shell::Get()->login_screen_controller()->ShowGaiaSignin(
        /*prefilled_account=*/account_id);
  }
}

void LockScreenReauthManager::ResetOnlineReauth() {
  user_manager::UserManager::Get()->SaveForceOnlineSignin(
      primary_user_->GetAccountId(), false);
  user_manager::KnownUser known_user(&local_state_.get());
  base::Time current_time = clock_->Now();
  known_user.SetLastOnlineSignin(primary_user_->GetAccountId(), current_time);
  // Also adding this information to prefs, because ephemeral users cannot
  // access local state properly.
  primary_profile_->GetPrefs()->SetTime(ash::prefs::kLastOnlineSignInTime,
                                        current_time);
}

void LockScreenReauthManager::CheckCredentials(
    const UserContext& user_context,
    PasswordChangedCallback callback) {
  user_context_ = user_context;
  password_changed_callback_ = std::move(callback);
  content::StoragePartition* lock_screen_partition =
      login::GetLockScreenPartition();
  if (!lock_screen_partition) {
    LOG(ERROR) << "The lock screen partition is not available yet";
    OnCookiesTransferred();
    return;
  }

  bool transfer_saml_auth_cookies_on_subsequent_login = false;
  if (primary_user_->IsAffiliated()) {
    CrosSettings::Get()->GetBoolean(
        kAccountsPrefTransferSAMLCookies,
        &transfer_saml_auth_cookies_on_subsequent_login);
  }

  ProfileAuthData::Transfer(
      lock_screen_partition, primary_profile_,
      false /*transfer_auth_cookies_on_first_login*/,
      transfer_saml_auth_cookies_on_subsequent_login,
      base::BindOnce(&LockScreenReauthManager::OnCookiesTransferred,
                     weak_factory_.GetWeakPtr()));
}

void LockScreenReauthManager::OnCookiesTransferred() {
  if (!auth_session_authenticator_) {
    auth_session_authenticator_ =
        base::MakeRefCounted<AuthSessionAuthenticator>(
            this, std::make_unique<ChromeSafeModeDelegate>(),
            /*user_recorder=*/base::DoNothing(),
            /* new_user_can_be_owner=*/false, &local_state_.get());
  }
  // Perform a fast ("verify-only") check of the current password. This is an
  // optimization: if the password wasn't actually changed the check will
  // finish faster. However, it also implies that if the password was actually
  // changed, we'll need to start a new cryptohome AuthSession for updating
  // the password auth factor (in `password_update_flow_`).
  auth_session_authenticator_->AuthenticateToUnlock(
      user_manager::UserManager::Get()->IsEphemeralAccountId(
          user_context_.GetAccountId()),
      std::make_unique<UserContext>(user_context_));
}

void LockScreenReauthManager::UpdateUserPassword(
    const std::string& old_password) {
  if (!password_update_flow_) {
    password_update_flow_ = std::make_unique<PasswordUpdateFlow>();
  }
  // TODO(b/258638651): The old password might be checked quicker using a
  // "verify-only" mode, before we go into the more expensive full update flow.
  password_update_flow_->Start(
      std::make_unique<UserContext>(user_context_), old_password,
      base::BindOnce(&LockScreenReauthManager::OnPasswordUpdateSuccess,
                     weak_factory_.GetWeakPtr()),
      base::BindOnce(&LockScreenReauthManager::OnPasswordUpdateFailure,
                     weak_factory_.GetWeakPtr()));
}

// TODO(crbug.com/40163256): Add UMA histograms for lockscreen online
// re-authentication.
void LockScreenReauthManager::OnAuthFailure(const AuthFailure& error) {
  password_changed_callback_.Run();
}

void LockScreenReauthManager::OnAuthSuccess(const UserContext& user_context) {
  SYSLOG(INFO) << "(LOGIN) LoginScreenReauthManager::OnAuthSuccess";
  if (user_context.GetAccountId() != primary_user_->GetAccountId()) {
    // Tried to re-authenicate with non-primary user: the authentication was
    // successful but we are allowed to unlock only with valid credentials of
    // the user who locked the screen. In this case show customized version
    // of first re-auth flow dialog with an error message.
    const std::string msg = "(LOGIN) Different user is unlocking the device ";
    SYSLOG(INFO) << msg;
    LOG(FATAL) << msg;
  }

  ResetOnlineReauth();
  SendLockscreenReauthReason();
  if (is_reauth_required_by_saml_token_mismatch_) {
    SYSLOG(INFO) << "(LOGIN) Reauth due to SAML token mismatch. Fetch token. ";
    in_session_password_sync_manager_.FetchTokenAsync();
  }

  // is_reauth_required_by_saml_token_mismatch_ shouldn't be reset until
  // SAML token is fetched.
  is_reauth_required_by_gaia_time_limit_policy_ =
      is_reauth_required_by_saml_time_limit_policy_ = false;

  if (screenlock_bridge_->IsLocked()) {
    screenlock_bridge_->lock_handler()->Unlock(user_context.GetAccountId());
  }
  LockScreenStartReauthDialog::Dismiss();
}

void LockScreenReauthManager::SendLockscreenReauthReason() {
  if (is_reauth_required_by_gaia_time_limit_policy_) {
    base::UmaHistogramEnumeration(kLockScreenReauthHistogram,
                                  ReauthReason::kGaiaLockScreenReauthPolicy,
                                  ReauthReason::kNumReauthFlowReasons);
  }

  if (is_reauth_required_by_saml_time_limit_policy_) {
    base::UmaHistogramEnumeration(kLockScreenReauthHistogram,
                                  ReauthReason::kSamlLockScreenReauthPolicy,
                                  ReauthReason::kNumReauthFlowReasons);
  }

  if (is_reauth_required_by_saml_token_mismatch_) {
    base::UmaHistogramEnumeration(
        kLockScreenReauthHistogram,
        ReauthReason::kSamlPasswordSyncTokenValidationFailed,
        ReauthReason::kNumReauthFlowReasons);
  }
}

void LockScreenReauthManager::OnPasswordUpdateSuccess(
    std::unique_ptr<UserContext> user_context) {
  CHECK(user_context);
  OnAuthSuccess(*user_context);
}

void LockScreenReauthManager::OnPasswordUpdateFailure(
    std::unique_ptr<UserContext> /*user_context*/,
    AuthenticationError /*error*/) {
  OnAuthFailure(AuthFailure(AuthFailure::COULD_NOT_MOUNT_CRYPTOHOME));
}

void LockScreenReauthManager::ResetReauthRequiredBySamlTokenDismatch() {
  is_reauth_required_by_saml_token_mismatch_ = false;
}

void LockScreenReauthManager::SetGetAuthfactorsConfigurationCallbackForTesting(
    OnGetAuthFactorsConfigurationExitCallback callback) {
  CHECK_IS_TEST();
  auth_factors_configuration_exit_callback_for_testing_ = std::move(callback);
}

}  // namespace ash
