[DBSC] Add API to prototype to stop cookies rotations

It adds `BoundSessionCookieController::StopCookiesRotation` to stop
cookies rotation for the given controller and
`BoundSessionCookieRefreshService::StopCookiesRotation` to stop cookies
rotations for a given session.

This API will be used by `BoundSessionOAuthMultiLoginDelegate` to stop
cookies rotations until the cookies are set (to avoid undesired race
conditions) - implemented in next CL(s).

Bug: 312719798
Change-Id: I13caaa145f44d00e7cd11077e350551ccddb4c3b
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6832201
Reviewed-by: Alex Ilin <alexilin@chromium.org>
Commit-Queue: Ernest Nguyen Hung <ernn@google.com>
Cr-Commit-Position: refs/heads/main@{#1503833}
diff --git a/chrome/browser/signin/bound_session_credentials/bound_session_cookie_controller.h b/chrome/browser/signin/bound_session_credentials/bound_session_cookie_controller.h
index 00284ad..891c34c 100644
--- a/chrome/browser/signin/bound_session_credentials/bound_session_cookie_controller.h
+++ b/chrome/browser/signin/bound_session_credentials/bound_session_cookie_controller.h
@@ -50,6 +50,12 @@
     // cookie expiration date changes. Cookie deletion is considered as a change
     // in the expiration date to the null time.
     virtual void OnBoundSessionThrottlerParamsChanged() = 0;
+
+    // Called when the cookie rotation has been stopped for more than a timeout
+    // period. `BoundSessionCookieController` is expected to be deleted after
+    // this call.
+    virtual void OnCookieRotationStoppedTimeout(
+        BoundSessionCookieController* controller) = 0;
   };
 
   BoundSessionCookieController(
@@ -68,6 +74,17 @@
       chrome::mojom::BoundSessionRequestThrottledHandler::
           HandleRequestBlockedOnCookieCallback resume_blocked_request) = 0;
 
+  // Stops the cookie rotation for the given session.
+  //
+  // Once the cookie rotation is stopped, all throttled requests will remain
+  // throttled until the session is terminated. This is used by OAML to ensure
+  // all requests are throttled until the returned cookies are set.
+  //
+  // The session will be terminated after a timeout if it has not been
+  // terminated explicitly. This is a safety net to ensure the session is
+  // eventually terminated even if OAML fails to terminate the session.
+  virtual void StopCookieRotation() = 0;
+
   // URL determining the scope of the bound session. All requests that are
   // within the scope are subject to throttling.
   const GURL& scope_url() const { return scope_url_; }
diff --git a/chrome/browser/signin/bound_session_credentials/bound_session_cookie_controller_impl.cc b/chrome/browser/signin/bound_session_credentials/bound_session_cookie_controller_impl.cc
index b639e4b..458e4135 100644
--- a/chrome/browser/signin/bound_session_credentials/bound_session_cookie_controller_impl.cc
+++ b/chrome/browser/signin/bound_session_credentials/bound_session_cookie_controller_impl.cc
@@ -23,6 +23,7 @@
 #include "chrome/browser/signin/bound_session_credentials/rotation_debug_info.pb.h"
 #include "chrome/browser/signin/bound_session_credentials/session_binding_helper.h"
 #include "chrome/common/renderer_configuration.mojom.h"
+#include "components/signin/public/base/signin_switches.h"
 #include "content/public/browser/storage_partition.h"
 #include "net/base/backoff_entry.h"
 #include "services/network/public/cpp/network_connection_tracker.h"
@@ -62,6 +63,8 @@
     false,
 };
 
+constexpr base::TimeDelta kResumeBlockedRequestsTimeout = base::Seconds(20);
+
 void RecordNumberOfSuccessiveTimeoutIfAny(size_t successive_timeout) {
   if (successive_timeout == 0) {
     return;
@@ -209,24 +212,36 @@
   resume_blocked_requests_.push_back(std::move(resume_blocked_request));
   MaybeRefreshCookie(
       BoundSessionRefreshCookieFetcher::Trigger::kBlockedRequest);
+  MaybeStartResumeBlockedRequestsTimer();
+}
 
-  if (!resume_blocked_requests_timer_.IsRunning() &&
-      !resume_blocked_requests_.empty()) {
-    // Ensure all blocked requests are released after a timeout.
-    // `base::Unretained(this)` is safe because `this` owns
-    // `resume_blocked_requests_timer_`.
-    const base::TimeDelta kResumeBlockedRequestTimeout = base::Seconds(20);
-    resume_blocked_requests_timer_.Start(
-        FROM_HERE, kResumeBlockedRequestTimeout,
-        base::BindRepeating(
-            &BoundSessionCookieControllerImpl::OnResumeBlockedRequestsTimeout,
-            base::Unretained(this)));
-  }
+void BoundSessionCookieControllerImpl::StopCookieRotation() {
+  CHECK(base::FeatureList::IsEnabled(
+      switches::kEnableOAuthMultiloginCookiesBinding));
+  rotation_stopped_ = true;
+  // Stop the cookie rotation.
+  cookie_refresh_timer_.Stop();
+  // Cancel any pending rotation request.
+  refresh_cookie_fetcher_.reset();
+  // Make sure that all blocked requests remain blocked until the session is
+  // terminated.
+  resume_blocked_requests_timer_.Stop();
+  // Start a timer to make sure the session is eventually terminated (reusing
+  // the same timeout period as for resuming blocked requests).
+  //
+  // `base::Unretained` is safe because:
+  // - it is guaranteed that `delegate_` outlives `this`,
+  // - `this` owns `rotation_stopped_timer_`.
+  rotation_stopped_timer_.Start(
+      FROM_HERE, kResumeBlockedRequestsTimeout,
+      base::BindOnce(&BoundSessionCookieController::Delegate::
+                         OnCookieRotationStoppedTimeout,
+                     base::Unretained(delegate_), base::Unretained(this)));
 }
 
 bool BoundSessionCookieControllerImpl::ShouldPauseThrottlingRequests() const {
-  return refresh_cookie_fetcher_backoff_.failure_count() >
-         kNumberOfErrorsToIgnoreForBackoff;
+  return !rotation_stopped_ && refresh_cookie_fetcher_backoff_.failure_count() >
+                                   kNumberOfErrorsToIgnoreForBackoff;
 }
 
 bound_session_credentials::RotationDebugInfo
@@ -302,7 +317,7 @@
 }
 
 bool BoundSessionCookieControllerImpl::CanCreateRefreshCookieFetcher() const {
-  return !refresh_cookie_fetcher_ &&
+  return !rotation_stopped_ && !refresh_cookie_fetcher_ &&
          !refresh_cookie_fetcher_backoff_.ShouldRejectRequest();
 }
 
@@ -464,6 +479,9 @@
 
 void BoundSessionCookieControllerImpl::MaybeScheduleCookieRotation(
     BoundSessionRefreshCookieFetcher::Trigger trigger) {
+  if (rotation_stopped_) {
+    return;
+  }
   const base::TimeDelta kCookieRefreshInterval = base::Minutes(2);
   base::TimeDelta preemptive_refresh_in =
       min_cookie_expiration_time() - base::Time::Now() - kCookieRefreshInterval;
@@ -494,6 +512,21 @@
                           base::Unretained(this), trigger));
 }
 
