[OTP][PhishGuard] Do phish guard checks before showing OTPs.
Bug: 415273169
Change-Id: If02fabf80a9cfcf6542856dd72670ccec931ba5c
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7093137
Reviewed-by: Matthias Körber <koerber@google.com>
Reviewed-by: Jan Keitel <jkeitel@google.com>
Commit-Queue: Luchen Peng <luchenpeng@google.com>
Cr-Commit-Position: refs/heads/main@{#1537847}
diff --git a/chrome/browser/ui/autofill/BUILD.gn b/chrome/browser/ui/autofill/BUILD.gn
index 73dd223..67a7c6e 100644
--- a/chrome/browser/ui/autofill/BUILD.gn
+++ b/chrome/browser/ui/autofill/BUILD.gn
@@ -127,6 +127,8 @@
"autofill_suggestion_controller.cc",
"autofill_suggestion_controller_utils.cc",
"chrome_autofill_client.cc",
+ "chrome_otp_phish_guard_delegate.cc",
+ "chrome_otp_phish_guard_delegate.h",
"next_idle_barrier.cc",
"popup_controller_common.cc",
"risk_util.cc",
@@ -169,6 +171,7 @@
"//components/password_manager/core/browser:password_form",
"//components/password_manager/core/browser/form_parsing",
"//components/profile_metrics",
+ "//components/safe_browsing/content/browser/password_protection",
"//components/security_state/content",
"//components/strings:components_strings_grit",
"//components/unified_consent",
diff --git a/chrome/browser/ui/autofill/chrome_autofill_client.cc b/chrome/browser/ui/autofill/chrome_autofill_client.cc
index 0acea8c..0873d82 100644
--- a/chrome/browser/ui/autofill/chrome_autofill_client.cc
+++ b/chrome/browser/ui/autofill/chrome_autofill_client.cc
@@ -57,6 +57,7 @@
#include "chrome/browser/translate/chrome_translate_client.h"
#include "chrome/browser/ui/autofill/address_bubbles_controller.h"
#include "chrome/browser/ui/autofill/autofill_suggestion_controller.h"
+#include "chrome/browser/ui/autofill/chrome_otp_phish_guard_delegate.h"
#include "chrome/browser/ui/autofill/edit_address_profile_dialog_controller_impl.h"
#include "chrome/browser/ui/autofill/payments/chrome_payments_autofill_client.h"
#include "chrome/browser/ui/autofill/payments/credit_card_scanner_controller.h"
@@ -1261,7 +1262,9 @@
ablation_study_(g_browser_process->local_state()),
identity_credential_delegate_(web_contents),
otp_field_detector_(std::make_unique<OtpFieldDetector>(this)),
- email_verifier_delegate_(std::make_unique<EmailVerifierDelegate>(this)) {
+ email_verifier_delegate_(std::make_unique<EmailVerifierDelegate>(this)),
+ otp_phish_guard_delegate_(
+ std::make_unique<ChromeOtpPhishGuardDelegate>(web_contents)) {
// Initialize StrikeDatabase so its cache will be loaded and ready to use
// when requested by other Autofill classes.
GetStrikeDatabase();
@@ -1352,6 +1355,16 @@
return otp_field_detector_.get();
}
+OtpPhishGuardDelegate* ChromeAutofillClient::GetOtpPhishGuardDelegate() {
+#if BUILDFLAG(IS_ANDROID)
+ if (base::FeatureList::IsEnabled(
+ password_manager::features::kOtpPhishGuard)) {
+ return otp_phish_guard_delegate_.get();
+ }
+#endif // BUILDFLAG(IS_ANDROID)
+ return nullptr;
+}
+
one_time_tokens::OneTimeTokenService*
ChromeAutofillClient::GetOneTimeTokenService() const {
#if BUILDFLAG(IS_ANDROID)
diff --git a/chrome/browser/ui/autofill/chrome_autofill_client.h b/chrome/browser/ui/autofill/chrome_autofill_client.h
index dcca7871..23ca31dd 100644
--- a/chrome/browser/ui/autofill/chrome_autofill_client.h
+++ b/chrome/browser/ui/autofill/chrome_autofill_client.h
@@ -66,6 +66,7 @@
class EmailVerifierDelegate;
class FormFieldData;
class OtpFieldDetector;
+class ChromeOtpPhishGuardDelegate;
class LogRouter;
enum class SuggestionType;
@@ -281,6 +282,7 @@
override;
OtpFieldDetector* GetOtpFieldDetector() override;
+ OtpPhishGuardDelegate* GetOtpPhishGuardDelegate() override;
one_time_tokens::OneTimeTokenService* GetOneTimeTokenService() const final;
@@ -343,6 +345,7 @@
ContentIdentityCredentialDelegate identity_credential_delegate_;
std::unique_ptr<OtpFieldDetector> otp_field_detector_;
std::unique_ptr<EmailVerifierDelegate> email_verifier_delegate_;
+ std::unique_ptr<ChromeOtpPhishGuardDelegate> otp_phish_guard_delegate_;
base::WeakPtrFactory<ChromeAutofillClient> weak_ptr_factory_{this};
};
diff --git a/chrome/browser/ui/autofill/chrome_otp_phish_guard_delegate.cc b/chrome/browser/ui/autofill/chrome_otp_phish_guard_delegate.cc
new file mode 100644
index 0000000..78290f0
--- /dev/null
+++ b/chrome/browser/ui/autofill/chrome_otp_phish_guard_delegate.cc
@@ -0,0 +1,35 @@
+// 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 "chrome/browser/ui/autofill/chrome_otp_phish_guard_delegate.h"
+
+#include "chrome/browser/password_manager/chrome_password_manager_client.h"
+#include "chrome/browser/profiles/profile.h"
+#include "components/safe_browsing/content/browser/password_protection/password_protection_service.h"
+#include "content/public/browser/web_contents.h"
+
+namespace autofill {
+
+ChromeOtpPhishGuardDelegate::ChromeOtpPhishGuardDelegate(
+ content::WebContents* web_contents)
+ : web_contents_(CHECK_DEREF(web_contents)) {}
+
+ChromeOtpPhishGuardDelegate::~ChromeOtpPhishGuardDelegate() = default;
+
+void ChromeOtpPhishGuardDelegate::StartOtpPhishGuardCheck(
+ const GURL& url,
+ base::OnceCallback<void(bool)> callback) {
+ if (auto* client =
+ ChromePasswordManagerClient::FromWebContents(&web_contents_.get())) {
+ if (safe_browsing::PasswordProtectionService* pps =
+ client->GetPasswordProtectionService()) {
+ pps->MaybeStartOtpPhishingRequest(&web_contents_.get(), url,
+ std::move(callback));
+ return;
+ }
+ }
+ std::move(callback).Run(false);
+}
+
+} // namespace autofill
diff --git a/chrome/browser/ui/autofill/chrome_otp_phish_guard_delegate.h b/chrome/browser/ui/autofill/chrome_otp_phish_guard_delegate.h
new file mode 100644
index 0000000..e36998a
--- /dev/null
+++ b/chrome/browser/ui/autofill/chrome_otp_phish_guard_delegate.h
@@ -0,0 +1,34 @@
+// 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 CHROME_BROWSER_UI_AUTOFILL_CHROME_OTP_PHISH_GUARD_DELEGATE_H_
+#define CHROME_BROWSER_UI_AUTOFILL_CHROME_OTP_PHISH_GUARD_DELEGATE_H_
+
+#include "base/check_deref.h"
+#include "base/memory/raw_ref.h"
+#include "components/autofill/core/browser/integrators/one_time_tokens/otp_phish_guard_delegate.h"
+
+namespace content {
+class WebContents;
+}
+
+namespace autofill {
+
+class ChromeOtpPhishGuardDelegate : public OtpPhishGuardDelegate {
+ public:
+ explicit ChromeOtpPhishGuardDelegate(content::WebContents* web_contents);
+ ~ChromeOtpPhishGuardDelegate() override;
+
+ // OtpPhishGuardDelegate:
+ void StartOtpPhishGuardCheck(
+ const GURL& url,
+ base::OnceCallback<void(bool)> callback) override;
+
+ private:
+ const raw_ref<content::WebContents> web_contents_;
+};
+
+} // namespace autofill
+
+#endif // CHROME_BROWSER_UI_AUTOFILL_CHROME_OTP_PHISH_GUARD_DELEGATE_H_
diff --git a/components/autofill/core/browser/BUILD.gn b/components/autofill/core/browser/BUILD.gn
index 9c49647..cfaa826a 100644
--- a/components/autofill/core/browser/BUILD.gn
+++ b/components/autofill/core/browser/BUILD.gn
@@ -414,6 +414,7 @@
"integrators/one_time_tokens/otp_manager.h",
"integrators/one_time_tokens/otp_manager_impl.cc",
"integrators/one_time_tokens/otp_manager_impl.h",
+ "integrators/one_time_tokens/otp_phish_guard_delegate.h",
"integrators/one_time_tokens/otp_suggestion.cc",
"integrators/one_time_tokens/otp_suggestion.h",
"integrators/optimization_guide/autofill_optimization_guide_decider.cc",
diff --git a/components/autofill/core/browser/foundations/autofill_client.cc b/components/autofill/core/browser/foundations/autofill_client.cc
index 70ea6bd4..27ef79a 100644
--- a/components/autofill/core/browser/foundations/autofill_client.cc
+++ b/components/autofill/core/browser/foundations/autofill_client.cc
@@ -339,4 +339,8 @@
return nullptr;
}
+OtpPhishGuardDelegate* AutofillClient::GetOtpPhishGuardDelegate() {
+ return nullptr;
+}
+
} // namespace autofill
diff --git a/components/autofill/core/browser/foundations/autofill_client.h b/components/autofill/core/browser/foundations/autofill_client.h
index 3dfcb5d..e70e75c1 100644
--- a/components/autofill/core/browser/foundations/autofill_client.h
+++ b/components/autofill/core/browser/foundations/autofill_client.h
@@ -115,6 +115,7 @@
struct FormInteractionsFlowId;
class LogManager;
class OtpFieldDetector;
+class OtpPhishGuardDelegate;
struct PasswordFormClassification;
class PasswordManagerDelegate;
class PersonalDataManager;
@@ -751,6 +752,10 @@
// May return null on platforms where OTPs are not supported.
virtual OtpFieldDetector* GetOtpFieldDetector();
+ // Returns the delegate for OTP phish guard, which can be used to perform
+ // security checks before offering an OTP. May return nullptr.
+ virtual OtpPhishGuardDelegate* GetOtpPhishGuardDelegate();
+
// May return null on platforms where no OneTimeTokenService is supported.
virtual one_time_tokens::OneTimeTokenService* GetOneTimeTokenService() const;
diff --git a/components/autofill/core/browser/foundations/test_autofill_client.h b/components/autofill/core/browser/foundations/test_autofill_client.h
index 4052e98..603baf4 100644
--- a/components/autofill/core/browser/foundations/test_autofill_client.h
+++ b/components/autofill/core/browser/foundations/test_autofill_client.h
@@ -38,6 +38,7 @@
#include "components/autofill/core/browser/integrators/autofill_ai/mock_autofill_ai_manager.h"
#include "components/autofill/core/browser/integrators/fast_checkout/mock_fast_checkout_client.h"
#include "components/autofill/core/browser/integrators/identity_credential/identity_credential_delegate.h"
+#include "components/autofill/core/browser/integrators/one_time_tokens/otp_phish_guard_delegate.h"
#include "components/autofill/core/browser/integrators/optimization_guide/mock_autofill_optimization_guide_decider.h"
#include "components/autofill/core/browser/integrators/password_manager/password_manager_delegate.h"
#include "components/autofill/core/browser/integrators/plus_addresses/autofill_plus_address_delegate.h"
@@ -457,6 +458,15 @@
bool IsLastQueriedField(FieldGlobalId field_id) override { return true; }
#endif
+ OtpPhishGuardDelegate* GetOtpPhishGuardDelegate() override {
+ return otp_phish_guard_delegate_.get();
+ }
+
+ void set_otp_phish_guard_delegate(
+ std::unique_ptr<OtpPhishGuardDelegate> otp_phish_guard_delegate) {
+ otp_phish_guard_delegate_ = std::move(otp_phish_guard_delegate);
+ }
+
void set_test_addresses(
std::vector<AutofillProfile> test_addresses) override {
for (AutofillProfile& profile : test_addresses) {
@@ -681,6 +691,7 @@
ukm::TestAutoSetUkmRecorder test_ukm_recorder_;
signin::IdentityTestEnvironment identity_test_env_;
raw_ptr<syncer::SyncService> test_sync_service_ = nullptr;
+ std::unique_ptr<OtpPhishGuardDelegate> otp_phish_guard_delegate_;
std::unique_ptr<AutofillPlusAddressDelegate> plus_address_delegate_;
std::unique_ptr<IdentityCredentialDelegate> identity_credential_delegate_;
std::unique_ptr<PasswordManagerDelegate> password_manager_delegate_;
diff --git a/components/autofill/core/browser/integrators/one_time_tokens/otp_manager_impl.cc b/components/autofill/core/browser/integrators/one_time_tokens/otp_manager_impl.cc
index 0411071..9936c5b 100644
--- a/components/autofill/core/browser/integrators/one_time_tokens/otp_manager_impl.cc
+++ b/components/autofill/core/browser/integrators/one_time_tokens/otp_manager_impl.cc
@@ -10,6 +10,7 @@
#include "components/autofill/core/browser/autofill_field.h"
#include "components/autofill/core/browser/form_structure.h"
#include "components/autofill/core/browser/foundations/browser_autofill_manager.h"
+#include "components/autofill/core/browser/integrators/one_time_tokens/otp_phish_guard_delegate.h"
#include "components/password_manager/core/browser/features/password_features.h"
using one_time_tokens::ExpiringSubscriptionHandle;
@@ -100,6 +101,7 @@
std::variant<OneTimeToken, OneTimeTokenRetrievalError> token_or_error) {
// TODO(crbug.com/415272524): Record metrics on how often the retrieval
// succeeds or fails, in combination with the OTP source.
+ // If token_or_error holds an error, run the callback with empty otp value.
if (std::holds_alternative<OneTimeTokenRetrievalError>(token_or_error)) {
if (!last_pending_get_suggestions_callback_.is_null()) {
std::move(last_pending_get_suggestions_callback_).Run({});
@@ -107,23 +109,46 @@
return;
}
- const OneTimeToken& token = std::get<OneTimeToken>(token_or_error);
+ // If we are here, token_or_error holds a OneTimeToken, we check if the
+ // callback is valid.
+ if (!last_pending_get_suggestions_callback_) {
+ return;
+ }
+
+ OneTimeToken& token = std::get<OneTimeToken>(token_or_error);
+
+ // We run PhishGuard check to make sure OTPs are not shown to users on
+ // potential phishing sites.
+ if (OtpPhishGuardDelegate* delegate =
+ owner_->client().GetOtpPhishGuardDelegate()) {
+ delegate->StartOtpPhishGuardCheck(
+ owner_->client().GetLastCommittedPrimaryMainFrameURL(),
+ base::BindOnce(&OtpManagerImpl::MaybeShowOtpSuggestions,
+ weak_ptr_factory_.GetWeakPtr(), std::move(token)));
+ } else {
+ MaybeShowOtpSuggestions(std::move(token), /*is_phishing_site=*/false);
+ }
+}
+
+void OtpManagerImpl::MaybeShowOtpSuggestions(OneTimeToken token,
+ bool is_phishing_site) {
+ if (!last_pending_get_suggestions_callback_) {
+ return;
+ }
std::vector<std::string> suggestions;
if (!token.value().empty()) {
- suggestions.emplace_back(token.value());
+ suggestions.emplace_back(std::move(token).value());
}
- if (IsOtpDeliveryBlocked()) {
+ if (IsOtpDeliveryBlocked() || is_phishing_site) {
suggestions.clear();
}
- if (!last_pending_get_suggestions_callback_.is_null()) {
- if (owner_->GetMetricState().has_value()) {
- owner_->GetMetricState()->otp_form_event_logger.OnOtpAvailable();
- }
- std::move(last_pending_get_suggestions_callback_).Run(suggestions);
+ if (owner_->GetMetricState().has_value()) {
+ owner_->GetMetricState()->otp_form_event_logger.OnOtpAvailable();
}
+ std::move(last_pending_get_suggestions_callback_).Run(std::move(suggestions));
}
bool OtpManagerImpl::IsOtpDeliveryBlocked() {
diff --git a/components/autofill/core/browser/integrators/one_time_tokens/otp_manager_impl.h b/components/autofill/core/browser/integrators/one_time_tokens/otp_manager_impl.h
index f966eb7f..9a36064 100644
--- a/components/autofill/core/browser/integrators/one_time_tokens/otp_manager_impl.h
+++ b/components/autofill/core/browser/integrators/one_time_tokens/otp_manager_impl.h
@@ -61,6 +61,10 @@
std::variant<one_time_tokens::OneTimeToken,
one_time_tokens::OneTimeTokenRetrievalError>);
+ // Callback for `OtpPhishGuardDelegate::StartOtpPhishGuardCheck`.
+ void MaybeShowOtpSuggestions(one_time_tokens::OneTimeToken token,
+ bool is_phishing_site);
+
// Returns true if an OTP must not be delivered to the caller in an autofill
// context, e.g., because the page called the WebOTP API.
bool IsOtpDeliveryBlocked();
@@ -80,6 +84,10 @@
// call to `GetOtpSuggestions()` invalidates the previous call.
GetOtpSuggestionsCallback last_pending_get_suggestions_callback_;
+ // The last received OTP. This is used to store the OTP between the phishing
+ // check and the actual display of the suggestions.
+ std::optional<one_time_tokens::OneTimeToken> last_received_otp_;
+
base::ScopedObservation<BrowserAutofillManager, AutofillManager::Observer>
autofill_manager_observation_{this};
diff --git a/components/autofill/core/browser/integrators/one_time_tokens/otp_manager_impl_unittest.cc b/components/autofill/core/browser/integrators/one_time_tokens/otp_manager_impl_unittest.cc
index 3eb2c79..163feba 100644
--- a/components/autofill/core/browser/integrators/one_time_tokens/otp_manager_impl_unittest.cc
+++ b/components/autofill/core/browser/integrators/one_time_tokens/otp_manager_impl_unittest.cc
@@ -27,7 +27,7 @@
using autofill::test::GetServerTypes;
using base::test::RunOnceCallback;
using one_time_tokens::OneTimeTokenServiceImpl;
-
+using ::testing::_;
namespace autofill {
namespace {
@@ -40,6 +40,14 @@
(override));
};
+class MockOtpPhishGuardDelegate : public OtpPhishGuardDelegate {
+ public:
+ MOCK_METHOD(void,
+ StartOtpPhishGuardCheck,
+ (const GURL&, base::OnceCallback<void(bool)>),
+ (override));
+};
+
} // namespace
class OtpManagerImplTest : public testing::Test,
@@ -50,6 +58,10 @@
void SetUp() override {
InitAutofillClient();
+ auto otp_phish_guard_delegate =
+ std::make_unique<MockOtpPhishGuardDelegate>();
+ autofill_client().set_otp_phish_guard_delegate(
+ std::move(otp_phish_guard_delegate));
CreateAutofillDriver();
}
@@ -95,6 +107,11 @@
AddForm(form_description);
}
+ MockOtpPhishGuardDelegate& otp_phish_guard_delegate() {
+ return static_cast<MockOtpPhishGuardDelegate&>(
+ *autofill_client().GetOtpPhishGuardDelegate());
+ }
+
protected:
base::test::TaskEnvironment task_environment_{
base::test::TaskEnvironment::TimeSource::MOCK_TIME};
@@ -132,7 +149,9 @@
// Prepare the handling of SMS requests from the SMS backend.
one_time_tokens::OtpFetchReply reply = GetDefaultOtpFetchReply();
EXPECT_CALL(sms_otp_backend_, RetrieveSmsOtp)
- .WillOnce(RunOnceCallback<0>(GetDefaultOtpFetchReply()));
+ .WillOnce(RunOnceCallback<0>(reply));
+ EXPECT_CALL(otp_phish_guard_delegate(), StartOtpPhishGuardCheck)
+ .WillOnce(RunOnceCallback<1>(false));
// Observing an OTP field is supposed to trigger an SMS OTP request.
AddFormWithOtpField();
@@ -157,6 +176,8 @@
.WillOnce(
[&](base::OnceCallback<void(const one_time_tokens::OtpFetchReply&)>
callback) { sms_backend_callback = std::move(callback); });
+ EXPECT_CALL(otp_phish_guard_delegate(), StartOtpPhishGuardCheck)
+ .WillOnce(RunOnceCallback<1>(false));
// Observing an OTP field is supposed to trigger an SMS OTP request.
AddFormWithOtpField();
@@ -181,10 +202,12 @@
TEST_F(OtpManagerImplTest, GetOtpSuggestions_FetchesSmsOnlyOnce) {
OtpManagerImpl otp_manager(autofill_manager(), &one_time_token_service_);
- // Prepare the handling of SMS requests from the SMS backend.
one_time_tokens::OtpFetchReply reply = GetDefaultOtpFetchReply();
EXPECT_CALL(sms_otp_backend_, RetrieveSmsOtp)
.WillOnce(RunOnceCallback<0>(GetDefaultOtpFetchReply()));
+ EXPECT_CALL(otp_phish_guard_delegate(), StartOtpPhishGuardCheck)
+ .WillOnce(RunOnceCallback<1>(false))
+ .WillOnce(RunOnceCallback<1>(false));
// Observing an OTP field is supposed to trigger an SMS OTP request.
AddFormWithOtpField();
@@ -220,6 +243,8 @@
.WillOnce(
[&](base::OnceCallback<void(const one_time_tokens::OtpFetchReply&)>
callback) { sms_backend_callback = std::move(callback); });
+ EXPECT_CALL(otp_phish_guard_delegate(), StartOtpPhishGuardCheck)
+ .WillOnce(RunOnceCallback<1>(false));
// Observing an OTP field is supposed to trigger an SMS OTP request.
AddFormWithOtpField();
@@ -262,6 +287,8 @@
EXPECT_CALL(sms_otp_backend_, RetrieveSmsOtp)
.WillOnce(RunOnceCallback<0>(reply));
+ EXPECT_CALL(otp_phish_guard_delegate(), StartOtpPhishGuardCheck)
+ .WillOnce(RunOnceCallback<1>(false));
// Observing an OTP field is supposed to trigger an SMS OTP request.
AddFormWithOtpField();
@@ -288,6 +315,8 @@
.WillOnce(
[&](base::OnceCallback<void(const one_time_tokens::OtpFetchReply&)>
callback) { sms_backend_callback = std::move(callback); });
+ EXPECT_CALL(otp_phish_guard_delegate(), StartOtpPhishGuardCheck)
+ .WillOnce(RunOnceCallback<1>(false));
// Observing an OTP field is supposed to trigger an SMS OTP request.
AddFormWithOtpField();
@@ -314,4 +343,45 @@
EXPECT_FALSE(future2.IsReady());
}
+// Tests that no suggestions are returned if the phishing check returns true.
+TEST_F(OtpManagerImplTest, GetOtpSuggestions_PhishingCheckReturnsTrue) {
+ OtpManagerImpl otp_manager(autofill_manager(), &one_time_token_service_);
+
+ // Prepare the handling of SMS requests from the SMS backend.
+ one_time_tokens::OtpFetchReply reply = GetDefaultOtpFetchReply();
+ EXPECT_CALL(sms_otp_backend_, RetrieveSmsOtp)
+ .WillOnce(RunOnceCallback<0>(reply));
+ EXPECT_CALL(otp_phish_guard_delegate(), StartOtpPhishGuardCheck)
+ .WillOnce(RunOnceCallback<1>(true)); // Phishing detected
+
+ // Observing an OTP field is supposed to trigger an SMS OTP request.
+ AddFormWithOtpField();
+
+ base::test::TestFuture<const std::vector<std::string>> future;
+ otp_manager.GetOtpSuggestions(future.GetCallback());
+
+ EXPECT_TRUE(future.Get().empty());
+}
+
+// Tests that suggestions are returned if the phishing check returns false.
+TEST_F(OtpManagerImplTest, GetOtpSuggestions_PhishingCheckReturnsFalse) {
+ OtpManagerImpl otp_manager(autofill_manager(), &one_time_token_service_);
+
+ // Prepare the handling of SMS requests from the SMS backend.
+ one_time_tokens::OtpFetchReply reply = GetDefaultOtpFetchReply();
+ EXPECT_CALL(sms_otp_backend_, RetrieveSmsOtp)
+ .WillOnce(RunOnceCallback<0>(reply));
+ EXPECT_CALL(otp_phish_guard_delegate(), StartOtpPhishGuardCheck)
+ .WillOnce(RunOnceCallback<1>(false)); // No phishing
+
+ // Observing an OTP field is supposed to trigger an SMS OTP request.
+ AddFormWithOtpField();
+
+ base::test::TestFuture<const std::vector<std::string>> future;
+ otp_manager.GetOtpSuggestions(future.GetCallback());
+
+ ASSERT_EQ(future.Get().size(), 1u);
+ EXPECT_EQ(future.Get()[0], reply.otp_value->value());
+}
+
} // namespace autofill
diff --git a/components/autofill/core/browser/integrators/one_time_tokens/otp_phish_guard_delegate.h b/components/autofill/core/browser/integrators/one_time_tokens/otp_phish_guard_delegate.h
new file mode 100644
index 0000000..2d28255
--- /dev/null
+++ b/components/autofill/core/browser/integrators/one_time_tokens/otp_phish_guard_delegate.h
@@ -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.
+
+#ifndef COMPONENTS_AUTOFILL_CORE_BROWSER_INTEGRATORS_ONE_TIME_TOKENS_OTP_PHISH_GUARD_DELEGATE_H_
+#define COMPONENTS_AUTOFILL_CORE_BROWSER_INTEGRATORS_ONE_TIME_TOKENS_OTP_PHISH_GUARD_DELEGATE_H_
+
+#include "base/functional/callback.h"
+#include "url/gurl.h"
+
+namespace autofill {
+
+// A delegate for checking if a given URL is a phishing site before unmasking
+// an OTP.
+class OtpPhishGuardDelegate {
+ public:
+ virtual ~OtpPhishGuardDelegate() = default;
+
+ // Checks if the given `url` is a phishing site. `callback` is run with the
+ // result.
+ virtual void StartOtpPhishGuardCheck(
+ const GURL& url,
+ base::OnceCallback<void(bool)> callback) = 0;
+};
+
+} // namespace autofill
+
+#endif // COMPONENTS_AUTOFILL_CORE_BROWSER_INTEGRATORS_ONE_TIME_TOKENS_OTP_PHISH_GUARD_DELEGATE_H_