[OTPs] Create individual form managers for OTP forms.
In this CL I introduce OtpFormManager class (1 instance per form) that is owned by OtpManager class (1 instance per tab). This class will be in charge of fetching OTP values from available backends, ands its lifetime will be limited by the lifetime of the underlying form. Bug: 415269545, 415273770 Change-Id: Ia51cbb5b1db3eab8c4b61a6fdf1612935c3aedb2 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6513570 Reviewed-by: Ioana Pandele <ioanap@chromium.org> Commit-Queue: Maria Kazinova <kazinova@google.com> Cr-Commit-Position: refs/heads/main@{#1456794}
This commit is contained in:

committed by
Chromium LUCI CQ

parent
998a443594
commit
b736216d23
components/password_manager/core/browser
@ -73,6 +73,8 @@ static_library("browser") {
|
||||
"move_password_to_account_store_helper.h",
|
||||
"old_google_credentials_cleaner.cc",
|
||||
"old_google_credentials_cleaner.h",
|
||||
"one_time_passwords/otp_form_manager.cc",
|
||||
"one_time_passwords/otp_form_manager.h",
|
||||
"one_time_passwords/otp_manager.cc",
|
||||
"one_time_passwords/otp_manager.h",
|
||||
"origin_credential_store.cc",
|
||||
@ -470,6 +472,7 @@ source_set("unit_tests") {
|
||||
"leak_detection_delegate_unittest.cc",
|
||||
"leak_detection_dialog_utils_unittest.cc",
|
||||
"old_google_credentials_cleaner_unittest.cc",
|
||||
"one_time_passwords/otp_manager_unittest.cc",
|
||||
"origin_credential_store_unittest.cc",
|
||||
"passkey_credential_unittest.cc",
|
||||
"password_autofill_manager_unittest.cc",
|
||||
|
@ -0,0 +1,28 @@
|
||||
// Copyright 2025 The Chromium Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
#include "components/password_manager/core/browser/one_time_passwords/otp_form_manager.h"
|
||||
|
||||
namespace password_manager {
|
||||
|
||||
OtpFormManager::OtpFormManager(
|
||||
autofill::FormGlobalId form_id,
|
||||
const std::vector<autofill::FieldGlobalId>& otp_field_ids)
|
||||
: form_id_(form_id), otp_field_ids_(std::move(otp_field_ids)) {
|
||||
// TODO(crbug.com/415273770): Trigger OTP fetching if needed.
|
||||
}
|
||||
|
||||
OtpFormManager::OtpFormManager(OtpFormManager&&) = default;
|
||||
OtpFormManager& OtpFormManager::operator=(OtpFormManager&&) = default;
|
||||
|
||||
OtpFormManager::~OtpFormManager() = default;
|
||||
|
||||
void OtpFormManager::ProcessUpdatedPredictions(
|
||||
const std::vector<autofill::FieldGlobalId>& otp_field_ids) {
|
||||
otp_field_ids_ = std::move(otp_field_ids);
|
||||
|
||||
// TODO(crbug.com/415273770): Check if OTP source has changed.
|
||||
}
|
||||
|
||||
} // namespace password_manager
|
@ -0,0 +1,48 @@
|
||||
// Copyright 2025 The Chromium Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
#ifndef COMPONENTS_PASSWORD_MANAGER_CORE_BROWSER_ONE_TIME_PASSWORDS_OTP_FORM_MANAGER_H_
|
||||
#define COMPONENTS_PASSWORD_MANAGER_CORE_BROWSER_ONE_TIME_PASSWORDS_OTP_FORM_MANAGER_H_
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "base/containers/flat_map.h"
|
||||
#include "components/autofill/core/browser/field_types.h"
|
||||
#include "components/autofill/core/common/unique_ids.h"
|
||||
|
||||
namespace password_manager {
|
||||
|
||||
// A class in charge of handling individual OTP forms, one instance per form.
|
||||
class OtpFormManager {
|
||||
public:
|
||||
OtpFormManager(autofill::FormGlobalId form_id,
|
||||
const std::vector<autofill::FieldGlobalId>& otp_field_ids);
|
||||
|
||||
OtpFormManager(const OtpFormManager&) = delete;
|
||||
OtpFormManager& operator=(const OtpFormManager&) = delete;
|
||||
OtpFormManager(OtpFormManager&&);
|
||||
OtpFormManager& operator=(OtpFormManager&&);
|
||||
|
||||
~OtpFormManager();
|
||||
|
||||
// Forms can change dynamically during their lifetime. Ensure the most recent
|
||||
// data is used for form filling.
|
||||
void ProcessUpdatedPredictions(
|
||||
const std::vector<autofill::FieldGlobalId>& otp_field_ids);
|
||||
|
||||
#if defined(UNIT_TEST)
|
||||
const std::vector<autofill::FieldGlobalId>& otp_field_ids() const {
|
||||
return otp_field_ids_;
|
||||
}
|
||||
#endif // defined(UNIT_TEST)
|
||||
|
||||
private:
|
||||
autofill::FormGlobalId form_id_;
|
||||
|
||||
std::vector<autofill::FieldGlobalId> otp_field_ids_;
|
||||
};
|
||||
|
||||
} // namespace password_manager
|
||||
|
||||
#endif // COMPONENTS_PASSWORD_MANAGER_CORE_BROWSER_ONE_TIME_PASSWORDS_OTP_FORM_MANAGER_H_
|
@ -5,22 +5,61 @@
|
||||
#include "components/password_manager/core/browser/one_time_passwords/otp_manager.h"
|
||||
|
||||
#include "components/autofill/core/common/form_data.h"
|
||||
#include "components/autofill/core/common/form_field_data.h"
|
||||
#include "components/password_manager/core/browser/one_time_passwords/otp_form_manager.h"
|
||||
#include "components/password_manager/core/browser/password_manager_client.h"
|
||||
|
||||
namespace password_manager {
|
||||
|
||||
OtpManager::OtpManager(PasswordManagerClient* client) : client_(client) {
|
||||
DCHECK(client_);
|
||||
namespace {
|
||||
|
||||
std::vector<autofill::FieldGlobalId> GetFillableOtpFieldIds(
|
||||
const autofill::FormData& form,
|
||||
const base::flat_map<autofill::FieldGlobalId, autofill::FieldType>&
|
||||
field_predictions) {
|
||||
std::vector<autofill::FieldGlobalId> fillable_otp_fields;
|
||||
for (const auto& prediction : field_predictions) {
|
||||
if (prediction.second != autofill::ONE_TIME_CODE) {
|
||||
continue;
|
||||
}
|
||||
const autofill::FormFieldData* field =
|
||||
form.FindFieldByGlobalId(prediction.first);
|
||||
if (field->IsTextInputElement()) {
|
||||
fillable_otp_fields.push_back(prediction.first);
|
||||
}
|
||||
}
|
||||
return fillable_otp_fields;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
OtpManager::OtpManager(PasswordManagerClient* client) : client_(client) {
|
||||
CHECK(client_);
|
||||
}
|
||||
|
||||
OtpManager::~OtpManager() = default;
|
||||
|
||||
void OtpManager::ProcessClassificationModelPredictions(
|
||||
const autofill::FormData& form,
|
||||
const base::flat_map<autofill::FieldGlobalId, autofill::FieldType>&
|
||||
field_predictions) {
|
||||
// TODO(415269545): Rationalize predictions and trigger OTP fetching if
|
||||
// needed.
|
||||
std::vector<autofill::FieldGlobalId> fillable_otp_fields(
|
||||
GetFillableOtpFieldIds(form, field_predictions));
|
||||
|
||||
client_->InformPasswordChangeServiceOfOtpPresent();
|
||||
autofill::FormGlobalId form_id(form.global_id());
|
||||
if (fillable_otp_fields.empty()) {
|
||||
form_managers_.erase(form_id);
|
||||
return;
|
||||
}
|
||||
|
||||
if (form_managers_.find(form_id) == form_managers_.end()) {
|
||||
form_managers_.emplace(form_id,
|
||||
OtpFormManager(form_id, fillable_otp_fields));
|
||||
client_->InformPasswordChangeServiceOfOtpPresent();
|
||||
|
||||
} else {
|
||||
form_managers_.at(form_id).ProcessUpdatedPredictions(fillable_otp_fields);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace password_manager
|
||||
|
@ -16,6 +16,7 @@ class FormData;
|
||||
|
||||
namespace password_manager {
|
||||
|
||||
class OtpFormManager;
|
||||
class PasswordManagerClient;
|
||||
|
||||
// A class in charge of handling one time passwords, one per tab.
|
||||
@ -23,15 +24,27 @@ class OtpManager {
|
||||
public:
|
||||
explicit OtpManager(PasswordManagerClient* client);
|
||||
|
||||
~OtpManager();
|
||||
|
||||
// Processes the classification model predictions received via Autofill.
|
||||
void ProcessClassificationModelPredictions(
|
||||
const autofill::FormData& form,
|
||||
const base::flat_map<autofill::FieldGlobalId, autofill::FieldType>&
|
||||
field_predictions);
|
||||
|
||||
#if defined(UNIT_TEST)
|
||||
const base::flat_map<autofill::FormGlobalId, OtpFormManager>& form_managers()
|
||||
const {
|
||||
return form_managers_;
|
||||
}
|
||||
#endif // defined(UNIT_TEST)
|
||||
|
||||
private:
|
||||
// The client that owns this class and is guaranteed to outlive it.
|
||||
const raw_ptr<PasswordManagerClient> client_;
|
||||
|
||||
// Managers managing individual forms.
|
||||
base::flat_map<autofill::FormGlobalId, OtpFormManager> form_managers_;
|
||||
};
|
||||
|
||||
} // namespace password_manager
|
||||
|
@ -0,0 +1,116 @@
|
||||
// Copyright 2025 The Chromium Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
#include "components/password_manager/core/browser/one_time_passwords/otp_manager.h"
|
||||
|
||||
#include "components/autofill/core/common/autofill_test_utils.h"
|
||||
#include "components/autofill/core/common/form_data.h"
|
||||
#include "components/password_manager/core/browser/one_time_passwords/otp_form_manager.h"
|
||||
#include "components/password_manager/core/browser/stub_password_manager_client.h"
|
||||
|
||||
namespace password_manager {
|
||||
|
||||
namespace {
|
||||
|
||||
using autofill::FormData;
|
||||
|
||||
class MockPasswordManagerClient : public StubPasswordManagerClient {
|
||||
public:
|
||||
MockPasswordManagerClient() = default;
|
||||
|
||||
MOCK_METHOD(void, InformPasswordChangeServiceOfOtpPresent, (), (override));
|
||||
};
|
||||
} // namespace
|
||||
|
||||
class OtpManagerTest : public testing::Test {
|
||||
public:
|
||||
OtpManagerTest() : otp_manager_(&mock_client_) {}
|
||||
|
||||
protected:
|
||||
MockPasswordManagerClient mock_client_;
|
||||
OtpManager otp_manager_;
|
||||
|
||||
private:
|
||||
autofill::test::AutofillUnitTestEnvironment autofill_environment_;
|
||||
};
|
||||
|
||||
TEST_F(OtpManagerTest, FormManagerCreatedForOtpForm) {
|
||||
FormData form;
|
||||
form.set_fields({autofill::test::CreateTestFormField(
|
||||
"some_label", "some_name", "some_value",
|
||||
autofill::FormControlType::kInputText)});
|
||||
|
||||
EXPECT_CALL(mock_client_, InformPasswordChangeServiceOfOtpPresent);
|
||||
otp_manager_.ProcessClassificationModelPredictions(
|
||||
form, {{form.fields()[0].global_id(), autofill::ONE_TIME_CODE}});
|
||||
|
||||
EXPECT_TRUE(otp_manager_.form_managers().contains(form.global_id()));
|
||||
std::vector<autofill::FieldGlobalId> expected_otp_field_ids = {
|
||||
form.fields()[0].global_id()};
|
||||
EXPECT_EQ(expected_otp_field_ids,
|
||||
otp_manager_.form_managers().at(form.global_id()).otp_field_ids());
|
||||
}
|
||||
|
||||
TEST_F(OtpManagerTest, FormManagerNotCreatedForNotFillableForm) {
|
||||
FormData form;
|
||||
form.set_fields({autofill::test::CreateTestFormField(
|
||||
"some_label", "some_name", "some_value",
|
||||
// Radio element cannot be filled with a text value, this prediction
|
||||
// should not be taken into account.
|
||||
autofill::FormControlType::kInputRadio)});
|
||||
|
||||
EXPECT_CALL(mock_client_, InformPasswordChangeServiceOfOtpPresent).Times(0);
|
||||
otp_manager_.ProcessClassificationModelPredictions(
|
||||
form, {{form.fields()[0].global_id(), autofill::ONE_TIME_CODE}});
|
||||
|
||||
EXPECT_FALSE(otp_manager_.form_managers().contains(form.global_id()));
|
||||
}
|
||||
|
||||
TEST_F(OtpManagerTest, ManagersUpdatedWhenPredictionsChange) {
|
||||
FormData form;
|
||||
form.set_fields({autofill::test::CreateTestFormField(
|
||||
"some_label1", "some_name1", "some_value1",
|
||||
autofill::FormControlType::kInputText),
|
||||
{autofill::test::CreateTestFormField(
|
||||
"some_label2", "some_name2", "some_value2",
|
||||
autofill::FormControlType::kInputPassword)}});
|
||||
|
||||
EXPECT_CALL(mock_client_, InformPasswordChangeServiceOfOtpPresent);
|
||||
otp_manager_.ProcessClassificationModelPredictions(
|
||||
form, {{form.fields()[0].global_id(), autofill::ONE_TIME_CODE},
|
||||
{form.fields()[1].global_id(), autofill::UNKNOWN_TYPE}});
|
||||
EXPECT_TRUE(otp_manager_.form_managers().contains(form.global_id()));
|
||||
|
||||
// Simulate receiving new predictions.
|
||||
// The client should not be notified the second time.
|
||||
EXPECT_CALL(mock_client_, InformPasswordChangeServiceOfOtpPresent).Times(0);
|
||||
otp_manager_.ProcessClassificationModelPredictions(
|
||||
form, {{form.fields()[0].global_id(), autofill::UNKNOWN_TYPE},
|
||||
{form.fields()[1].global_id(), autofill::ONE_TIME_CODE}});
|
||||
|
||||
EXPECT_TRUE(otp_manager_.form_managers().contains(form.global_id()));
|
||||
// Check that the manager reflects the latest predictions.
|
||||
std::vector<autofill::FieldGlobalId> expected_otp_field_ids = {
|
||||
form.fields()[1].global_id()};
|
||||
EXPECT_EQ(expected_otp_field_ids,
|
||||
otp_manager_.form_managers().at(form.global_id()).otp_field_ids());
|
||||
}
|
||||
|
||||
TEST_F(OtpManagerTest, FormManagerdDeletedWhenOtpFieldIsNoLongerParsedAsSuch) {
|
||||
FormData form;
|
||||
form.set_fields({autofill::test::CreateTestFormField(
|
||||
"some_label", "some_name", "some_value",
|
||||
autofill::FormControlType::kInputText)});
|
||||
|
||||
EXPECT_CALL(mock_client_, InformPasswordChangeServiceOfOtpPresent);
|
||||
otp_manager_.ProcessClassificationModelPredictions(
|
||||
form, {{form.fields()[0].global_id(), autofill::ONE_TIME_CODE}});
|
||||
EXPECT_TRUE(otp_manager_.form_managers().contains(form.global_id()));
|
||||
|
||||
otp_manager_.ProcessClassificationModelPredictions(
|
||||
form, {{form.fields()[0].global_id(), autofill::UNKNOWN_TYPE}});
|
||||
EXPECT_TRUE(otp_manager_.form_managers().empty());
|
||||
}
|
||||
|
||||
} // namespace password_manager
|
Reference in New Issue
Block a user