+void BoundSessionCookieControllerImpl::MaybeStartResumeBlockedRequestsTimer() {
+  if (rotation_stopped_ || resume_blocked_requests_timer_.IsRunning() ||
+      resume_blocked_requests_.empty()) {
+    return;
+  }
+  // Ensure all blocked requests are released after a timeout.
+  // `base::Unretained(this)` is safe because `this` owns
+  // `resume_blocked_requests_timer_`.
+  resume_blocked_requests_timer_.Start(
+      FROM_HERE, kResumeBlockedRequestsTimeout,
+      base::BindRepeating(
+          &BoundSessionCookieControllerImpl::OnResumeBlockedRequestsTimeout,
+          base::Unretained(this)));
+}
+
 void BoundSessionCookieControllerImpl::ResumeBlockedRequests(
     ResumeBlockedRequestsTrigger trigger) {
   resume_blocked_requests_timer_.Stop();
diff --git a/chrome/browser/signin/bound_session_credentials/bound_session_cookie_controller_impl.h b/chrome/browser/signin/bound_session_credentials/bound_session_cookie_controller_impl.h
index c91b5ba..5576326 100644
--- a/chrome/browser/signin/bound_session_credentials/bound_session_cookie_controller_impl.h
+++ b/chrome/browser/signin/bound_session_credentials/bound_session_cookie_controller_impl.h
@@ -56,6 +56,7 @@
   void HandleRequestBlockedOnCookie(
       chrome::mojom::BoundSessionRequestThrottledHandler::
           HandleRequestBlockedOnCookieCallback resume_blocked_request) override;
+  void StopCookieRotation() override;
   bool ShouldPauseThrottlingRequests() const override;
   bound_session_credentials::RotationDebugInfo TakeDebugInfo() override;
 
@@ -91,6 +92,7 @@
   void ResetCookieFetcherBackoff();
   void MaybeScheduleCookieRotation(
       BoundSessionRefreshCookieFetcher::Trigger trigger);
+  void MaybeStartResumeBlockedRequestsTimer();
   void ResumeBlockedRequests(
       chrome::mojom::ResumeBlockedRequestsTrigger trigger);
   void OnResumeBlockedRequestsTimeout();
@@ -140,6 +142,15 @@
   base::OneShotTimer resume_blocked_requests_timer_;
   size_t successive_timeout_ = 0;
 
+  // Once set to `true`, the cookies rotation is stopped and all requests are
+  // throttled until the session is terminated (i.e. this field is never flipped
+  // back to `false`).
+  bool rotation_stopped_ = false;
+
+  // Used to call `OnCookieRotationStoppedTimeout` after the session has been
+  // stopped for more than a timeout threshold.
+  base::OneShotTimer rotation_stopped_timer_;
+
   RefreshCookieFetcherFactoryForTesting
       refresh_cookie_fetcher_factory_for_testing_;
 
diff --git a/chrome/browser/signin/bound_session_credentials/bound_session_cookie_controller_impl_unittest.cc b/chrome/browser/signin/bound_session_credentials/bound_session_cookie_controller_impl_unittest.cc
index 7489dea..27d69c2 100644
--- a/chrome/browser/signin/bound_session_credentials/bound_session_cookie_controller_impl_unittest.cc
+++ b/chrome/browser/signin/bound_session_credentials/bound_session_cookie_controller_impl_unittest.cc
@@ -12,6 +12,7 @@
 #include "base/functional/callback_helpers.h"
 #include "base/test/metrics/histogram_tester.h"
 #include "base/test/protobuf_matchers.h"
+#include "base/test/scoped_feature_list.h"
 #include "base/test/task_environment.h"
 #include "base/test/test_future.h"
 #include "base/time/time.h"
@@ -26,6 +27,7 @@
 #include "chrome/browser/signin/bound_session_credentials/rotation_debug_info.pb.h"
 #include "chrome/browser/signin/bound_session_credentials/session_binding_helper.h"
 #include "chrome/common/renderer_configuration.mojom-shared.h"
+#include "components/signin/public/base/signin_switches.h"
 #include "components/unexportable_keys/service_error.h"
 #include "components/unexportable_keys/unexportable_key_id.h"
 #include "components/unexportable_keys/unexportable_key_loader.h"
@@ -239,6 +241,11 @@
     on_persistent_error_encountered_refresh_error_ = refresh_error;
   }
 
+  void OnCookieRotationStoppedTimeout(
+      BoundSessionCookieController* controller) override {
+    on_cookie_rotation_stopped_timeout_called_ = true;
+  }
+
   void SetExpirationTimeAndNotify(const std::string& cookie_name,
                                   const base::Time& expiration_time) {
     bound_session_cookie_controller_->SetCookieExpirationTimeAndNotify(
@@ -316,6 +323,10 @@
     return on_persistent_error_encountered_refresh_error_;
   }
 
+  bool on_cookie_rotation_stopped_timeout_called() {
+    return on_cookie_rotation_stopped_timeout_called_;
+  }
+
   void ResetOnBoundSessionThrottlerParamsChangedCallCount() {
     on_bound_session_throttler_params_changed_call_count_ = 0;
   }
@@ -405,6 +416,7 @@
       bound_session_cookie_controller_;
   size_t on_bound_session_throttler_params_changed_call_count_ = 0;
   std::optional<Result> on_persistent_error_encountered_refresh_error_;
+  bool on_cookie_rotation_stopped_timeout_called_ = false;
 };
 
 TEST_F(BoundSessionCookieControllerImplTest, KeyLoadedOnStartup) {
@@ -1545,3 +1557,122 @@
       /*periodic=*/false,
       kNumberOfErrorsToIgnoreForBackoff + 1 + kFailedAttempts);
 }
