chromium/ash/in_session_auth/authentication_dialog.cc

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

#include "ash/in_session_auth/authentication_dialog.h"

#include <memory>
#include <optional>
#include <utility>

#include "ash/public/cpp/in_session_auth_token_provider.h"
#include "ash/public/cpp/shelf_config.h"
#include "ash/strings/grit/ash_strings.h"
#include "base/functional/bind.h"
#include "base/logging.h"
#include "base/strings/utf_string_conversions.h"
#include "base/time/time.h"
#include "chromeos/ash/components/auth_panel/public/shared_types.h"
#include "chromeos/ash/components/cryptohome/common_types.h"
#include "chromeos/ash/components/cryptohome/error_util.h"
#include "chromeos/ash/components/dbus/cryptohome/UserDataAuth.pb.h"
#include "chromeos/ash/components/login/auth/auth_performer.h"
#include "chromeos/ash/components/login/auth/public/auth_session_intent.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/common_types.h"
#include "ui/base/ime/text_input_type.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/mojom/dialog_button.mojom.h"
#include "ui/base/mojom/ui_base_types.mojom-shared.h"
#include "ui/base/ui_base_types.h"
#include "ui/display/screen.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/controls/button/label_button.h"
#include "ui/views/controls/label.h"
#include "ui/views/controls/textfield/textfield.h"
#include "ui/views/layout/flex_layout.h"
#include "ui/views/layout/layout_provider.h"
#include "ui/views/layout/layout_types.h"
#include "ui/views/view_class_properties.h"
#include "ui/views/widget/widget.h"
#include "ui/views/window/dialog_delegate.h"

