// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chromeos/ash/components/osauth/impl/auth_hub_impl.h"
#include <memory>
#include <utility>
#include "base/check.h"
#include "base/check_op.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/logging.h"
#include "base/notreached.h"
#include "chromeos/ash/components/osauth/impl/auth_factor_presence_cache.h"
#include "chromeos/ash/components/osauth/impl/auth_hub_common.h"
#include "chromeos/ash/components/osauth/impl/auth_hub_mode_lifecycle.h"
#include "chromeos/ash/components/osauth/impl/auth_hub_vector_lifecycle.h"
#include "chromeos/ash/components/osauth/public/auth_attempt_consumer.h"
#include "chromeos/ash/components/osauth/public/auth_factor_engine_factory.h"
#include "chromeos/ash/components/osauth/public/auth_factor_status_consumer.h"
#include "chromeos/ash/components/osauth/public/common_types.h"
#include "chromeos/ash/components/osauth/public/string_utils.h"
#include "components/account_id/account_id.h"
namespace ash {
AuthHubImpl::AuthHubImpl(AuthFactorPresenceCache* factor_cache)
: cache_(factor_cache) {
mode_lifecycle_ = std::make_unique<AuthHubModeLifecycle>(this);
}
AuthHubImpl::~AuthHubImpl() = default;
void AuthHubImpl::InitializeForMode(AuthHubMode target) {
CHECK_NE(target, AuthHubMode::kNone);
SwitchToModeImpl(target);
}
void AuthHubImpl::CancelCurrentAttempt(AuthHubConnector* connector) {
if (attempt_handler_->HasOngoingAttempt()) {
attempt_handler_->PrepareForShutdown(
base::BindOnce(&AuthHubImpl::OnFactorAttemptFinishedForCancel,
weak_factory_.GetWeakPtr()));
return;
}
vector_lifecycle_->CancelAttempt();
}
void AuthHubImpl::Shutdown() {
SwitchToModeImpl(AuthHubMode::kNone);
}
void AuthHubImpl::SwitchToModeImpl(AuthHubMode target) {
if (vector_lifecycle_ && !vector_lifecycle_->IsIdle()) {
target_mode_ = target;
// Eventually, after the current attempt gets canceled, `OnIdle()` will be
// triggered, which then switches the mode to `target_mode_`.
if (attempt_handler_->HasOngoingAttempt()) {
attempt_handler_->PrepareForShutdown(
base::BindOnce(&AuthHubImpl::OnFactorAttemptFinishedForCancel,
weak_factory_.GetWeakPtr()));
return;
}
vector_lifecycle_->CancelAttempt();
return;
}
mode_lifecycle_->SwitchToMode(target);
}
void AuthHubImpl::OnFactorAttemptFinishedForCancel() {
vector_lifecycle_->CancelAttempt();
}
void AuthHubImpl::EnsureInitialized(base::OnceClosure on_initialized) {
if (mode_lifecycle_->IsReady()) {
std::move(on_initialized).Run();
return;
}
on_initialized_listeners_.AddUnsafe(std::move(on_initialized));
}
void AuthHubImpl::StartAuthentication(AccountId account_id,
AuthPurpose purpose,
AuthAttemptConsumer* consumer) {
if (!PurposeMatchesMode(purpose, mode_lifecycle_->GetCurrentMode())) {
LOG(ERROR) << "Attempt for " << purpose
<< " rejected due to incorrect mode "
<< mode_lifecycle_->GetCurrentMode();
consumer->OnUserAuthAttemptRejected();
return;
}
CHECK(vector_lifecycle_);
AuthAttemptVector attempt{account_id, purpose};
if (current_attempt_.has_value()) {
// If we have two login attempts, let new attempt take
// over the existing one.
if (AttemptShouldOverrideAnother(attempt, *current_attempt_)) {
LOG(WARNING) << "Overriding ongoing attempt";
pending_attempt_ = attempt;
pending_consumer_ = consumer;
if (attempt_handler_->HasOngoingAttempt()) {
attempt_handler_->PrepareForShutdown(
base::BindOnce(&AuthHubImpl::OnFactorAttemptFinishedForCancel,
weak_factory_.GetWeakPtr()));
return;
}
vector_lifecycle_->CancelAttempt();
return;
}
if (AttemptShouldOverrideAnother(*current_attempt_, attempt)) {
LOG(WARNING) << "Attempt rejected: another higher-priority attempt";
consumer->OnUserAuthAttemptRejected();
return;
}
// Neither attempt is considered "Stronger" one,
// so we should preserve ongoing one.
LOG(WARNING) << "Attempt rejected: another same-priority attempt";
consumer->OnUserAuthAttemptRejected();
return;
}
if (pending_attempt_.has_value()) {
// If we have two login attempts, let new attempt take
// over the pending one.
if (AttemptShouldOverrideAnother(attempt, *pending_attempt_)) {
LOG(WARNING) << "Overriding pending attempt";
pending_consumer_->OnUserAuthAttemptRejected();
// Override pending attempt.
pending_attempt_ = attempt;
pending_consumer_ = consumer;
return;
}
if (AttemptShouldOverrideAnother(*pending_attempt_, attempt)) {
LOG(WARNING)
<< "Attempt rejected: another higher-priority pending attempt";
consumer->OnUserAuthAttemptRejected();
return;
}
// Neither attempt is considered "Stronger" one,
// so we should preserve pending one.
LOG(WARNING) << "Attempt rejected: pending same-priority attempt";
consumer->OnUserAuthAttemptRejected();
return;
}
CHECK(!attempt_consumer_);
CHECK(!attempt_handler_);
attempt_consumer_ = consumer;
current_attempt_ = attempt;
AuthFactorsSet cached_factors =
cache_->GetExpectedFactorsPresence(*current_attempt_);
attempt_handler_ = std::make_unique<AuthHubAttemptHandler>(
this, *current_attempt_, engines_, cached_factors);
raw_ptr<AuthFactorStatusConsumer> status_consumer;
attempt_consumer_->OnUserAuthAttemptConfirmed(
attempt_handler_->GetConnector(), status_consumer);
attempt_handler_->SetConsumer(status_consumer);
vector_lifecycle_->StartAttempt(*current_attempt_);
}
bool AuthHubImpl::PurposeMatchesMode(AuthPurpose purpose, AuthHubMode mode) {
switch (mode) {
case AuthHubMode::kLoginScreen:
return purpose == AuthPurpose::kLogin;
case AuthHubMode::kInSession:
return purpose != AuthPurpose::kLogin;
case AuthHubMode::kNone:
NOTREACHED();
}
}
bool AuthHubImpl::AttemptShouldOverrideAnother(
const AuthAttemptVector& first,
const AuthAttemptVector& second) {
if (first.purpose == AuthPurpose::kLogin &&
second.purpose == AuthPurpose::kLogin) {
// New login attempt always overrides previous.
return true;
}
// All login cases should be covered by check above + `PurposeMatchesMode`.
CHECK_NE(first.purpose, AuthPurpose::kLogin);
CHECK_NE(second.purpose, AuthPurpose::kLogin);
if (first.purpose == AuthPurpose::kScreenUnlock) {
// Lock screen always overrides any other attempt.
return true;
}
if (second.purpose == AuthPurpose::kScreenUnlock) {
// Nothing in-session can override lock screen.
return false;
}
// Currently various in-session attempts should not override ongoing attempt.
return false;
}
// AuthHubModeLifecycle::Owner:
void AuthHubImpl::OnReadyForMode(AuthHubMode mode,
AuthEnginesMap available_engines) {
CHECK(engines_.empty());
CHECK(!vector_lifecycle_);
engines_ = std::move(available_engines);
vector_lifecycle_ =
std::make_unique<AuthHubVectorLifecycle>(this, mode, engines_);
on_initialized_listeners_.Notify();
}
void AuthHubImpl::OnExitedMode(AuthHubMode mode) {
engines_.clear();
vector_lifecycle_.reset();
}
void AuthHubImpl::OnModeShutdown() {}
// AuthHubVectorLifecycle::Owner:
AuthFactorEngine::FactorEngineObserver* AuthHubImpl::AsEngineObserver() {
CHECK(attempt_handler_);
return attempt_handler_.get();
}
void AuthHubImpl::OnAttemptStarted(const AuthAttemptVector& attempt,
AuthFactorsSet available_factors,
AuthFactorsSet failed_factors) {
CHECK(attempt == *current_attempt_);
CHECK(attempt_handler_);
attempt_handler_->OnFactorsChecked(available_factors, failed_factors);
}
void AuthHubImpl::OnAttemptCleanedUp(const AuthAttemptVector& attempt) {
CHECK(attempt == *current_attempt_);
if (authenticated_factor_.has_value()) {
AuthProofToken token =
engines_[*authenticated_factor_]->StoreAuthenticationContext();
attempt_consumer_->OnUserAuthSuccess(*authenticated_factor_, token);
authenticated_factor_.reset();
}
}
void AuthHubImpl::OnAttemptFinished(const AuthAttemptVector& attempt) {
CHECK(attempt == *current_attempt_);
attempt_consumer_ = nullptr;
current_attempt_.reset();
attempt_handler_.reset();
}
void AuthHubImpl::OnAttemptCancelled(const AuthAttemptVector& attempt) {
CHECK(attempt == *current_attempt_);
attempt_consumer_->OnUserAuthAttemptCancelled();
attempt_consumer_ = nullptr;
current_attempt_.reset();
attempt_handler_.reset();
}
void AuthHubImpl::OnIdle() {
if (target_mode_.has_value()) {
if (pending_attempt_.has_value()) {
// Cancel pending attempt.
pending_consumer_->OnUserAuthAttemptRejected();
target_mode_.reset();
pending_consumer_ = nullptr;
}
// We can get into this branch if attempt was never
// started/finished, e.g. if Mode change was requested before
// one of the engines started.
if (attempt_consumer_) {
CHECK(current_attempt_);
CHECK(attempt_handler_);
attempt_consumer_->OnUserAuthAttemptCancelled();
attempt_consumer_ = nullptr;
current_attempt_.reset();
attempt_handler_.reset();
}
AuthHubMode mode = *target_mode_;
target_mode_.reset();
SwitchToModeImpl(mode);
return;
}
if (pending_attempt_.has_value()) {
AuthAttemptVector attempt = *pending_attempt_;
AuthAttemptConsumer* consumer = pending_consumer_.get();
pending_consumer_ = nullptr;
pending_attempt_.reset();
StartAuthentication(attempt.account, attempt.purpose, consumer);
return;
}
}
void AuthHubImpl::UpdateFactorUiCache(const AuthAttemptVector& attempt,
AuthFactorsSet available_factors) {
CHECK(attempt == *current_attempt_);
cache_->StoreFactorPresenceCache(attempt, available_factors);
}
void AuthHubImpl::OnFactorAttemptFailed(const AuthAttemptVector& attempt,
AshAuthFactor factor) {
CHECK(attempt == *current_attempt_);
attempt_consumer_->OnFactorAttemptFailed(factor);
}
void AuthHubImpl::OnAuthenticationSuccess(const AuthAttemptVector& attempt,
AshAuthFactor factor) {
CHECK(attempt == *current_attempt_);
CHECK(engines_.contains(factor));
// Record the authenticated factor and start terminating all engines.
// AuthProofToken will be retrieved after all auth engines clean up their
// internal states during the termination process, to avoid race conditions
// on authenticated UserContext.
authenticated_factor_ = factor;
vector_lifecycle_->CancelAttempt();
}
} // namespace ash