+
+TEST_F(BoundSessionCookieControllerImplTest,
+       StopCookieRotationCancelsPendingRequests) {
+  base::test::ScopedFeatureList feature_list;
+  feature_list.InitAndEnableFeature(
+      switches::kEnableOAuthMultiloginCookiesBinding);
+
+  ASSERT_EQ(cookie_fetcher_trigger(),
+            BoundSessionRefreshCookieFetcher::Trigger::kStartup);
+
+  bound_session_cookie_controller()->StopCookieRotation();
+
+  EXPECT_FALSE(CompletePendingRefreshRequestIfAny());
+  base::test::TestFuture<ResumeBlockedRequestsTrigger> future;
+  bound_session_cookie_controller()->HandleRequestBlockedOnCookie(
+      future.GetCallback());
+  // The request is not resumed.
+  EXPECT_FALSE(future.IsReady());
+}
+
+TEST_F(BoundSessionCookieControllerImplTest,
+       StopCookieRotationBlocksNewRequests) {
+  base::test::ScopedFeatureList feature_list;
+  feature_list.InitAndEnableFeature(
+      switches::kEnableOAuthMultiloginCookiesBinding);
+
+  bound_session_cookie_controller()->StopCookieRotation();
+
+  base::test::TestFuture<ResumeBlockedRequestsTrigger> future;
+  bound_session_cookie_controller()->HandleRequestBlockedOnCookie(
+      future.GetCallback());
+  task_environment()->FastForwardBy(kResumeBlockedRequestTimeout);
+
+  EXPECT_FALSE(future.IsReady());
+}
+
+TEST_F(BoundSessionCookieControllerImplTest,
+       StopCookieRotationKeepsBlockingAlreadyBlockedRequests) {
+  base::test::ScopedFeatureList feature_list;
+  feature_list.InitAndEnableFeature(
+      switches::kEnableOAuthMultiloginCookiesBinding);
+
+  base::test::TestFuture<ResumeBlockedRequestsTrigger> future;
+  bound_session_cookie_controller()->HandleRequestBlockedOnCookie(
+      future.GetCallback());
+
+  // Verify that the request is blocked and the resume timer is running.
+  ASSERT_FALSE(future.IsReady());
+  base::OneShotTimer* resume_requests_timer = resume_blocked_requests_timer();
+  ASSERT_NE(resume_requests_timer, nullptr);
+  ASSERT_TRUE(resume_requests_timer->IsRunning());
+
+  bound_session_cookie_controller()->StopCookieRotation();
+
+  // The request is still blocked (even after the resume timer timeout) and the
+  // resume timer is stopped.
+  task_environment()->FastForwardBy(kResumeBlockedRequestTimeout);
+  EXPECT_FALSE(future.IsReady());
+  EXPECT_FALSE(resume_requests_timer->IsRunning());
+}
+
+TEST_F(BoundSessionCookieControllerImplTest,
+       StopCookieRotationStopsCookieRotationTimer) {
+  base::test::ScopedFeatureList feature_list;
+  feature_list.InitAndEnableFeature(
+      switches::kEnableOAuthMultiloginCookiesBinding);
+
+  CompletePendingRefreshRequestIfAny();
+  ASSERT_TRUE(cookie_refresh_timer()->IsRunning());
+
+  bound_session_cookie_controller()->StopCookieRotation();
+
+  EXPECT_FALSE(cookie_refresh_timer()->IsRunning());
+}
+
+TEST_F(BoundSessionCookieControllerImplTest,
+       StopCookieRotationsPreventsFromSchedulingCookieRotation) {
+  base::test::ScopedFeatureList feature_list;
+  feature_list.InitAndEnableFeature(
+      switches::kEnableOAuthMultiloginCookiesBinding);
+
+  bound_session_cookie_controller()->StopCookieRotation();
+
+  // Setting up an offline state and then online state to trigger on connection
+  // changed.
+  SetConnectionType(network::mojom::ConnectionType::CONNECTION_NONE);
+
+  // Cookie rotation should NOT start if the cookie rotation is stopped.
+  SetConnectionType(network::mojom::ConnectionType::CONNECTION_5G);
+  EXPECT_EQ(cookie_fetcher(), nullptr);
+}
+
+TEST_F(BoundSessionCookieControllerImplTest,
+       StopCookieRotationPreventsFromPausingThrottling) {
+  base::test::ScopedFeatureList feature_list;
+  feature_list.InitAndEnableFeature(
+      switches::kEnableOAuthMultiloginCookiesBinding);
+
+  TriggerThrottlingPausedAndVerify();
+
+  bound_session_cookie_controller()->StopCookieRotation();
+
+  EXPECT_FALSE(
+      bound_session_cookie_controller()->ShouldPauseThrottlingRequests());
+}
+
+TEST_F(BoundSessionCookieControllerImplTest, StopCookieRotationTimeout) {
+  base::test::ScopedFeatureList feature_list;
+  feature_list.InitAndEnableFeature(
+      switches::kEnableOAuthMultiloginCookiesBinding);
+
+  ASSERT_FALSE(on_cookie_rotation_stopped_timeout_called());
+
+  bound_session_cookie_controller()->StopCookieRotation();
+
+  EXPECT_FALSE(on_cookie_rotation_stopped_timeout_called());
+  task_environment()->FastForwardBy(kResumeBlockedRequestTimeout);
+  EXPECT_TRUE(on_cookie_rotation_stopped_timeout_called());
+}
diff --git a/chrome/browser/signin/bound_session_credentials/bound_session_cookie_refresh_service.h b/chrome/browser/signin/bound_session_credentials/bound_session_cookie_refresh_service.h
index 35d38073..0e18b823 100644
--- a/chrome/browser/signin/bound_session_credentials/bound_session_cookie_refresh_service.h
+++ b/chrome/browser/signin/bound_session_credentials/bound_session_cookie_refresh_service.h
@@ -19,6 +19,7 @@
 #include "mojo/public/cpp/bindings/pending_receiver.h"
 
 struct BoundSessionDebugInfo;
+struct BoundSessionKey;
 
 // BoundSessionCookieRefreshService is responsible for maintaining cookies
 // associated with bound sessions. This class does the following:
@@ -73,6 +74,19 @@
   virtual void CreateRegistrationRequest(
       BoundSessionRegistrationFetcherParam registration_params) = 0;
 
+  // Stops the cookie rotation for the given session. This is a no-op if the
+  // given session does not exist.
+  //
+  // Once the cookie rotation is stopped, all throttled requests will remain
+  // throttled until the session is terminated.
+  // This is used by OAML to ensure all requests are throttled until the
+  // returned cookies are set.
+  //
+  // The session will be terminated after a timeout if it has not been
+  // terminated explicitly. This is a safety net to ensure the session is
+  // eventually terminated even if OAML fails to terminate the session.
+  virtual void StopCookieRotation(const BoundSessionKey& key) = 0;
+
   virtual base::WeakPtr<BoundSessionCookieRefreshService> GetWeakPtr() = 0;
 
   virtual void AddObserver(Observer* observer) = 0;
diff --git a/chrome/browser/signin/bound_session_credentials/bound_session_cookie_refresh_service_impl.cc b/chrome/browser/signin/bound_session_credentials/bound_session_cookie_refresh_service_impl.cc
index b084d13e..7a680e3 100644
--- a/chrome/browser/signin/bound_session_credentials/bound_session_cookie_refresh_service_impl.cc
+++ b/chrome/browser/signin/bound_session_credentials/bound_session_cookie_refresh_service_impl.cc
@@ -121,6 +121,8 @@
                            : RotationDebugInfo::TERMINATION_REASON_OTHER;
     case kSessionOverride:
       return RotationDebugInfo::SESSION_OVERRIDE;
+    case kRotationStoppedTimeout:
+      return RotationDebugInfo::ROTATION_STOPPED_TIMEOUT;
     case kCookiesCleared:
       // `kCookiesCleared` should not be reported in the debug header.
       NOTREACHED();
@@ -407,6 +409,15 @@
   }
 }
 
+void BoundSessionCookieRefreshServiceImpl::StopCookieRotation(
+    const BoundSessionKey& key) {
+  auto controller_it = cookie_controllers_.find(key);
+  if (controller_it == cookie_controllers_.end()) {
+    return;
+  }
+  controller_it->second->StopCookieRotation();
+}
+
 base::WeakPtr<BoundSessionCookieRefreshService>
 BoundSessionCookieRefreshServiceImpl::GetWeakPtr() {
   return weak_ptr_factory_.GetWeakPtr();
@@ -466,6 +477,12 @@
   UpdateAllRenderers();
 }
 
+void BoundSessionCookieRefreshServiceImpl::OnCookieRotationStoppedTimeout(
+    BoundSessionCookieController* controller) {
+  TerminateSession(controller,
+                   SessionTerminationTrigger::kRotationStoppedTimeout);
+}
+
 void BoundSessionCookieRefreshServiceImpl::OnPersistentErrorEncountered(
     BoundSessionCookieController* controller,
     BoundSessionRefreshCookieFetcher::Result refresh_error) {
@@ -599,8 +616,9 @@
     SessionTerminationTrigger trigger,
     std::optional<BoundSessionRefreshCookieFetcher::Result> refresh_error) {
   if (trigger == SessionTerminationTrigger::kCookiesCleared) {
-    // Do not send the debug report if cookies were cleared as the request won't
-    // be attributed to a user in any case.
+    // Do not send the debug report if cookies were cleared or the rotation was
+    // terminated due to a stopping timeout as the request won't be attributed
+    // to a user in any case.
     return;
   }
 
diff --git a/chrome/browser/signin/bound_session_credentials/bound_session_cookie_refresh_service_impl.h b/chrome/browser/signin/bound_session_credentials/bound_session_cookie_refresh_service_impl.h
index f7aaae7..ba8ea15 100644
--- a/chrome/browser/signin/bound_session_credentials/bound_session_cookie_refresh_service_impl.h
+++ b/chrome/browser/signin/bound_session_credentials/bound_session_cookie_refresh_service_impl.h
@@ -61,7 +61,8 @@
     kCookiesCleared = 1,
     kSessionTerminationHeader = 2,
     kSessionOverride = 3,
-    kMaxValue = kSessionOverride,
+    kRotationStoppedTimeout = 4,
+    kMaxValue = kRotationStoppedTimeout,
   };
 
   BoundSessionCookieRefreshServiceImpl(
@@ -87,6 +88,7 @@
           receiver) override;
   void CreateRegistrationRequest(
       BoundSessionRegistrationFetcherParam registration_params) override;
+  void StopCookieRotation(const BoundSessionKey& key) override;
   base::WeakPtr<BoundSessionCookieRefreshService> GetWeakPtr() override;
   void AddObserver(
       BoundSessionCookieRefreshService::Observer* observer) override;