namespace ash {

namespace {

void AddMargins(views::View* view) {
  const auto* layout_provider = views::LayoutProvider::Get();
  const int horizontal_spacing = layout_provider->GetDistanceMetric(
      views::DISTANCE_RELATED_CONTROL_HORIZONTAL);
  const int vertical_spacing = layout_provider->GetDistanceMetric(
      views::DISTANCE_RELATED_CONTROL_VERTICAL);

  view->SetProperty(views::kMarginsKey,
                    gfx::Insets::VH(vertical_spacing, horizontal_spacing));
}

void ConfigurePasswordField(views::Textfield* password_field) {
  const auto password_field_name =
      l10n_util::GetStringUTF16(IDS_ASH_LOGIN_POD_PASSWORD_PLACEHOLDER);
  password_field->GetViewAccessibility().SetName(password_field_name);
  password_field->SetReadOnly(false);
  password_field->SetTextInputType(ui::TextInputType::TEXT_INPUT_TYPE_PASSWORD);
  password_field->SetPlaceholderText(password_field_name);
  AddMargins(password_field);
}

void ConfigureInvalidPasswordLabel(views::Label* invalid_password_label) {
  invalid_password_label->SetProperty(views::kCrossAxisAlignmentKey,
                                      views::LayoutAlignment::kStart);
  invalid_password_label->SetEnabledColor(SK_ColorRED);
  AddMargins(invalid_password_label);
}

void CenterWidgetOnPrimaryDisplay(views::Widget* widget) {
  auto bounds = display::Screen::GetScreen()->GetPrimaryDisplay().work_area();
  bounds.ClampToCenteredSize(widget->GetContentsView()->GetPreferredSize());
  widget->SetBounds(bounds);
}

}  // namespace

AuthenticationDialog::AuthenticationDialog(
    auth_panel::AuthCompletionCallback on_auth_complete,
    InSessionAuthTokenProvider* auth_token_provider,
    std::unique_ptr<AuthPerformer> auth_performer,
    const AccountId& account_id)
    : password_field_(AddChildView(std::make_unique<views::Textfield>())),
      invalid_password_label_(AddChildView(std::make_unique<views::Label>())),
      on_auth_complete_(std::move(on_auth_complete)),
      auth_performer_(std::move(auth_performer)),
      auth_token_provider_(auth_token_provider) {
  // Dialog setup
  set_fixed_width(views::LayoutProvider::Get()->GetDistanceMetric(
      views::DistanceMetric::DISTANCE_BUBBLE_PREFERRED_WIDTH));
  SetTitle(l10n_util::GetStringUTF16(IDS_ASH_IN_SESSION_AUTH_TITLE));
  SetModalType(ui::mojom::ModalType::kSystem);

  // Callback setup
  SetCancelCallback(base::BindOnce(&AuthenticationDialog::CancelAuthAttempt,
                                   base::Unretained(this)));
  SetCloseCallback(base::BindOnce(&AuthenticationDialog::CancelAuthAttempt,
                                  base::Unretained(this)));

  SetLayoutManager(std::make_unique<views::FlexLayout>())
      ->SetOrientation(views::LayoutOrientation::kVertical)
      .SetCollapseMargins(true);

  ConfigureChildViews();

  // We don't want the user to submit an auth factor to cryptohome before the
  // auth session has started. We re-enable the UI in `OnAuthSessionStarted`
  SetUIDisabled(true);

  auto user_context = std::make_unique<UserContext>();
  user_context->SetAccountId(account_id);

  // TODO(b/240147756): Choose the intent based on
  // `InSessionAuthDialogController::Reason`.
  auth_performer_->StartAuthSession(
      std::move(user_context), /*ephemeral=*/false, AuthSessionIntent::kDecrypt,
      base::BindOnce(&AuthenticationDialog::OnAuthSessionStarted,
                     weak_factory_.GetWeakPtr()));
}

AuthenticationDialog::~AuthenticationDialog() = default;

void AuthenticationDialog::Show() {
  auto* widget = DialogDelegateView::CreateDialogWidget(this,
                                                        /*context=*/nullptr,
                                                        /*parent=*/nullptr);
  CenterWidgetOnPrimaryDisplay(widget);
  Init();
  widget->Show();
}

void AuthenticationDialog::Init() {
  ConfigureOkButton();
  password_field_->RequestFocus();
}

void AuthenticationDialog::NotifyResult(bool success,
                                        const AuthProofToken& token,
                                        base::TimeDelta timeout) {
  if (on_auth_complete_) {
    std::move(on_auth_complete_).Run(success, token, timeout);
  }
}

void AuthenticationDialog::ConfigureOkButton() {
  views::LabelButton* ok_button = GetOkButton();
  ok_button->SetText(
      l10n_util::GetStringUTF16(IDS_ASH_LOGIN_SUBMIT_BUTTON_ACCESSIBLE_NAME));
  ok_button->SetCallback(base::BindRepeating(
      &AuthenticationDialog::ValidateAuthFactor, weak_factory_.GetWeakPtr()));
}

void AuthenticationDialog::SetUIDisabled(bool is_disabled) {
  SetButtonEnabled(ui::mojom::DialogButton::kOk, !is_disabled);
  SetButtonEnabled(ui::mojom::DialogButton::kCancel, !is_disabled);
  password_field_->SetReadOnly(is_disabled);
}

void AuthenticationDialog::ValidateAuthFactor() {
  // Clear warning message.
  invalid_password_label_->SetText({});

  SetUIDisabled(true);

  const auto* password_factor =
      user_context_->GetAuthFactorsData().FindAnyPasswordFactor();
  if (!password_factor) {
    LOG(ERROR) << "Could not find password key";
    ShowAuthError();
    return;
  }

  cryptohome::KeyLabel key_label = password_factor->ref().label();

  // Create a copy of `user_context_` so that we don't lose it to std::move
  // for future auth attempts
  auth_performer_->AuthenticateWithPassword(
      key_label.value(), base::UTF16ToUTF8(password_field_->GetText()),
      std::make_unique<UserContext>(*user_context_),
      base::BindOnce(&AuthenticationDialog::OnAuthFactorValidityChecked,
                     weak_factory_.GetWeakPtr()));
}

void AuthenticationDialog::OnAuthFactorValidityChecked(
    std::unique_ptr<UserContext> user_context,
    std::optional<AuthenticationError> authentication_error) {
  if (authentication_error.has_value()) {
    if (cryptohome::ErrorMatches(
            authentication_error.value().get_cryptohome_error(),
            user_data_auth::CRYPTOHOME_INVALID_AUTH_SESSION_TOKEN)) {
      // Auth session expired for some reason, start it again and reattempt
      // authentication.
      // TODO(b/240147756): Choose the intent based on
      // `InSessionAuthDialogController::Reason`.
      auth_performer_->StartAuthSession(
          std::move(user_context), /*ephemeral=*/false,
          AuthSessionIntent::kDecrypt,
          base::BindOnce(&AuthenticationDialog::OnAuthSessionInvalid,
                         weak_factory_.GetWeakPtr()));
      return;
    }
    LOG(ERROR) << "An error happened during the attempt to validate"
                  "the password: "
               << authentication_error.value().get_cryptohome_error();
    ShowAuthError();
    return;
  }

  is_closing_ = true;

  auth_token_provider_->ExchangeForToken(
      std::move(user_context),
      base::BindOnce(&AuthenticationDialog::NotifyResult,
                     weak_factory_.GetWeakPtr(), /*success=*/true));

  SetUIDisabled(false);
  CancelDialog();
  return;
}

void AuthenticationDialog::ShowAuthError() {
  password_field_->SetInvalid(true);
  password_field_->SelectAll(false);
  invalid_password_label_->SetText(
      l10n_util::GetStringUTF16(IDS_ASH_LOGIN_ERROR_AUTHENTICATING));
  SetUIDisabled(false);
}

void AuthenticationDialog::CancelAuthAttempt() {
  // If dialog is closing after the submission of a valid auth factor,
  // we should not notify any parties, as they would have already been
  // notified after `AuthenticationDialog::OnAuthFactorValidityChecked`
  if (!is_closing_) {
    NotifyResult(/*success=*/false, /*token=*/{}, /*timeout=*/{});
  }
}

void AuthenticationDialog::ConfigureChildViews() {
  ConfigurePasswordField(password_field_);
  ConfigureInvalidPasswordLabel(invalid_password_label_);
}

void AuthenticationDialog::OnAuthSessionInvalid(
    bool user_exists,
    std::unique_ptr<UserContext> user_context,
    std::optional<AuthenticationError> authentication_error) {
  OnAuthSessionStarted(user_exists, std::move(user_context),
                       authentication_error);
  ValidateAuthFactor();
}

void AuthenticationDialog::OnAuthSessionStarted(
    bool user_exists,
    std::unique_ptr<UserContext> user_context,
    std::optional<AuthenticationError> authentication_error) {
  if (authentication_error.has_value()) {
    LOG(ERROR) << "Error starting authsession for in session authentication: "
               << authentication_error.value().get_cryptohome_error();
    CancelAuthAttempt();
  } else if (!user_exists) {
    LOG(ERROR) << "Attempting to authenticate a user which does not exist. "
                  "Aborting authentication attempt";
    CancelAuthAttempt();
  } else {
    user_context_ = std::move(user_context);
    SetUIDisabled(false);
  }
}

}  // namespace ash