blob: 5fd56b3d6267ac41317af5c08645343eda90be45 [file] [log] [blame]
// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "net/device_bound_sessions/session_service_impl.h"
#include "base/test/gmock_callback_support.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_future.h"
#include "crypto/scoped_fake_unexportable_key_provider.h"
#include "net/base/features.h"
#include "net/device_bound_sessions/mock_session_store.h"
#include "net/device_bound_sessions/proto/storage.pb.h"
#include "net/device_bound_sessions/session_store.h"
#include "net/device_bound_sessions/test_support.h"
#include "net/device_bound_sessions/unexportable_key_service_factory.h"
#include "net/log/test_net_log.h"
#include "net/test/test_with_task_environment.h"
#include "net/url_request/url_request_context_builder.h"
#include "net/url_request/url_request_test_util.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"
using base::test::RunOnceCallback;
using ::testing::_;
using ::testing::ElementsAre;
using ::testing::InSequence;
using ::testing::Invoke;
using ::testing::StrictMock;
using ::testing::UnorderedElementsAre;
namespace net::device_bound_sessions {
namespace {
constexpr net::NetworkTrafficAnnotationTag kDummyAnnotation =
net::DefineNetworkTrafficAnnotation("dbsc_registration", "");
// Constant variables
constexpr char kUrlString[] = "https://example.com";
const GURL kTestUrl(kUrlString);
constexpr char kRefreshUrlString[] = "https://example.com/refresh";
const GURL kTestRefreshUrl(kRefreshUrlString);
const std::string kSessionId = "SessionId";
const std::string kOrigin = "https://example.com";
constexpr char kUrlString2[] = "https://example2.com";
const GURL kTestUrl2(kUrlString2);
constexpr char kRefreshUrlString2[] = "https://example2.com/refresh";
const GURL kTestRefreshUrl2(kRefreshUrlString);
const std::string kSessionId2 = "SessionId2";
const std::string kOrigin2 = "https://example2.com";
const std::string kChallenge = "challenge";
// Matcher for SessionKeys
auto ExpectId(std::string_view id) {
return testing::Field(&SessionKey::id, Session::Id(std::string(id)));
}
class FakeDeviceBoundSessionObserver {
public:
const std::vector<SessionAccess>& notifications() const {
return notifications_;
}
void ClearNotifications() { notifications_.clear(); }
void OnSessionAccessed(const SessionAccess& session_access) {
notifications_.push_back(session_access);
}
SessionService::OnAccessCallback GetCallback() {
return base::BindRepeating(
&FakeDeviceBoundSessionObserver::OnSessionAccessed,
base::Unretained(this));
}
private:
std::vector<SessionAccess> notifications_;
};
class SessionServiceImplTest : public ::testing::Test,
public WithTaskEnvironment {
public:
SessionServiceImplTest()
: WithTaskEnvironment(base::test::TaskEnvironment::TimeSource::MOCK_TIME),
context_(CreateTestURLRequestContextBuilder()->Build()) {}
void SetUp() override {
service_ = std::make_unique<SessionServiceImpl>(
*UnexportableKeyServiceFactory::GetInstance()->GetShared(),
context_.get(),
/*store=*/nullptr);
}
URLRequestContext* context() { return context_.get(); }
SessionServiceImpl& service() { return *service_; }
// Take list of <session_id, site_url> to add sessions for testing.
void AddSessionsForTesting(
std::vector<std::tuple<std::string, std::string, std::string>>
id_url_origin_list) {
for (const auto& [id, url_str, origin] : id_url_origin_list) {
auto scoped_test_fetcher =
ScopedTestRegistrationFetcher::CreateWithSuccess(id, url_str, origin);
auto fetch_param = RegistrationFetcherParam::CreateInstanceForTesting(
GURL(url_str),
{crypto::SignatureVerifier::SignatureAlgorithm::ECDSA_SHA256},
"challenge", /*authorization=*/std::nullopt);
service_->RegisterBoundSession(
base::DoNothing(), std::move(fetch_param),
IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
NetLogWithSource(), /*original_request_initiator=*/std::nullopt);
}
}
private:
crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider_;
std::unique_ptr<URLRequestContext> context_;
std::unique_ptr<SessionServiceImpl> service_;
};
class SessionServiceImplNoRefreshQuotaTest : public SessionServiceImplTest {
public:
SessionServiceImplNoRefreshQuotaTest() {
scoped_feature_list_.InitAndDisableFeature(
net::features::kDeviceBoundSessionsRefreshQuota);
}
private:
base::test::ScopedFeatureList scoped_feature_list_;
};
TEST_F(SessionServiceImplTest, RegisterSuccess) {
AddSessionsForTesting({{kSessionId, kRefreshUrlString, kOrigin}});
net::TestDelegate delegate;
std::unique_ptr<URLRequest> request =
context()->CreateRequest(kTestUrl, IDLE, &delegate, kDummyAnnotation);
// The request needs to be samesite for it to be considered
// candidate for deferral.
request->set_site_for_cookies(SiteForCookies::FromUrl(kTestUrl));
HttpRequestHeaders extra_headers;
std::optional<SessionService::DeferralParams> maybe_deferral =
service().ShouldDefer(request.get(), &extra_headers,
FirstPartySetMetadata());
ASSERT_TRUE(maybe_deferral);
EXPECT_FALSE(maybe_deferral->is_pending_initialization);
EXPECT_EQ(**maybe_deferral->session_id, kSessionId);
}
TEST_F(SessionServiceImplTest, RegisterNoId) {
AddSessionsForTesting({{/*session_id=*/"", kRefreshUrlString, kOrigin}});
net::TestDelegate delegate;
std::unique_ptr<URLRequest> request =
context()->CreateRequest(kTestUrl, IDLE, &delegate, kDummyAnnotation);
request->set_site_for_cookies(SiteForCookies::FromUrl(kTestUrl));
HttpRequestHeaders extra_headers;
std::optional<SessionService::DeferralParams> maybe_deferral =
service().ShouldDefer(request.get(), &extra_headers,
FirstPartySetMetadata());
// session_id is empty, so should not be valid
EXPECT_FALSE(maybe_deferral);
}
TEST_F(SessionServiceImplTest, RegisterNullFetcher) {
auto scoped_null_fetcher = ScopedTestRegistrationFetcher::CreateWithFailure(
SessionError::ErrorType::kNetError, kRefreshUrlString);
auto fetch_param = RegistrationFetcherParam::CreateInstanceForTesting(
kTestUrl, {crypto::SignatureVerifier::SignatureAlgorithm::ECDSA_SHA256},
kChallenge,
/*authorization=*/std::nullopt);
service().RegisterBoundSession(
base::DoNothing(), std::move(fetch_param),
IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
NetLogWithSource(), /*original_request_initiator=*/std::nullopt);
net::TestDelegate delegate;
std::unique_ptr<URLRequest> request =
context()->CreateRequest(kTestUrl, IDLE, &delegate, kDummyAnnotation);
request->set_site_for_cookies(SiteForCookies::FromUrl(kTestUrl));
HttpRequestHeaders extra_headers;
std::optional<SessionService::DeferralParams> maybe_deferral =
service().ShouldDefer(request.get(), &extra_headers,
FirstPartySetMetadata());
// NullFetcher, so should not be valid
EXPECT_FALSE(maybe_deferral);
}
TEST_F(SessionServiceImplTest, SetChallengeForBoundSession) {
AddSessionsForTesting({{kSessionId, kRefreshUrlString, kOrigin}});
scoped_refptr<net::HttpResponseHeaders> headers =
HttpResponseHeaders::Builder({1, 1}, "200 OK").Build();
headers->AddHeader(
"Sec-Session-Challenge",
R"("challenge";id="SessionId", "challenge1";id="NonExisted")");
headers->AddHeader("Sec-Session-Challenge", R"("challenge2")");
std::vector<SessionChallengeParam> params =
SessionChallengeParam::CreateIfValid(kTestUrl, headers.get());
EXPECT_EQ(params.size(), 3U);
for (const auto& param : params) {
service().SetChallengeForBoundSession(base::DoNothing(), kTestUrl, param);
}
const Session* session =
service().GetSession({SchemefulSite(kTestUrl), Session::Id(kSessionId)});
ASSERT_TRUE(session);
EXPECT_EQ(session->cached_challenge(), "challenge");
session = service().GetSession(
{SchemefulSite(kTestUrl), Session::Id("NonExisted")});
ASSERT_FALSE(session);
}
TEST_F(SessionServiceImplTest, ExpiryExtendedOnUser) {
AddSessionsForTesting({{kSessionId, kRefreshUrlString, kOrigin}});
Session* session =
service().GetSession({SchemefulSite(kTestUrl), Session::Id(kSessionId)});
ASSERT_TRUE(session);
session->set_expiry_date(base::Time::Now() + base::Days(1));
net::TestDelegate delegate;
std::unique_ptr<URLRequest> request =
context()->CreateRequest(kTestUrl, IDLE, &delegate, kDummyAnnotation);
// The request needs to be samesite for it to be considered
// candidate for deferral.
request->set_site_for_cookies(SiteForCookies::FromUrl(kTestUrl));
HttpRequestHeaders extra_headers;
service().ShouldDefer(request.get(), &extra_headers, FirstPartySetMetadata());
EXPECT_GT(session->expiry_date(), base::Time::Now() + base::Days(399));
}
TEST_F(SessionServiceImplTest, NullAccessObserver) {
auto scoped_test_fetcher = ScopedTestRegistrationFetcher::CreateWithSuccess(
kSessionId, kRefreshUrlString, kOrigin);
auto fetch_param = RegistrationFetcherParam::CreateInstanceForTesting(
kTestUrl, {crypto::SignatureVerifier::SignatureAlgorithm::ECDSA_SHA256},
"challenge", /*authorization=*/std::nullopt);
service().RegisterBoundSession(
SessionService::OnAccessCallback(), std::move(fetch_param),
IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
NetLogWithSource(), /*original_request_initiator=*/std::nullopt);
// The access observer was null, so no call is expected
}
TEST_F(SessionServiceImplTest, AccessObserverCalledOnRegistration) {
auto scoped_test_fetcher = ScopedTestRegistrationFetcher::CreateWithSuccess(
kSessionId, kRefreshUrlString, kOrigin);
auto fetch_param = RegistrationFetcherParam::CreateInstanceForTesting(
kTestUrl, {crypto::SignatureVerifier::SignatureAlgorithm::ECDSA_SHA256},
"challenge", /*authorization=*/std::nullopt);
base::test::TestFuture<SessionAccess> future;
service().RegisterBoundSession(
future.GetRepeatingCallback<const SessionAccess&>(),
std::move(fetch_param),
IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
NetLogWithSource(), /*original_request_initiator=*/std::nullopt);
SessionAccess access = future.Take();
EXPECT_EQ(access.access_type, SessionAccess::AccessType::kCreation);
EXPECT_EQ(access.session_key.site, SchemefulSite(kTestUrl));
EXPECT_EQ(access.session_key.id.value(), kSessionId);
EXPECT_TRUE(access.cookies.empty());
}
TEST_F(SessionServiceImplTest, AccessObserverCalledOnDeferral) {
AddSessionsForTesting({{kSessionId, kRefreshUrlString, kOrigin}});
net::TestDelegate delegate;
std::unique_ptr<URLRequest> request =
context()->CreateRequest(kTestUrl, IDLE, &delegate, kDummyAnnotation);
// The request needs to be samesite for it to be considered
// candidate for deferral.
request->set_site_for_cookies(SiteForCookies::FromUrl(kTestUrl));
base::test::TestFuture<SessionAccess> future;
request->SetDeviceBoundSessionAccessCallback(
future.GetRepeatingCallback<const SessionAccess&>());
HttpRequestHeaders extra_headers;
service().ShouldDefer(request.get(), &extra_headers, FirstPartySetMetadata());
SessionAccess access = future.Take();
EXPECT_EQ(access.access_type, SessionAccess::AccessType::kUpdate);
EXPECT_EQ(access.session_key.site, SchemefulSite(kTestUrl));
EXPECT_EQ(access.session_key.id.value(), kSessionId);
EXPECT_TRUE(access.cookies.empty());
}
TEST_F(SessionServiceImplTest, AccessObserverCalledOnSetChallenge) {
AddSessionsForTesting({{kSessionId, kRefreshUrlString, kOrigin}});
scoped_refptr<net::HttpResponseHeaders> headers =
HttpResponseHeaders::Builder({1, 1}, "200 OK").Build();
headers->AddHeader("Sec-Session-Challenge", "\"challenge\";id=\"SessionId\"");
std::vector<SessionChallengeParam> params =
SessionChallengeParam::CreateIfValid(kTestUrl, headers.get());
ASSERT_EQ(params.size(), 1U);
base::test::TestFuture<SessionAccess> future;
service().SetChallengeForBoundSession(
future.GetRepeatingCallback<const SessionAccess&>(), kTestUrl, params[0]);
SessionAccess access = future.Take();
EXPECT_EQ(access.access_type, SessionAccess::AccessType::kUpdate);
EXPECT_EQ(access.session_key.site, SchemefulSite(kTestUrl));
EXPECT_EQ(access.session_key.id.value(), kSessionId);
EXPECT_TRUE(access.cookies.empty());
}
TEST_F(SessionServiceImplTest, GetAllSessions) {
AddSessionsForTesting({{kSessionId, kRefreshUrlString, kOrigin},
{kSessionId2, kRefreshUrlString2, kOrigin2}});
base::test::TestFuture<std::vector<SessionKey>> future;
service().GetAllSessionsAsync(
future.GetCallback<const std::vector<SessionKey>&>());
EXPECT_THAT(future.Take(), UnorderedElementsAre(ExpectId(kSessionId),
ExpectId(kSessionId2)));
}
TEST_F(SessionServiceImplTest, DeleteSession) {
base::HistogramTester histograms;
AddSessionsForTesting({{kSessionId, kRefreshUrlString, kOrigin}});
auto site = SchemefulSite(kTestUrl);
auto session_id = Session::Id(kSessionId);
ASSERT_TRUE(service().GetSession({site, session_id}));
base::test::TestFuture<SessionAccess> future;
service().DeleteSessionAndNotify(
DeletionReason::kClearBrowsingData, {site, session_id},
future.GetRepeatingCallback<const SessionAccess&>());
SessionAccess access = future.Take();
EXPECT_EQ(access.access_type, SessionAccess::AccessType::kTermination);
EXPECT_EQ(access.session_key.site, site);
EXPECT_EQ(access.session_key.id, session_id);
EXPECT_EQ(access.cookies, std::vector<std::string>{"test_cookie"});
histograms.ExpectUniqueSample("Net.DeviceBoundSessions.DeletionReason",
DeletionReason::kClearBrowsingData, 1);
}
TEST_F(SessionServiceImplTest, DeleteAllSessionsByCreationTime) {
base::HistogramTester histograms;
net::SchemefulSite site(kTestUrl);
AddSessionsForTesting({{"SessionA", kRefreshUrlString, kOrigin},
{"SessionB", kRefreshUrlString, kOrigin},
{"SessionC", kRefreshUrlString, kOrigin}});
service()
.GetSession({site, Session::Id("SessionA")})
->set_creation_date(base::Time::Now() - base::Days(6));
service()
.GetSession({site, Session::Id("SessionB")})
->set_creation_date(base::Time::Now() - base::Days(4));
service()
.GetSession({site, Session::Id("SessionC")})
->set_creation_date(base::Time::Now() - base::Days(2));
base::RunLoop run_loop;
service().DeleteAllSessions(DeletionReason::kStoragePartitionCleared,
base::Time::Now() - base::Days(5),
base::Time::Now() - base::Days(3),
/*origin_and_site_matcher=*/
base::NullCallback(), run_loop.QuitClosure());
run_loop.Run();
EXPECT_TRUE(service().GetSession({site, Session::Id("SessionA")}));
EXPECT_FALSE(service().GetSession({site, Session::Id("SessionB")}));
EXPECT_TRUE(service().GetSession({site, Session::Id("SessionC")}));
histograms.ExpectUniqueSample("Net.DeviceBoundSessions.DeletionReason",
DeletionReason::kStoragePartitionCleared, 1);
}
TEST_F(SessionServiceImplTest, DeleteAllSessionsBySite) {
GURL url_a("https://a_example.com");
GURL url_b("https://b_example.com");
AddSessionsForTesting({{kSessionId, url_a.spec(), "https://a_example.com"},
{kSessionId, url_b.spec(), "https://b_example.com"}});
SchemefulSite site_a(url_a);
SchemefulSite site_b(url_b);
// `origin_and_site_matcher` only matches `site_a`
base::RepeatingCallback<bool(const url::Origin&, const net::SchemefulSite&)>
origin_and_site_matcher = base::BindRepeating(
[](const net::SchemefulSite& site_a, const url::Origin& origin,
const net::SchemefulSite& site) { return site == site_a; },
site_a);
base::RunLoop run_loop;
service().DeleteAllSessions(DeletionReason::kStoragePartitionCleared,
/*created_after_time=*/std::nullopt,
/*created_before_time=*/std::nullopt,
origin_and_site_matcher, run_loop.QuitClosure());
run_loop.Run();
EXPECT_FALSE(service().GetSession({site_a, Session::Id(kSessionId)}));
EXPECT_TRUE(service().GetSession({site_b, Session::Id(kSessionId)}));
}
TEST_F(SessionServiceImplTest, DeleteAllSessionsByOrigin) {
GURL url_a("https://example.com:1234");
GURL url_b("https://example.com:5678");
AddSessionsForTesting(
{{kSessionId, url_a.spec(), "https://example.com:1234"},
{kSessionId2, url_b.spec(), "https://example.com:5678"}});
// Both sessions have the same site, but different origins.
SchemefulSite site(url_a);
// `origin_and_site_matcher` only matches the first origin.
base::RepeatingCallback<bool(const url::Origin&, const net::SchemefulSite&)>
origin_and_site_matcher = base::BindRepeating(
[](const url::Origin& origin_a, const url::Origin& origin,
const net::SchemefulSite& site) { return origin == origin_a; },
url::Origin::Create(url_a));
base::RunLoop run_loop;
service().DeleteAllSessions(DeletionReason::kStoragePartitionCleared,
/*created_after_time=*/std::nullopt,
/*created_before_time=*/std::nullopt,
origin_and_site_matcher, run_loop.QuitClosure());
run_loop.Run();
EXPECT_FALSE(service().GetSession({site, Session::Id(kSessionId)}));
EXPECT_TRUE(service().GetSession({site, Session::Id(kSessionId2)}));
}
TEST_F(SessionServiceImplTest, TestDeferWithRequestRestart) {
AddSessionsForTesting({{kSessionId, kRefreshUrlString, kOrigin}});
SchemefulSite site(kTestUrl);
ASSERT_TRUE(service().GetSession({site, Session::Id(kSessionId)}));
// Create a request to kTestUrl and defer it.
net::TestDelegate delegate;
std::unique_ptr<URLRequest> request =
context()->CreateRequest(kTestUrl, IDLE, &delegate, kDummyAnnotation);
// The request needs to be samesite for it to be considered
// candidate for deferral.
request->set_site_for_cookies(SiteForCookies::FromUrl(kTestUrl));
HttpRequestHeaders extra_headers;
std::optional<SessionService::DeferralParams> maybe_deferral =
service().ShouldDefer(request.get(), &extra_headers,
FirstPartySetMetadata());
ASSERT_TRUE(maybe_deferral);
EXPECT_FALSE(maybe_deferral->is_pending_initialization);
EXPECT_EQ(**maybe_deferral->session_id, kSessionId);
// Defer the request.
// Set AccessCallback for DeferRequestForRefresh().
FakeDeviceBoundSessionObserver observer;
request->SetDeviceBoundSessionAccessCallback(observer.GetCallback());
// Set up the fetcher for a successful refresh.
auto scoped_test_fetcher = ScopedTestRegistrationFetcher::CreateWithSuccess(
kSessionId, kRefreshUrlString, kOrigin);
base::test::TestFuture<SessionService::RefreshResult> future;
service().DeferRequestForRefresh(
request.get(), SessionService::DeferralParams(Session::Id(kSessionId)),
future.GetCallback());
// Check access callback triggered by DeferRequestForRefresh.
SessionAccess expected_access{SessionAccess::AccessType::kCreation,
SessionKey(site, Session::Id(kSessionId))};
ASSERT_THAT(
observer.notifications(),
ElementsAre(SessionAccess{SessionAccess::AccessType::kUpdate,
SessionKey(site, Session::Id(kSessionId))}));
// Check that the request successfully refreshed.
EXPECT_EQ(future.Take(), SessionService::RefreshResult::kRefreshed);
}
TEST_F(SessionServiceImplTest, TestDeferWithRequestContinue_FatalError) {
AddSessionsForTesting({{kSessionId, kRefreshUrlString, kOrigin}});
SchemefulSite site_1(kTestUrl);
ASSERT_TRUE(service().GetSession({site_1, Session::Id(kSessionId)}));
// Create a request to kTestUrl and defer it.
net::TestDelegate delegate;
std::unique_ptr<URLRequest> request =
context()->CreateRequest(kTestUrl, IDLE, &delegate, kDummyAnnotation);
// The request needs to be samesite for it to be considered
// candidate for deferral.
request->set_site_for_cookies(SiteForCookies::FromUrl(kTestUrl));
HttpRequestHeaders extra_headers;
std::optional<SessionService::DeferralParams> maybe_deferral =
service().ShouldDefer(request.get(), &extra_headers,
FirstPartySetMetadata());
ASSERT_TRUE(maybe_deferral);
EXPECT_FALSE(maybe_deferral->is_pending_initialization);
EXPECT_EQ(**maybe_deferral->session_id, kSessionId);
// Defer the request.
// Set AccessCallback for DeferRequestForRefresh().
FakeDeviceBoundSessionObserver observer;
request->SetDeviceBoundSessionAccessCallback(observer.GetCallback());
base::test::TestFuture<SessionService::RefreshResult> future_2;
// Set up a null fetcher for failure refresh.
auto scoped_null_fetcher = ScopedTestRegistrationFetcher::CreateWithFailure(
SessionError::ErrorType::kPersistentHttpError, kRefreshUrlString);
service().DeferRequestForRefresh(
request.get(), SessionService::DeferralParams(Session::Id(kSessionId)),
future_2.GetCallback());
// Check access callback triggered by DeferRequestForRefresh.
ASSERT_THAT(
observer.notifications(),
ElementsAre(SessionAccess{SessionAccess::AccessType::kUpdate,
SessionKey(site_1, Session::Id(kSessionId))},
SessionAccess{SessionAccess::AccessType::kTermination,
SessionKey(site_1, Session::Id(kSessionId)),
std::vector<std::string>{"test_cookie"}}));
EXPECT_EQ(future_2.Take(), SessionService::RefreshResult::kFatalError);
}
TEST_F(SessionServiceImplTest, TestDeferWithRequestContinue_NonFatalError) {
AddSessionsForTesting({{kSessionId, kRefreshUrlString, kOrigin}});
SchemefulSite site_1(kTestUrl);
ASSERT_TRUE(service().GetSession({site_1, Session::Id(kSessionId)}));
// Create a request to kTestUrl and defer it.
net::TestDelegate delegate;
std::unique_ptr<URLRequest> request =
context()->CreateRequest(kTestUrl, IDLE, &delegate, kDummyAnnotation);
// The request needs to be samesite for it to be considered
// candidate for deferral.
request->set_site_for_cookies(SiteForCookies::FromUrl(kTestUrl));
HttpRequestHeaders extra_headers;
std::optional<SessionService::DeferralParams> maybe_deferral =
service().ShouldDefer(request.get(), &extra_headers,
FirstPartySetMetadata());
ASSERT_TRUE(maybe_deferral);
EXPECT_FALSE(maybe_deferral->is_pending_initialization);
EXPECT_EQ(**maybe_deferral->session_id, kSessionId);
// Defer the request.
// Set AccessCallback for DeferRequestForRefresh().
FakeDeviceBoundSessionObserver observer;
request->SetDeviceBoundSessionAccessCallback(observer.GetCallback());
base::test::TestFuture<SessionService::RefreshResult> future;
// Set up a null fetcher for failure refresh.
auto scoped_null_fetcher = ScopedTestRegistrationFetcher::CreateWithFailure(
SessionError::ErrorType::kNetError, kRefreshUrlString);
service().DeferRequestForRefresh(
request.get(), SessionService::DeferralParams(Session::Id(kSessionId)),
future.GetCallback());
// Check access callback triggered by DeferRequestForRefresh.
ASSERT_THAT(
observer.notifications(),
ElementsAre(SessionAccess{SessionAccess::AccessType::kUpdate,
SessionKey(site_1, Session::Id(kSessionId))}));
// Check that the refresh failed.
EXPECT_EQ(future.Take(), SessionService::RefreshResult::kUnreachable);
}
TEST_F(SessionServiceImplTest, RefreshWithNewSessionId) {
// Register a session with kSessionId.
AddSessionsForTesting({{kSessionId, kRefreshUrlString, kOrigin}});
auto site = SchemefulSite(kTestUrl);
ASSERT_TRUE(service().GetSession({site, Session::Id(kSessionId)}));
// Create a request and defer it.
net::TestDelegate delegate;
std::unique_ptr<URLRequest> request =
context()->CreateRequest(kTestUrl, IDLE, &delegate, kDummyAnnotation);
// The request needs to be samesite for it to be considered
// candidate for deferral.
request->set_site_for_cookies(SiteForCookies::FromUrl(kTestUrl));
HttpRequestHeaders extra_headers;
std::optional<SessionService::DeferralParams> maybe_deferral =
service().ShouldDefer(request.get(), &extra_headers,
FirstPartySetMetadata());
ASSERT_TRUE(maybe_deferral);
EXPECT_FALSE(maybe_deferral->is_pending_initialization);
EXPECT_EQ(**maybe_deferral->session_id, kSessionId);
// Defer the request.
// Set AccessCallback for DeferRequestForRefresh().
FakeDeviceBoundSessionObserver observer;
request->SetDeviceBoundSessionAccessCallback(observer.GetCallback());
base::test::TestFuture<SessionService::RefreshResult> future;
// Set up the fetcher for a failed refresh due to a new session ID
// which doesn't equal to the refreshing one.
auto scoped_test_fetcher = ScopedTestRegistrationFetcher::CreateWithFailure(
SessionError::ErrorType::kInvalidSessionId, kRefreshUrlString);
service().DeferRequestForRefresh(
request.get(), SessionService::DeferralParams(Session::Id(kSessionId)),
future.GetCallback());
// Check access callback triggered by DeferRequestForRefresh.
EXPECT_THAT(
observer.notifications(),
ElementsAre(SessionAccess{SessionAccess::AccessType::kUpdate,
SessionKey(site, Session::Id(kSessionId))},
SessionAccess{SessionAccess::AccessType::kTermination,
SessionKey(site, Session::Id(kSessionId)),
std::vector<std::string>{"test_cookie"}}));
// Check session hits fatal error
EXPECT_EQ(future.Take(), SessionService::RefreshResult::kFatalError);
ASSERT_FALSE(service().GetSession({site, Session::Id(kSessionId2)}));
ASSERT_FALSE(service().GetSession({site, Session::Id(kSessionId)}));
}
TEST_F(SessionServiceImplTest, RefreshWithInvalidParams) {
// Register a session with kSessionId.
AddSessionsForTesting({{kSessionId, kRefreshUrlString, kOrigin}});
auto site = SchemefulSite(kTestUrl);
ASSERT_TRUE(service().GetSession({site, Session::Id(kSessionId)}));
// Create a request and defer it.
net::TestDelegate delegate;
std::unique_ptr<URLRequest> request =
context()->CreateRequest(kTestUrl, IDLE, &delegate, kDummyAnnotation);
// The request needs to be samesite for it to be considered
// candidate for deferral.
request->set_site_for_cookies(SiteForCookies::FromUrl(kTestUrl));
HttpRequestHeaders extra_headers;
std::optional<SessionService::DeferralParams> maybe_deferral =
service().ShouldDefer(request.get(), &extra_headers,
FirstPartySetMetadata());
ASSERT_TRUE(maybe_deferral);
EXPECT_FALSE(maybe_deferral->is_pending_initialization);
EXPECT_EQ(**maybe_deferral->session_id, kSessionId);
// Defer the request.
// Set AccessCallback for DeferRequestForRefresh().
FakeDeviceBoundSessionObserver observer;
request->SetDeviceBoundSessionAccessCallback(observer.GetCallback());
base::test::TestFuture<SessionService::RefreshResult> future;
// Set up the fetcher for a successful refresh, but with invalid
// parameters (e.g. doesn't specify any bound credentials).
ScopedTestRegistrationFetcher scoped_test_fetcher(base::BindRepeating([]() {
return base::expected<SessionParams, SessionError>(
SessionParams(kSessionId, GURL(), "", SessionParams::Scope(),
std::vector<SessionParams::Credential>(),
unexportable_keys::UnexportableKeyId()));
}));
service().DeferRequestForRefresh(
request.get(), SessionService::DeferralParams(Session::Id(kSessionId)),
future.GetCallback());
// Check access callback triggered by DeferRequestForRefresh.
EXPECT_THAT(
observer.notifications(),
ElementsAre(SessionAccess{SessionAccess::AccessType::kUpdate,
SessionKey(site, Session::Id(kSessionId))},
SessionAccess{SessionAccess::AccessType::kTermination,
SessionKey(site, Session::Id(kSessionId)),
std::vector<std::string>{"test_cookie"}}));
// Check the session refresh fails.
EXPECT_EQ(future.Take(), SessionService::RefreshResult::kFatalError);
ASSERT_FALSE(service().GetSession({site, Session::Id(kSessionId)}));
}
TEST_F(SessionServiceImplTest, SessionTerminationFromContinueFalse) {
AddSessionsForTesting({{kSessionId, kRefreshUrlString, kOrigin}});
ASSERT_TRUE(
service().GetSession({SchemefulSite(kTestUrl), Session::Id(kSessionId)}));
auto scoped_fetcher = ScopedTestRegistrationFetcher::CreateWithTermination(
kSessionId, kRefreshUrlString);
// Create a request and defer it.
net::TestDelegate delegate;
std::unique_ptr<URLRequest> request =
context()->CreateRequest(kTestUrl, IDLE, &delegate, kDummyAnnotation);
// The request needs to be samesite for it to be considered
// candidate for deferral.
request->set_site_for_cookies(SiteForCookies::FromUrl(kTestUrl));
base::test::TestFuture<SessionService::RefreshResult> future;
service().DeferRequestForRefresh(
request.get(), SessionService::DeferralParams(Session::Id(kSessionId)),
future.GetCallback());
EXPECT_EQ(future.Take(), SessionService::RefreshResult::kFatalError);
EXPECT_FALSE(
service().GetSession({SchemefulSite(kTestUrl), Session::Id(kSessionId)}));
}
TEST_F(SessionServiceImplTest, NetLogRegistration) {
RecordingNetLogObserver observer;
auto scoped_test_fetcher = ScopedTestRegistrationFetcher::CreateWithSuccess(
kSessionId, kRefreshUrlString, kOrigin);
auto fetch_param = RegistrationFetcherParam::CreateInstanceForTesting(
kTestUrl, {crypto::SignatureVerifier::SignatureAlgorithm::ECDSA_SHA256},
"challenge", /*authorization=*/std::nullopt);
service().RegisterBoundSession(
base::DoNothing(), std::move(fetch_param),
IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
NetLogWithSource::Make(NetLogSourceType::URL_REQUEST),
/*original_request_initiator=*/std::nullopt);
EXPECT_EQ(
observer.GetEntriesWithType(NetLogEventType::DBSC_REGISTRATION_REQUEST)
.size(),
1u);
}
TEST_F(SessionServiceImplTest, NetLogRefresh) {
AddSessionsForTesting({{kSessionId, kRefreshUrlString, kOrigin}});
SchemefulSite site(kTestUrl);
ASSERT_TRUE(service().GetSession({site, Session::Id(kSessionId)}));
net::TestDelegate delegate;
std::unique_ptr<URLRequest> request =
context()->CreateRequest(kTestUrl, IDLE, &delegate, kDummyAnnotation);
request->set_site_for_cookies(SiteForCookies::FromUrl(kTestUrl));
base::test::TestFuture<SessionService::RefreshResult> future;
RecordingNetLogObserver observer;
auto scoped_test_fetcher = ScopedTestRegistrationFetcher::CreateWithSuccess(
kSessionId, kRefreshUrlString, kOrigin);
service().DeferRequestForRefresh(
request.get(), SessionService::DeferralParams(Session::Id(kSessionId)),
future.GetCallback());
EXPECT_EQ(
observer.GetEntriesWithType(NetLogEventType::DBSC_REFRESH_REQUEST).size(),
1u);
}
TEST_F(SessionServiceImplTest, SessionRefreshQuota) {
AddSessionsForTesting({{kSessionId, kRefreshUrlString, kOrigin}});
auto scoped_test_fetcher = ScopedTestRegistrationFetcher::CreateWithSuccess(
kSessionId, kRefreshUrlString, kOrigin);
net::TestDelegate delegate;
std::unique_ptr<URLRequest> request =
context()->CreateRequest(kTestUrl, IDLE, &delegate, kDummyAnnotation);
request->set_site_for_cookies(SiteForCookies::FromUrl(kTestUrl));
// The first refresh succeeds
{
base::test::TestFuture<SessionService::RefreshResult> future;
service().DeferRequestForRefresh(
request.get(), SessionService::DeferralParams(Session::Id(kSessionId)),
future.GetCallback());
EXPECT_EQ(future.Take(), SessionService::RefreshResult::kRefreshed);
}
// The second refresh succeeds
{
base::test::TestFuture<SessionService::RefreshResult> future;
service().DeferRequestForRefresh(
request.get(), SessionService::DeferralParams(Session::Id(kSessionId)),
future.GetCallback());
EXPECT_EQ(future.Take(), SessionService::RefreshResult::kRefreshed);
}
// The third refresh is throttled
{
base::test::TestFuture<SessionService::RefreshResult> future;
service().DeferRequestForRefresh(
request.get(), SessionService::DeferralParams(Session::Id(kSessionId)),
future.GetCallback());
EXPECT_EQ(future.Take(), SessionService::RefreshResult::kQuotaExceeded);
}
// After five minutes, the quota is restored and the fourth refresh
// succeeds.
FastForwardBy(base::Minutes(5));
{
base::test::TestFuture<SessionService::RefreshResult> future;
service().DeferRequestForRefresh(
request.get(), SessionService::DeferralParams(Session::Id(kSessionId)),
future.GetCallback());
EXPECT_EQ(future.Take(), SessionService::RefreshResult::kRefreshed);
}
}
TEST_F(SessionServiceImplNoRefreshQuotaTest, SessionRefreshQuotaDisabled) {
AddSessionsForTesting({{kSessionId, kRefreshUrlString, kOrigin}});
auto scoped_test_fetcher = ScopedTestRegistrationFetcher::CreateWithSuccess(
kSessionId, kRefreshUrlString, kOrigin);
net::TestDelegate delegate;
std::unique_ptr<URLRequest> request =
context()->CreateRequest(kTestUrl, IDLE, &delegate, kDummyAnnotation);
request->set_site_for_cookies(SiteForCookies::FromUrl(kTestUrl));
// The third refresh is not throttled because the refresh quota is disabled.
for (size_t i = 0; i < 3; i++) {
base::test::TestFuture<SessionService::RefreshResult> future;
service().DeferRequestForRefresh(
request.get(), SessionService::DeferralParams(Session::Id(kSessionId)),
future.GetCallback());
EXPECT_EQ(future.Take(), SessionService::RefreshResult::kRefreshed);
}
}
TEST_F(SessionServiceImplTest, SessionBackoff) {
AddSessionsForTesting({{kSessionId, kRefreshUrlString, kOrigin}});
auto scoped_test_fetcher = ScopedTestRegistrationFetcher::CreateWithFailure(
SessionError::ErrorType::kTransientHttpError, kRefreshUrlString);
net::TestDelegate delegate;
std::unique_ptr<URLRequest> request =
context()->CreateRequest(kTestUrl, IDLE, &delegate, kDummyAnnotation);
request->set_site_for_cookies(SiteForCookies::FromUrl(kTestUrl));
HttpRequestHeaders extra_headers;
EXPECT_TRUE(service().ShouldDefer(request.get(), &extra_headers,
FirstPartySetMetadata()));
// Do four failing refreshes.
for (size_t i = 0; i < 4; i++) {
FastForwardBy(base::Minutes(5));
base::test::TestFuture<SessionService::RefreshResult> future;
service().DeferRequestForRefresh(
request.get(), SessionService::DeferralParams(Session::Id(kSessionId)),
future.GetCallback());
EXPECT_EQ(future.Take(), SessionService::RefreshResult::kServerError);
}
// Backoff should prevent us from deferring anymore.
base::test::TestFuture<SessionService::RefreshResult> future;
service().DeferRequestForRefresh(
request.get(), SessionService::DeferralParams(Session::Id(kSessionId)),
future.GetCallback());
EXPECT_EQ(future.Take(), SessionService::RefreshResult::kUnreachable);
}
TEST_F(SessionServiceImplTest, RepeatedDeferral) {
AddSessionsForTesting({{kSessionId, kRefreshUrlString, kOrigin}});
net::TestDelegate delegate;
std::unique_ptr<URLRequest> request =
context()->CreateRequest(kTestUrl, IDLE, &delegate, kDummyAnnotation);
// The request needs to be samesite for it to be considered
// candidate for deferral.
request->set_site_for_cookies(SiteForCookies::FromUrl(kTestUrl));
HttpRequestHeaders extra_headers;
std::optional<SessionService::DeferralParams> maybe_deferral =
service().ShouldDefer(request.get(), &extra_headers,
FirstPartySetMetadata());
ASSERT_TRUE(maybe_deferral);
EXPECT_FALSE(maybe_deferral->is_pending_initialization);
EXPECT_EQ(**maybe_deferral->session_id, kSessionId);
request->AddDeviceBoundSessionDeferral(
SessionKey{SchemefulSite(kTestUrl), Session::Id(kSessionId)},
SessionService::RefreshResult::kRefreshed);
maybe_deferral = service().ShouldDefer(request.get(), &extra_headers,
FirstPartySetMetadata());
EXPECT_FALSE(maybe_deferral);
}
TEST_F(SessionServiceImplTest, AddsDebugHeader) {
AddSessionsForTesting({{kSessionId, kRefreshUrlString, kOrigin}});
AddSessionsForTesting({{kSessionId2, kRefreshUrlString, kOrigin}});
net::TestDelegate delegate;
std::unique_ptr<URLRequest> request =
context()->CreateRequest(kTestUrl, IDLE, &delegate, kDummyAnnotation);
// The request needs to be samesite for it to be considered
// candidate for deferral.
request->set_site_for_cookies(SiteForCookies::FromUrl(kTestUrl));
// Simulate failed refresh for two different sessions
request->AddDeviceBoundSessionDeferral(
SessionKey{SchemefulSite(kTestUrl), Session::Id(kSessionId)},
SessionService::RefreshResult::kUnreachable);
request->AddDeviceBoundSessionDeferral(
SessionKey{SchemefulSite(kTestUrl), Session::Id(kSessionId2)},
SessionService::RefreshResult::kQuotaExceeded);
HttpRequestHeaders extra_headers;
std::optional<SessionService::DeferralParams> maybe_deferral =
service().ShouldDefer(request.get(), &extra_headers,
FirstPartySetMetadata());
EXPECT_FALSE(maybe_deferral);
std::optional<std::string> debug_header =
extra_headers.GetHeader("Secure-Session-Skipped");
EXPECT_TRUE(debug_header.has_value());
EXPECT_EQ(*debug_header,
"unreachable;session_identifier=\"SessionId\", "
"quota_exceeded;session_identifier=\"SessionId2\"");
}
TEST_F(SessionServiceImplTest, NoDebugHeaderOnSuccess) {
AddSessionsForTesting({{kSessionId, kRefreshUrlString, kOrigin}});
net::TestDelegate delegate;
std::unique_ptr<URLRequest> request =
context()->CreateRequest(kTestUrl, IDLE, &delegate, kDummyAnnotation);
// The request needs to be samesite for it to be considered
// candidate for deferral.
request->set_site_for_cookies(SiteForCookies::FromUrl(kTestUrl));
// Simulate success refresh.
request->AddDeviceBoundSessionDeferral(
SessionKey{SchemefulSite(kTestUrl), Session::Id(kSessionId)},
SessionService::RefreshResult::kRefreshed);
HttpRequestHeaders extra_headers;
std::optional<SessionService::DeferralParams> maybe_deferral =
service().ShouldDefer(request.get(), &extra_headers,
FirstPartySetMetadata());
EXPECT_FALSE(maybe_deferral);
std::optional<std::string> debug_header =
extra_headers.GetHeader("Secure-Session-Skipped");
EXPECT_FALSE(debug_header.has_value());
}
} // namespace
class SessionServiceImplWithStoreTest : public TestWithTaskEnvironment {
public:
SessionServiceImplWithStoreTest()
: context_(CreateTestURLRequestContextBuilder()->Build()),
store_(std::make_unique<StrictMock<SessionStoreMock>>()),
service_(*UnexportableKeyServiceFactory::GetInstance()->GetShared(),
context_.get(),
store_.get()) {}
SessionServiceImpl& service() { return service_; }
StrictMock<SessionStoreMock>& store() { return *store_; }
void OnSessionsLoaded() {
service().OnLoadSessionsComplete(SessionStore::SessionsMap());
}
void FinishLoadingSessions(SessionStore::SessionsMap loaded_sessions) {
service().OnLoadSessionsComplete(std::move(loaded_sessions));
}
size_t GetSiteSessionsCount(const SchemefulSite& site) {
auto [begin, end] = service().GetSessionsForSite(site);
return std::distance(begin, end);
}
URLRequestContext* context() { return context_.get(); }
private:
crypto::ScopedFakeUnexportableKeyProvider scoped_fake_key_provider_;
std::unique_ptr<URLRequestContext> context_;
std::unique_ptr<StrictMock<SessionStoreMock>> store_;
SessionServiceImpl service_;
};
TEST_F(SessionServiceImplWithStoreTest, UsesSessionStore) {
{
InSequence seq;
EXPECT_CALL(store(), LoadSessions)
.Times(1)
.WillOnce(
Invoke(this, &SessionServiceImplWithStoreTest::OnSessionsLoaded));
EXPECT_CALL(store(), SaveSession).Times(1);
EXPECT_CALL(store(), DeleteSession).Times(1);
}
// Will invoke the store's load session method.
service().LoadSessionsAsync();
auto scoped_test_fetcher = ScopedTestRegistrationFetcher::CreateWithSuccess(
kSessionId, kRefreshUrlString, kOrigin);
auto fetch_param = RegistrationFetcherParam::CreateInstanceForTesting(
kTestUrl, {crypto::SignatureVerifier::SignatureAlgorithm::ECDSA_SHA256},
"challenge", /*authorization=*/std::nullopt);
// Will invoke the store's save session method.
service().RegisterBoundSession(
base::DoNothing(), std::move(fetch_param),
IsolationInfo::CreateTransient(/*nonce=*/std::nullopt),
NetLogWithSource(), /*original_request_initiator=*/std::nullopt);
auto site = SchemefulSite(kTestUrl);
Session* session = service().GetSession({site, Session::Id(kSessionId)});
ASSERT_TRUE(session);
EXPECT_EQ(GetSiteSessionsCount(site), 1u);
session->set_expiry_date(base::Time::Now() - base::Days(1));
// Will invoke the store's delete session method.
EXPECT_EQ(GetSiteSessionsCount(site), 0u);
}
TEST_F(SessionServiceImplWithStoreTest, GetAllSessionsWaitsForSessionsToLoad) {
// Start loading
EXPECT_CALL(store(), LoadSessions).Times(1);
service().LoadSessionsAsync();
// Request sessions, which should wait until we finish loading.
base::test::TestFuture<std::vector<SessionKey>> future;
service().GetAllSessionsAsync(
future.GetCallback<const std::vector<SessionKey>&>());
SessionParams::Scope scope;
scope.origin = "https://example.com";
auto session_or_error = Session::CreateIfValid(SessionParams(
"session_id", kTestUrl, "https://example.com/refresh", std::move(scope),
/*creds=*/{}, unexportable_keys::UnexportableKeyId()));
ASSERT_TRUE(session_or_error.has_value());
std::unique_ptr<Session> session = std::move(*session_or_error);
ASSERT_TRUE(session);
// Complete loading. If we did not defer, we'd miss this session.
SessionStore::SessionsMap session_map;
session_map.insert(
{SessionKey{SchemefulSite(kTestUrl), session->id()}, std::move(session)});
FinishLoadingSessions(std::move(session_map));
// But we did defer, so we found it.
EXPECT_THAT(future.Take(), UnorderedElementsAre(ExpectId("session_id")));
}
TEST_F(SessionServiceImplWithStoreTest, RequestsWaitForSessionsToLoad) {
// Start loading
EXPECT_CALL(store(), LoadSessions).Times(1);
service().LoadSessionsAsync();
// Create a request that should be deferred due to initialization not
// having completed.
net::TestDelegate delegate;
std::unique_ptr<URLRequest> request =
context()->CreateRequest(kTestUrl, IDLE, &delegate, kDummyAnnotation);
request->set_site_for_cookies(SiteForCookies::FromUrl(kTestUrl));
HttpRequestHeaders extra_headers;
std::optional<SessionService::DeferralParams> maybe_deferral =
service().ShouldDefer(request.get(), &extra_headers,
FirstPartySetMetadata());
ASSERT_TRUE(maybe_deferral);
EXPECT_TRUE(maybe_deferral->is_pending_initialization);
// Now actually defer the request
base::test::TestFuture<SessionService::RefreshResult> future;
service().DeferRequestForRefresh(request.get(), *maybe_deferral,
future.GetCallback());
EXPECT_FALSE(future.IsReady());
// Complete loading. We should now restart the request.
FinishLoadingSessions(SessionStore::SessionsMap());
EXPECT_EQ(future.Take(), SessionService::RefreshResult::kInitializedService);
}
TEST_F(SessionServiceImplWithStoreTest, SessionKeyRestoredOnUse) {
// Start loading
EXPECT_CALL(store(), LoadSessions).Times(1);
service().LoadSessionsAsync();
base::Time expiry_time = base::Time::Now() + base::Days(1);
proto::Session session_proto;
session_proto.set_id(kSessionId);
session_proto.set_refresh_url(kUrlString);
session_proto.set_should_defer_when_expired(false);
session_proto.set_expiry_time(
expiry_time.ToDeltaSinceWindowsEpoch().InMicroseconds());
session_proto.mutable_session_inclusion_rules()->set_origin(
"https://example.com");
session_proto.mutable_session_inclusion_rules()->set_do_include_site(true);
proto::CookieCraving* craving_proto = session_proto.add_cookie_cravings();
craving_proto->set_name("test_cookie");
craving_proto->set_domain("example.com");
craving_proto->set_path("/");
craving_proto->set_secure(true);
craving_proto->set_httponly(true);
craving_proto->set_source_port(443);
craving_proto->set_creation_time(
base::Time::Now().ToDeltaSinceWindowsEpoch().InMicroseconds());
craving_proto->set_same_site(proto::CookieSameSite::LAX_MODE);
craving_proto->set_source_scheme(proto::CookieSourceScheme::SECURE);
std::unique_ptr<Session> session = Session::CreateFromProto(session_proto);
ASSERT_TRUE(session);
SessionStore::SessionsMap session_map;
session_map.insert(
{SessionKey{SchemefulSite(kTestUrl), session->id()}, std::move(session)});
FinishLoadingSessions(std::move(session_map));
// Create a request that should be deferred due to the session
net::TestDelegate delegate;
std::unique_ptr<URLRequest> request =
context()->CreateRequest(kTestUrl, IDLE, &delegate, kDummyAnnotation);
request->set_site_for_cookies(SiteForCookies::FromUrl(kTestUrl));
HttpRequestHeaders extra_headers;
std::optional<SessionService::DeferralParams> maybe_deferral =
service().ShouldDefer(request.get(), &extra_headers,
FirstPartySetMetadata());
ASSERT_TRUE(maybe_deferral);
EXPECT_EQ(**maybe_deferral->session_id, kSessionId);
// Now actually defer the request
auto scoped_test_fetcher = ScopedTestRegistrationFetcher::CreateWithSuccess(
kSessionId, kUrlString, kOrigin);
EXPECT_CALL(store(), SaveSession(_, _)).Times(1);
EXPECT_CALL(
store(),
RestoreSessionBindingKey(
SessionKey(SchemefulSite(kTestUrl), Session::Id(kSessionId)), _))
.WillOnce(RunOnceCallback<1>(unexportable_keys::UnexportableKeyId()));
base::test::TestFuture<SessionService::RefreshResult> future;
service().DeferRequestForRefresh(request.get(), *maybe_deferral,
future.GetCallback());
EXPECT_EQ(future.Take(), SessionService::RefreshResult::kRefreshed);
}
} // namespace net::device_bound_sessions