@@ -160,6 +162,8 @@
   void OnPersistentErrorEncountered(
       BoundSessionCookieController* controller,
       BoundSessionRefreshCookieFetcher::Result refresh_error) override;
+  void OnCookieRotationStoppedTimeout(
+      BoundSessionCookieController* controller) override;
 
   // StoragePartition::DataRemovalObserver:
   void OnStorageKeyDataCleared(
diff --git a/chrome/browser/signin/bound_session_credentials/bound_session_cookie_refresh_service_impl_unittest.cc b/chrome/browser/signin/bound_session_credentials/bound_session_cookie_refresh_service_impl_unittest.cc
index 63a6777..52f50ae 100644
--- a/chrome/browser/signin/bound_session_credentials/bound_session_cookie_refresh_service_impl_unittest.cc
+++ b/chrome/browser/signin/bound_session_credentials/bound_session_cookie_refresh_service_impl_unittest.cc
@@ -142,6 +142,10 @@
     resume_blocked_requests_.push_back(std::move(resume_blocked_request));
   }
 
+  bool rotation_stopped() const { return rotation_stopped_; }
+
+  void StopCookieRotation() override { rotation_stopped_ = true; }
+
   bound_session_credentials::RotationDebugInfo TakeDebugInfo() override {
     return {};
   }
@@ -169,6 +173,10 @@
         this, BoundSessionRefreshCookieFetcher::Result::kServerPersistentError);
   }
 
+  void SimulateOnCookieRotationStoppedTimeout() {
+    delegate_->OnCookieRotationStoppedTimeout(this);
+  }
+
   void SimulateRefreshBoundSessionCompleted() {
     EXPECT_FALSE(resume_blocked_requests_.empty());
     std::vector<chrome::mojom::BoundSessionRequestThrottledHandler::
@@ -191,6 +199,7 @@
   std::vector<uint8_t> wrapped_key_;
   signin::Tribool was_new_session_at_init_ = signin::Tribool::kUnknown;
   bool throttling_requests_paused_ = false;
+  bool rotation_stopped_ = false;
   base::WeakPtrFactory<FakeBoundSessionCookieController> weak_ptr_factory_{
       this};
 };
@@ -405,7 +414,9 @@
                bound_session_credentials::RotationDebugInfo debug_info),
               ());
 
-  BoundSessionCookieRefreshServiceImpl* GetCookieRefreshServiceImpl() {
+  // Returns the cookie refresh service if it already exists, or creates a new
+  // one.
+  BoundSessionCookieRefreshServiceImpl* GetOrCreateCookieRefreshServiceImpl() {
     if (!cookie_refresh_service_) {
       cookie_refresh_service_ = CreateBoundSessionCookieRefreshServiceImpl();
     }
@@ -464,6 +475,10 @@
 
   MockObserver* mock_observer() { return &mock_observer_; }
 
+  void FastForwardBy(base::TimeDelta delta) {
+    task_environment_.FastForwardBy(delta);
+  }
+
   void RunUntilIdle() { task_environment_.RunUntilIdle(); }
 
   BoundSessionRegistrationFetcherParam CreateTestRegistrationFetcherParams(
@@ -603,7 +618,7 @@
 
 TEST_F(BoundSessionCookieRefreshServiceImplTest, VerifyControllerParams) {
   SetupPreConditionForBoundSession();
-  GetCookieRefreshServiceImpl();
+  GetOrCreateCookieRefreshServiceImpl();
   VerifyBoundSession(CreateTestBoundSessionParams());
   EXPECT_EQ(cookie_controller()->was_new_session_at_init(),
             signin::Tribool::kFalse);
@@ -611,14 +626,15 @@
 
 TEST_F(BoundSessionCookieRefreshServiceImplTest,
        VerifyBoundSessionThrottlerParamsUnboundSession) {
-  GetCookieRefreshServiceImpl();
+  GetOrCreateCookieRefreshServiceImpl();
   VerifyNoBoundSession();
 }
 
 TEST_F(BoundSessionCookieRefreshServiceImplTest,
        VerifyBoundSessionThrottlerParamsBoundSession) {
   SetupPreConditionForBoundSession();
-  BoundSessionCookieRefreshServiceImpl* service = GetCookieRefreshServiceImpl();
+  BoundSessionCookieRefreshServiceImpl* service =
+      GetOrCreateCookieRefreshServiceImpl();
   ASSERT_TRUE(cookie_controller());
 
   std::vector<chrome::mojom::BoundSessionThrottlerParamsPtr>
@@ -640,14 +656,15 @@
       CreateCookieCredential("cookieB", GURL("https://accounts.google.com"));
 
   ASSERT_TRUE(storage()->SaveParams(params));
-  GetCookieRefreshServiceImpl();
+  GetOrCreateCookieRefreshServiceImpl();
   VerifyBoundSession(params);
 }
 
 TEST_F(BoundSessionCookieRefreshServiceImplTest,
        RefreshBoundSessionCookieBoundSession) {
   SetupPreConditionForBoundSession();
-  BoundSessionCookieRefreshServiceImpl* service = GetCookieRefreshServiceImpl();
+  BoundSessionCookieRefreshServiceImpl* service =
+      GetOrCreateCookieRefreshServiceImpl();
   EXPECT_TRUE(cookie_controller());
   base::test::TestFuture<ResumeBlockedRequestsTrigger> future;
   service->HandleRequestBlockedOnCookie(kTestGoogleURL, future.GetCallback());
@@ -661,7 +678,8 @@
 TEST_F(BoundSessionCookieRefreshServiceImplTest,
        RequestBlockedOnCookieThrottlingPaused) {
   SetupPreConditionForBoundSession();
-  BoundSessionCookieRefreshServiceImpl* service = GetCookieRefreshServiceImpl();
+  BoundSessionCookieRefreshServiceImpl* service =
+      GetOrCreateCookieRefreshServiceImpl();
   EXPECT_TRUE(cookie_controller());
   cookie_controller()->SetThrottlingRequestsPaused(true);
   base::test::TestFuture<ResumeBlockedRequestsTrigger> future;
@@ -674,7 +692,8 @@
 
 TEST_F(BoundSessionCookieRefreshServiceImplTest,
        RefreshBoundSessionCookieUnboundSession) {
-  BoundSessionCookieRefreshServiceImpl* service = GetCookieRefreshServiceImpl();
+  BoundSessionCookieRefreshServiceImpl* service =
+      GetOrCreateCookieRefreshServiceImpl();
   EXPECT_FALSE(cookie_controller());
 
   // Unbound session, the callback should be called immediately.
@@ -687,7 +706,8 @@
 
 TEST_F(BoundSessionCookieRefreshServiceImplTest,
        UpdateAllRenderersOnBoundSessionStarted) {
-  BoundSessionCookieRefreshServiceImpl* service = GetCookieRefreshServiceImpl();
+  BoundSessionCookieRefreshServiceImpl* service =
+      GetOrCreateCookieRefreshServiceImpl();
   EXPECT_FALSE(cookie_controller());
   EXPECT_THAT(service->GetBoundSessionThrottlerParams(), IsEmpty());
   base::MockRepeatingCallback<void()> renderer_updater;
@@ -709,7 +729,8 @@
   base::MockRepeatingCallback<void()> renderer_updater;
   EXPECT_CALL(renderer_updater, Run()).Times(0);
   SetupPreConditionForBoundSession();
-  BoundSessionCookieRefreshServiceImpl* service = GetCookieRefreshServiceImpl();
+  BoundSessionCookieRefreshServiceImpl* service =
+      GetOrCreateCookieRefreshServiceImpl();
   EXPECT_TRUE(cookie_controller());
   SetRendererUpdater(renderer_updater.Get());
   testing::Mock::VerifyAndClearExpectations(&renderer_updater);
@@ -733,7 +754,7 @@
   base::MockRepeatingCallback<void()> renderer_updater;
   EXPECT_CALL(renderer_updater, Run()).Times(0);
   SetupPreConditionForBoundSession();
-  GetCookieRefreshServiceImpl();
+  GetOrCreateCookieRefreshServiceImpl();
   EXPECT_TRUE(cookie_controller());
   SetRendererUpdater(renderer_updater.Get());
   testing::Mock::VerifyAndClearExpectations(&renderer_updater);
@@ -755,7 +776,7 @@
 TEST_F(BoundSessionCookieRefreshServiceImplTest,
        SendDebugReportOnBoundSessionTerminated) {
   SetupPreConditionForBoundSession();
-  GetCookieRefreshServiceImpl();
+  GetOrCreateCookieRefreshServiceImpl();
   EXPECT_TRUE(cookie_controller());
 
   EXPECT_CALL(
@@ -783,7 +804,8 @@
 
 TEST_F(BoundSessionCookieRefreshServiceImplTest, TerminateSession) {
   SetupPreConditionForBoundSession();
-  BoundSessionCookieRefreshServiceImpl* service = GetCookieRefreshServiceImpl();
+  BoundSessionCookieRefreshServiceImpl* service =
+      GetOrCreateCookieRefreshServiceImpl();
   EXPECT_THAT(service->GetBoundSessionThrottlerParams(), Not(IsEmpty()));
 
   EXPECT_CALL(
@@ -801,7 +823,7 @@
   // Verify prefs were cleared.
   // Ensure on next startup, there won't be a bound session.
   ResetCookieRefreshService();
-  service = GetCookieRefreshServiceImpl();
+  service = GetOrCreateCookieRefreshServiceImpl();
 
   SCOPED_TRACE("No bound session on Startup.");
   VerifyNoBoundSession();
@@ -810,7 +832,8 @@
 TEST_F(BoundSessionCookieRefreshServiceImplTest,
        TerminateSessionOnPersistentErrorEncountered) {
   SetupPreConditionForBoundSession();
-  BoundSessionCookieRefreshServiceImpl* service = GetCookieRefreshServiceImpl();
+  BoundSessionCookieRefreshServiceImpl* service =
+      GetOrCreateCookieRefreshServiceImpl();
   EXPECT_THAT(service->GetBoundSessionThrottlerParams(), Not(IsEmpty()));
 
   ASSERT_TRUE(cookie_controller());
@@ -829,7 +852,7 @@
   // Verify prefs were cleared.
   // Ensure on next startup, there won't be a bound session.
   ResetCookieRefreshService();
-  service = GetCookieRefreshServiceImpl();
+  service = GetOrCreateCookieRefreshServiceImpl();
 
   SCOPED_TRACE("No bound session on Startup.");
   VerifyNoBoundSession();
@@ -842,7 +865,8 @@
       base::MakeRefCounted<net::HttpResponseHeaders>("");
   headers->AddHeader(kSessionTerminationHeaderName,
                      GetSessionTerminationHeaderValue(kTestSessionId));
-  BoundSessionCookieRefreshServiceImpl* service = GetCookieRefreshServiceImpl();
+  BoundSessionCookieRefreshServiceImpl* service =
+      GetOrCreateCookieRefreshServiceImpl();
   EXPECT_CALL(
       *mock_observer(),
       OnBoundSessionTerminated(kTestGoogleURL,
@@ -866,7 +890,8 @@
       base::StringPrintf(
           "first_attribute=abc;session_id=%s;third_attribute=edf",
           kTestSessionId));
-  BoundSessionCookieRefreshServiceImpl* service = GetCookieRefreshServiceImpl();
+  BoundSessionCookieRefreshServiceImpl* service =
+      GetOrCreateCookieRefreshServiceImpl();
   EXPECT_CALL(
       *mock_observer(),
       OnBoundSessionTerminated(kTestGoogleURL,
@@ -887,7 +912,8 @@
       base::MakeRefCounted<net::HttpResponseHeaders>("");
   headers->AddHeader(kSessionTerminationHeaderName,
                      GetSessionTerminationHeaderValue(kTestSessionId));
-  BoundSessionCookieRefreshServiceImpl* service = GetCookieRefreshServiceImpl();
+  BoundSessionCookieRefreshServiceImpl* service =
+      GetOrCreateCookieRefreshServiceImpl();
   EXPECT_CALL(
       *mock_observer(),
       OnBoundSessionTerminated(kTestGoogleURL,
@@ -909,7 +935,8 @@
   headers->AddHeader(kSessionTerminationHeaderName,
                      GetSessionTerminationHeaderValue("different_session_id"));
 
-  BoundSessionCookieRefreshServiceImpl* service = GetCookieRefreshServiceImpl();
+  BoundSessionCookieRefreshServiceImpl* service =
+      GetOrCreateCookieRefreshServiceImpl();
   service->MaybeTerminateSession(kTestGoogleURL, headers.get());
   VerifyBoundSession(CreateTestBoundSessionParams());
   histogram_tester().ExpectTotalCount(
@@ -924,7 +951,8 @@
   headers->AddHeader(kSessionTerminationHeaderName,
                      GetSessionTerminationHeaderValue(kTestSessionId));
 
-  BoundSessionCookieRefreshServiceImpl* service = GetCookieRefreshServiceImpl();
+  BoundSessionCookieRefreshServiceImpl* service =
+      GetOrCreateCookieRefreshServiceImpl();
   // `kTestOtherURL` and the bound session URL are from different sites.
   service->MaybeTerminateSession(kTestOtherURL, headers.get());
   VerifyBoundSession(CreateTestBoundSessionParams());
@@ -938,7 +966,8 @@
   scoped_refptr<net::HttpResponseHeaders> headers =
       base::MakeRefCounted<net::HttpResponseHeaders>("");
 
-  BoundSessionCookieRefreshServiceImpl* service = GetCookieRefreshServiceImpl();
+  BoundSessionCookieRefreshServiceImpl* service =
+      GetOrCreateCookieRefreshServiceImpl();
   service->MaybeTerminateSession(kTestGoogleURL, headers.get());
   VerifyBoundSession(CreateTestBoundSessionParams());
   histogram_tester().ExpectTotalCount(
@@ -952,7 +981,8 @@
       base::MakeRefCounted<net::HttpResponseHeaders>("");
   headers->AddHeader(kSessionTerminationHeaderName,
                      kTestSessionId);  // no "session_id=" key
-  BoundSessionCookieRefreshServiceImpl* service = GetCookieRefreshServiceImpl();
+  BoundSessionCookieRefreshServiceImpl* service =
+      GetOrCreateCookieRefreshServiceImpl();
 
   service->MaybeTerminateSession(kTestGoogleURL, headers.get());
 
@@ -969,7 +999,8 @@
   headers->AddHeader(kSessionTerminationHeaderName,
                      base::StringPrintf("other_attribute=%s", kTestSessionId));
 
-  BoundSessionCookieRefreshServiceImpl* service = GetCookieRefreshServiceImpl();
+  BoundSessionCookieRefreshServiceImpl* service =
+      GetOrCreateCookieRefreshServiceImpl();
   service->MaybeTerminateSession(kTestGoogleURL, headers.get());
   VerifyBoundSession(CreateTestBoundSessionParams());
   histogram_tester().ExpectTotalCount(
@@ -979,7 +1010,8 @@
 TEST_F(BoundSessionCookieRefreshServiceImplTest,
        AddBoundSessionRequestThrottledHandlerReceivers) {
   SetupPreConditionForBoundSession();
-  BoundSessionCookieRefreshServiceImpl* service = GetCookieRefreshServiceImpl();
+  BoundSessionCookieRefreshServiceImpl* service =
+      GetOrCreateCookieRefreshServiceImpl();
   ASSERT_TRUE(cookie_controller());
   mojo::Remote<chrome::mojom::BoundSessionRequestThrottledHandler> handler_1;
   mojo::Remote<chrome::mojom::BoundSessionRequestThrottledHandler> handler_2;
@@ -1007,7 +1039,8 @@
 }
 
 TEST_F(BoundSessionCookieRefreshServiceImplTest, RegisterNewBoundSession) {
-  BoundSessionCookieRefreshServiceImpl* service = GetCookieRefreshServiceImpl();
+  BoundSessionCookieRefreshServiceImpl* service =
+      GetOrCreateCookieRefreshServiceImpl();
   VerifyNoBoundSession();
 
   auto params = CreateTestBoundSessionParams();
@@ -1019,7 +1052,8 @@
 
 TEST_F(BoundSessionCookieRefreshServiceImplTest,
        OverrideExistingBoundSessionSameSessionId) {
-  BoundSessionCookieRefreshServiceImpl* service = GetCookieRefreshServiceImpl();
+  BoundSessionCookieRefreshServiceImpl* service =
+      GetOrCreateCookieRefreshServiceImpl();
   service->RegisterNewBoundSession(CreateTestBoundSessionParams());
 
   auto new_params = CreateTestBoundSessionParams();
@@ -1036,7 +1070,8 @@
 
 TEST_F(BoundSessionCookieRefreshServiceImplTest,
        OverrideExistingBoundSessionWithInvalidParams) {
-  BoundSessionCookieRefreshServiceImpl* service = GetCookieRefreshServiceImpl();
+  BoundSessionCookieRefreshServiceImpl* service =
+      GetOrCreateCookieRefreshServiceImpl();
   auto original_params = CreateTestBoundSessionParams();
   service->RegisterNewBoundSession(original_params);
 
@@ -1051,7 +1086,8 @@
 }
 
 TEST_F(BoundSessionCookieRefreshServiceImplTest, ClearMatchingData) {
-  BoundSessionCookieRefreshServiceImpl* service = GetCookieRefreshServiceImpl();
+  BoundSessionCookieRefreshServiceImpl* service =
+      GetOrCreateCookieRefreshServiceImpl();
   service->RegisterNewBoundSession(CreateTestBoundSessionParams());
 
   EXPECT_CALL(
@@ -1069,7 +1105,8 @@
 
 TEST_F(BoundSessionCookieRefreshServiceImplTest,
        ClearMatchingDataTypeMismatch) {
-  BoundSessionCookieRefreshServiceImpl* service = GetCookieRefreshServiceImpl();
+  BoundSessionCookieRefreshServiceImpl* service =
+      GetOrCreateCookieRefreshServiceImpl();
   auto params = CreateTestBoundSessionParams();
   service->RegisterNewBoundSession(params);
 
@@ -1082,7 +1119,8 @@
 
 TEST_F(BoundSessionCookieRefreshServiceImplTest,
        ClearMatchingDataOriginMismatch) {
-  BoundSessionCookieRefreshServiceImpl* service = GetCookieRefreshServiceImpl();
+  BoundSessionCookieRefreshServiceImpl* service =
+      GetOrCreateCookieRefreshServiceImpl();
   auto params = CreateTestBoundSessionParams();
   service->RegisterNewBoundSession(params);
 
@@ -1095,7 +1133,8 @@
 
 TEST_F(BoundSessionCookieRefreshServiceImplTest,
        ClearMatchingDataOriginMismatchSuborigin) {
-  BoundSessionCookieRefreshServiceImpl* service = GetCookieRefreshServiceImpl();
+  BoundSessionCookieRefreshServiceImpl* service =
+      GetOrCreateCookieRefreshServiceImpl();
   auto params = CreateTestBoundSessionParams();
   service->RegisterNewBoundSession(params);
 
@@ -1108,7 +1147,8 @@
 
 TEST_F(BoundSessionCookieRefreshServiceImplTest,
        ClearMatchingDataCreationTimeMismatch) {
-  BoundSessionCookieRefreshServiceImpl* service = GetCookieRefreshServiceImpl();
+  BoundSessionCookieRefreshServiceImpl* service =
+      GetOrCreateCookieRefreshServiceImpl();
   auto params = CreateTestBoundSessionParams();
   service->RegisterNewBoundSession(params);
 
@@ -1122,7 +1162,8 @@
 }
 
 TEST_F(BoundSessionCookieRefreshServiceImplTest, CreateRegistrationRequest) {
-  BoundSessionCookieRefreshServiceImpl* service = GetCookieRefreshServiceImpl();
+  BoundSessionCookieRefreshServiceImpl* service =
+      GetOrCreateCookieRefreshServiceImpl();
   service->CreateRegistrationRequest(
       CreateTestRegistrationFetcherParams(kDefaultRegistrationPath));
   ASSERT_TRUE(registration_fetcher());
@@ -1140,7 +1181,8 @@
 TEST_F(BoundSessionCookieRefreshServiceImplTest,
        CreateRegistrationRequestNonDefaultPath) {
   const std::string kNonDefaultRegistrationPath = "/NonDefaultPath";
-  BoundSessionCookieRefreshServiceImpl* service = GetCookieRefreshServiceImpl();
+  BoundSessionCookieRefreshServiceImpl* service =
+      GetOrCreateCookieRefreshServiceImpl();
   service->CreateRegistrationRequest(
       CreateTestRegistrationFetcherParams(kNonDefaultRegistrationPath));
   ASSERT_TRUE(registration_fetcher());
@@ -1155,7 +1197,8 @@
   scoped_feature_list.InitAndEnableFeatureWithParameters(
       switches::kEnableBoundSessionCredentials,
       {{"exclusive-registration-path", ""}});
-  BoundSessionCookieRefreshServiceImpl* service = GetCookieRefreshServiceImpl();
+  BoundSessionCookieRefreshServiceImpl* service =
+      GetOrCreateCookieRefreshServiceImpl();
   service->CreateRegistrationRequest(
       CreateTestRegistrationFetcherParams(kNonDefaultRegistrationPath));
   ASSERT_TRUE(registration_fetcher());
@@ -1170,7 +1213,8 @@
   scoped_feature_list.InitAndEnableFeatureWithParameters(
       switches::kEnableBoundSessionCredentials,
       {{"exclusive-registration-path", kCustomPath}});
-  BoundSessionCookieRefreshServiceImpl* service = GetCookieRefreshServiceImpl();
+  BoundSessionCookieRefreshServiceImpl* service =
+      GetOrCreateCookieRefreshServiceImpl();
   service->CreateRegistrationRequest(
       CreateTestRegistrationFetcherParams(kCustomPath));
   ASSERT_TRUE(registration_fetcher());
@@ -1184,12 +1228,61 @@
   scoped_feature_list.InitAndEnableFeatureWithParameters(
       switches::kEnableBoundSessionCredentials,
       {{"exclusive-registration-path", "/CustomPath"}});
-  BoundSessionCookieRefreshServiceImpl* service = GetCookieRefreshServiceImpl();
+  BoundSessionCookieRefreshServiceImpl* service =
+      GetOrCreateCookieRefreshServiceImpl();
   service->CreateRegistrationRequest(
       CreateTestRegistrationFetcherParams("/AnotherPath"));
   EXPECT_FALSE(registration_fetcher());
 }
 
+TEST_F(BoundSessionCookieRefreshServiceImplTest, StopCookieRotation) {
+  SetupPreConditionForBoundSession();
+  BoundSessionCookieRefreshServiceImpl* service =
+      GetOrCreateCookieRefreshServiceImpl();
+  ASSERT_NE(service, nullptr);
+  ASSERT_NE(cookie_controller(), nullptr);
+  EXPECT_FALSE(cookie_controller()->rotation_stopped());
+
+  service->StopCookieRotation(cookie_controller()->GetBoundSessionKey());
+
+  EXPECT_TRUE(cookie_controller()->rotation_stopped());
+}
+
+TEST_F(BoundSessionCookieRefreshServiceImplTest,
+       StopCookieRotationUnknownSession) {
+  SetupPreConditionForBoundSession();
+  BoundSessionCookieRefreshServiceImpl* service =
+      GetOrCreateCookieRefreshServiceImpl();
+  ASSERT_NE(service, nullptr);
+  ASSERT_NE(cookie_controller(), nullptr);
+  EXPECT_FALSE(cookie_controller()->rotation_stopped());
+
+  service->StopCookieRotation(
+      {.site = kTestGoogleURL, .session_id = "unknown_session"});
+
+  EXPECT_FALSE(cookie_controller()->rotation_stopped());
+}
+
+TEST_F(BoundSessionCookieRefreshServiceImplTest,
+       TerminatesSessionOnCookieRotationStoppedTimeout) {
+  SetupPreConditionForBoundSession();
+  GetOrCreateCookieRefreshServiceImpl();
+
+  EXPECT_CALL(
+      *mock_observer(),
+      OnBoundSessionTerminated(kTestGoogleURL,
+                               base::flat_set<std::string>(
+                                   {k1PSIDTSCookieName, k3PSIDTSCookieName})))
+      .Times(1);
+
+  ASSERT_NE(cookie_controller(), nullptr);
+  cookie_controller()->SimulateOnCookieRotationStoppedTimeout();
+
+  VerifyNoBoundSession();
+  VerifySessionTerminationTriggerRecorded(
+      SessionTerminationTrigger::kRotationStoppedTimeout);
+}
+
 // Test suite for tests involving multiple sessions.
 class BoundSessionCookieRefreshServiceImplMultiSessionTest
     : public BoundSessionCookieRefreshServiceImplTestBase {
@@ -1286,7 +1379,7 @@
   for (const auto& params : all_params) {
     ASSERT_TRUE(storage()->SaveParams(params));
   }
-  GetCookieRefreshServiceImpl();
+  GetOrCreateCookieRefreshServiceImpl();
   VerifyBoundSessions(all_params);
 }
 
@@ -1298,7 +1391,8 @@
       switches::kEnableBoundSessionCredentials,
       {{"exclusive-registration-path", ""}});
 
-  BoundSessionCookieRefreshServiceImpl* service = GetCookieRefreshServiceImpl();
+  BoundSessionCookieRefreshServiceImpl* service =
+      GetOrCreateCookieRefreshServiceImpl();
   const std::string kFirstPath = "/First";
   const std::string kSecondPath = "/Second";
 
@@ -1341,7 +1435,8 @@
   for (const auto& params : all_params) {
     ASSERT_TRUE(storage()->SaveParams(params));
   }
-  BoundSessionCookieRefreshServiceImpl* service = GetCookieRefreshServiceImpl();
+  BoundSessionCookieRefreshServiceImpl* service =
+      GetOrCreateCookieRefreshServiceImpl();
 
   base::test::TestFuture<ResumeBlockedRequestsTrigger> future;
   service->HandleRequestBlockedOnCookie(kTestGoogleURL, future.GetCallback());
@@ -1365,7 +1460,8 @@
   for (const auto& params : all_params) {
     ASSERT_TRUE(storage()->SaveParams(params));
   }
-  BoundSessionCookieRefreshServiceImpl* service = GetCookieRefreshServiceImpl();
+  BoundSessionCookieRefreshServiceImpl* service =
+      GetOrCreateCookieRefreshServiceImpl();
 
   base::test::TestFuture<ResumeBlockedRequestsTrigger> future;
   service->HandleRequestBlockedOnCookie(kTestGoogleURL, future.GetCallback());
@@ -1395,7 +1491,8 @@
     ASSERT_TRUE(storage()->SaveParams(params));
   }
 
-  BoundSessionCookieRefreshServiceImpl* service = GetCookieRefreshServiceImpl();
+  BoundSessionCookieRefreshServiceImpl* service =
+      GetOrCreateCookieRefreshServiceImpl();
   GetCookieController(kGoogleSessionKeyOne)->SetThrottlingRequestsPaused(true);
 
   base::test::TestFuture<ResumeBlockedRequestsTrigger> future;
@@ -1418,7 +1515,8 @@
     ASSERT_TRUE(storage()->SaveParams(params));
   }
 
-  BoundSessionCookieRefreshServiceImpl* service = GetCookieRefreshServiceImpl();
+  BoundSessionCookieRefreshServiceImpl* service =
+      GetOrCreateCookieRefreshServiceImpl();
   GetCookieController(kGoogleSessionKeyOne)->SetThrottlingRequestsPaused(true);
   GetCookieController(kGoogleSessionKeyTwo)->SetThrottlingRequestsPaused(true);
 
@@ -1437,7 +1535,8 @@
   for (const auto& params : all_params) {
     ASSERT_TRUE(storage()->SaveParams(params));
   }
-  BoundSessionCookieRefreshServiceImpl* service = GetCookieRefreshServiceImpl();
+  BoundSessionCookieRefreshServiceImpl* service =
+      GetOrCreateCookieRefreshServiceImpl();
   // Mark the second session as fresh.
   GetCookieController(kGoogleSessionKeyTwo)
       ->SimulateOnCookieExpirationDateChanged(
@@ -1461,7 +1560,8 @@
   for (const auto& params : all_params) {
     ASSERT_TRUE(storage()->SaveParams(params));
   }
-  BoundSessionCookieRefreshServiceImpl* service = GetCookieRefreshServiceImpl();
+  BoundSessionCookieRefreshServiceImpl* service =
+      GetOrCreateCookieRefreshServiceImpl();
   // Mark both sessions as fresh.
   GetCookieController(kGoogleSessionKeyOne)
       ->SimulateOnCookieExpirationDateChanged(
@@ -1488,7 +1588,8 @@
   for (const auto& params : all_params) {
     ASSERT_TRUE(storage()->SaveParams(params));
   }
-  BoundSessionCookieRefreshServiceImpl* service = GetCookieRefreshServiceImpl();
+  BoundSessionCookieRefreshServiceImpl* service =
+      GetOrCreateCookieRefreshServiceImpl();
 
   base::test::TestFuture<ResumeBlockedRequestsTrigger> future;
   // This request is not covered by any session.
@@ -1500,7 +1601,8 @@
 
 TEST_F(BoundSessionCookieRefreshServiceImplMultiSessionTest,
        HandleRequestBlockedOnCookieNoSessions) {
-  BoundSessionCookieRefreshServiceImpl* service = GetCookieRefreshServiceImpl();
+  BoundSessionCookieRefreshServiceImpl* service =
+      GetOrCreateCookieRefreshServiceImpl();
 
   base::test::TestFuture<ResumeBlockedRequestsTrigger> future;
   service->HandleRequestBlockedOnCookie(kTestGoogleURL, future.GetCallback());
@@ -1518,7 +1620,8 @@
   for (const auto& params : all_params) {
     ASSERT_TRUE(storage()->SaveParams(params));
   }
-  BoundSessionCookieRefreshServiceImpl* service = GetCookieRefreshServiceImpl();
+  BoundSessionCookieRefreshServiceImpl* service =
+      GetOrCreateCookieRefreshServiceImpl();
 
   base::test::TestFuture<ResumeBlockedRequestsTrigger> future;
   service->HandleRequestBlockedOnCookie(kTestGoogleURL, future.GetCallback());
@@ -1532,7 +1635,8 @@
 
 TEST_F(BoundSessionCookieRefreshServiceImplMultiSessionTest,
        RegisterNewBoundSession) {
-  BoundSessionCookieRefreshServiceImpl* service = GetCookieRefreshServiceImpl();
+  BoundSessionCookieRefreshServiceImpl* service =
+      GetOrCreateCookieRefreshServiceImpl();
   VerifyBoundSessions({});
 
   auto params =
@@ -1543,7 +1647,8 @@
 
 TEST_F(BoundSessionCookieRefreshServiceImplMultiSessionTest,
        RegisterSecondBoundSessionSameDomainDifferentSessionIds) {
-  BoundSessionCookieRefreshServiceImpl* service = GetCookieRefreshServiceImpl();
+  BoundSessionCookieRefreshServiceImpl* service =
+      GetOrCreateCookieRefreshServiceImpl();
   auto first_params =
       CreateBoundSessionParams(kGoogleSessionKeyOne, {"cookieA", "cookieB"});
   service->RegisterNewBoundSession(first_params);
@@ -1556,7 +1661,8 @@
 
 TEST_F(BoundSessionCookieRefreshServiceImplMultiSessionTest,
        RegisterSecondBoundSessionSameSessionIdDifferentDomains) {
-  BoundSessionCookieRefreshServiceImpl* service = GetCookieRefreshServiceImpl();
+  BoundSessionCookieRefreshServiceImpl* service =
+      GetOrCreateCookieRefreshServiceImpl();
   auto first_params =
       CreateBoundSessionParams(kGoogleSessionKeyOne, {"cookieA", "cookieB"});
   service->RegisterNewBoundSession(first_params);
@@ -1569,7 +1675,8 @@
 
 TEST_F(BoundSessionCookieRefreshServiceImplMultiSessionTest,
        RegisterBoundSessionSameSessionKey) {
-  BoundSessionCookieRefreshServiceImpl* service = GetCookieRefreshServiceImpl();
+  BoundSessionCookieRefreshServiceImpl* service =
+      GetOrCreateCookieRefreshServiceImpl();
   auto other_params =
       CreateBoundSessionParams(kGoogleSessionKeyTwo, {"cookieX"});
   service->RegisterNewBoundSession(other_params);
@@ -1594,7 +1701,7 @@
   for (const auto& params : all_params) {
     ASSERT_TRUE(storage()->SaveParams(params));
   }
-  GetCookieRefreshServiceImpl();
+  GetOrCreateCookieRefreshServiceImpl();
 
   EXPECT_CALL(
       *mock_observer(),
@@ -1629,7 +1736,8 @@
       OnBoundSessionTerminated(
           kTestGoogleURL, base::flat_set<std::string>({"cookieA", "cookieB"})))
       .WillOnce([this] { PruneDestroyedControllers(); });
-  BoundSessionCookieRefreshServiceImpl* service = GetCookieRefreshServiceImpl();
+  BoundSessionCookieRefreshServiceImpl* service =
+      GetOrCreateCookieRefreshServiceImpl();
   service->MaybeTerminateSession(GURL("https://google.com/SignOut"),
                                  headers.get());
   // all_params[0] should have been terminated.
@@ -1647,7 +1755,7 @@
   for (const auto& params : all_params) {
     ASSERT_TRUE(storage()->SaveParams(params));
   }
-  GetCookieRefreshServiceImpl();
+  GetOrCreateCookieRefreshServiceImpl();
 
   EXPECT_CALL(
       *mock_observer(),
@@ -1676,7 +1784,7 @@
     ASSERT_TRUE(storage()->SaveParams(params));
   }
   base::HistogramTester histogram_tester;
-  GetCookieRefreshServiceImpl();
+  GetOrCreateCookieRefreshServiceImpl();
   histogram_tester.ExpectUniqueSample(
       "Signin.BoundSessionCredentials.SessionCountOnInit", all_params.size(),
       /*expected_bucket_count=*/1);
@@ -1724,7 +1832,7 @@
   for (const auto& params : all_params) {
     ASSERT_TRUE(storage()->SaveParams(params));
   }
-  GetCookieRefreshServiceImpl();
+  GetOrCreateCookieRefreshServiceImpl();
   std::vector<bound_session_credentials::BoundSessionParams> expected_sessions;
   if (IsContinuityEnabled()) {
     // All sessions are expected to run.
@@ -1738,7 +1846,8 @@
 
 TEST_P(BoundSessionCookieRefreshServiceImplFeatureDisabledTest,
        CreateRegistrationRequest) {
-  BoundSessionCookieRefreshServiceImpl* service = GetCookieRefreshServiceImpl();
+  BoundSessionCookieRefreshServiceImpl* service =
+      GetOrCreateCookieRefreshServiceImpl();
   service->CreateRegistrationRequest(
       CreateTestRegistrationFetcherParams("/RegisterSession"));
   // New sessions shouldn't be registered no matter the extra feature state.
@@ -1747,7 +1856,8 @@
 
 TEST_P(BoundSessionCookieRefreshServiceImplFeatureDisabledTest,
        CreateRegistrationRequestWithWsbeta) {
-  BoundSessionCookieRefreshServiceImpl* service = GetCookieRefreshServiceImpl();
+  BoundSessionCookieRefreshServiceImpl* service =
+      GetOrCreateCookieRefreshServiceImpl();
   service->CreateRegistrationRequest(CreateTestRegistrationFetcherParams(
       "/RegisterSession", /*is_wsbeta=*/true));
   if (IsWsbetaEnabled()) {
diff --git a/chrome/browser/signin/bound_session_credentials/fake_bound_session_cookie_refresh_service.h b/chrome/browser/signin/bound_session_credentials/fake_bound_session_cookie_refresh_service.h
index db09710..6435391 100644
--- a/chrome/browser/signin/bound_session_credentials/fake_bound_session_cookie_refresh_service.h
+++ b/chrome/browser/signin/bound_session_credentials/fake_bound_session_cookie_refresh_service.h
@@ -47,6 +47,7 @@
       base::RepeatingClosure updated_callback) override {}
   void CreateRegistrationRequest(
       BoundSessionRegistrationFetcherParam registration_params) override {}
+  void StopCookieRotation(const BoundSessionKey& key) override {}
   base::WeakPtr<BoundSessionCookieRefreshService> GetWeakPtr() override;
   void AddObserver(Observer* observer) override;
   void RemoveObserver(Observer* observer) override;
diff --git a/chrome/browser/signin/bound_session_credentials/rotation_debug_info.proto b/chrome/browser/signin/bound_session_credentials/rotation_debug_info.proto
index d7a0d38..c23b696 100644
--- a/chrome/browser/signin/bound_session_credentials/rotation_debug_info.proto
+++ b/chrome/browser/signin/bound_session_credentials/rotation_debug_info.proto
@@ -39,6 +39,7 @@
     TERMINATION_HEADER_RECEIVED = 7;
     SESSION_OVERRIDE = 8;
     ROTATION_CHALLENGE_SESSION_ID_MISMATCH = 9;
+    ROTATION_STOPPED_TIMEOUT = 10;
   }
 
   // Contains how many times a specific type of error occurred.
diff --git a/chrome/browser/signin/header_modification_delegate_impl_unittest.cc b/chrome/browser/signin/header_modification_delegate_impl_unittest.cc
index b4b36d942..ee43695 100644
--- a/chrome/browser/signin/header_modification_delegate_impl_unittest.cc
+++ b/chrome/browser/signin/header_modification_delegate_impl_unittest.cc
@@ -48,6 +48,11 @@
               (BoundSessionRegistrationFetcherParam registration_params),
               (override));
 
+  MOCK_METHOD(void,
+              StopCookieRotation,
+              (const BoundSessionKey& key),
+              (override));
+
   MOCK_METHOD(void, Initialize, (), (override));
   MOCK_METHOD(void,
               RegisterNewBoundSession,
diff --git a/tools/metrics/histograms/metadata/signin/enums.xml b/tools/metrics/histograms/metadata/signin/enums.xml
index 9333606..6d46c67 100644
--- a/tools/metrics/histograms/metadata/signin/enums.xml
+++ b/tools/metrics/histograms/metadata/signin/enums.xml
@@ -808,6 +808,7 @@
   <int value="1" label="Cookies cleared"/>
   <int value="2" label="Session termination header"/>
   <int value="3" label="Session override"/>
+  <int value="4" label="Rotation stopped timeout"/>
 </enum>
 
 <enum name="ShouldShowChromeSigninBubbleWithReason">