1 // Copyright 2024 The Chromium Authors
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "net/device_bound_sessions/session_service_impl.h"
6
7 #include "base/test/test_future.h"
8 #include "crypto/scoped_mock_unexportable_key_provider.h"
9 #include "net/device_bound_sessions/mock_session_store.h"
10 #include "net/device_bound_sessions/session_store.h"
11 #include "net/device_bound_sessions/unexportable_key_service_factory.h"
12 #include "net/test/test_with_task_environment.h"
13 #include "net/url_request/url_request_context_builder.h"
14 #include "net/url_request/url_request_test_util.h"
15 #include "testing/gtest/include/gtest/gtest.h"
16 #include "url/gurl.h"
17
18 using ::testing::InSequence;
19 using ::testing::Invoke;
20 using ::testing::StrictMock;
21 using ::testing::UnorderedElementsAre;
22
23 namespace net::device_bound_sessions {
24
25 namespace {
26
27 constexpr net::NetworkTrafficAnnotationTag kDummyAnnotation =
28 net::DefineNetworkTrafficAnnotation("dbsc_registration", "");
29
30 // Constant variables
31 constexpr char kUrlString[] = "https://example.com";
32 const GURL kTestUrl(kUrlString);
33 const std::string kSessionId = "SessionId";
34 const std::string kChallenge = "challenge";
35
36 // Matcher for SessionKeys
ExpectId(std::string_view id)37 auto ExpectId(std::string_view id) {
38 return testing::Field(&SessionKey::id, Session::Id(std::string(id)));
39 }
40
TestFetcher(std::string session_id,std::optional<std::string> referral_session_identifier)41 std::optional<RegistrationFetcher::RegistrationCompleteParams> TestFetcher(
42 std::string session_id,
43 std::optional<std::string> referral_session_identifier) {
44 std::vector<SessionParams::Credential> cookie_credentials;
45 cookie_credentials.push_back(
46 SessionParams::Credential{"test_cookie", "secure"});
47 SessionParams::Scope scope;
48 scope.include_site = true;
49 SessionParams session_params(std::move(session_id), kUrlString,
50 std::move(scope), std::move(cookie_credentials));
51 unexportable_keys::UnexportableKeyId key_id;
52 return std::make_optional<RegistrationFetcher::RegistrationCompleteParams>(
53 std::move(session_params), std::move(key_id), kTestUrl,
54 std::move(referral_session_identifier));
55 }
56
NullFetcher()57 std::optional<RegistrationFetcher::RegistrationCompleteParams> NullFetcher() {
58 return std::nullopt;
59 }
60
TestFetcherFactory(std::string session_id,std::optional<std::string> referral_session_identifier)61 RegistrationFetcher::FetcherType TestFetcherFactory(
62 std::string session_id,
63 std::optional<std::string> referral_session_identifier) {
64 static std::string g_session_id;
65 static std::optional<std::string> g_referral_session_id;
66 g_session_id = std::move(session_id);
67 g_referral_session_id = std::move(referral_session_identifier);
68
69 return []() { return TestFetcher(g_session_id, g_referral_session_id); };
70 }
71
72 class ScopedTestFetcher {
73 public:
ScopedTestFetcher(std::string session_id,std::optional<std::string> referral_session_identifier=std::nullopt)74 explicit ScopedTestFetcher(
75 std::string session_id,
76 std::optional<std::string> referral_session_identifier = std::nullopt) {
77 RegistrationFetcher::SetFetcherForTesting(TestFetcherFactory(
78 std::move(session_id), std::move(referral_session_identifier)));
79 }
80
~ScopedTestFetcher()81 ~ScopedTestFetcher() { RegistrationFetcher::SetFetcherForTesting(nullptr); }
82 };
83
84 class ScopedNullFetcher {
85 public:
ScopedNullFetcher()86 ScopedNullFetcher() {
87 RegistrationFetcher::SetFetcherForTesting(NullFetcher);
88 }
~ScopedNullFetcher()89 ~ScopedNullFetcher() { RegistrationFetcher::SetFetcherForTesting(nullptr); }
90 };
91
92 class SessionServiceImplTest : public TestWithTaskEnvironment {
93 public:
SessionServiceImplTest()94 SessionServiceImplTest()
95 : context_(CreateTestURLRequestContextBuilder()->Build()),
96 service_(*UnexportableKeyServiceFactory::GetInstance()->GetShared(),
97 context_.get(),
98 /*store=*/nullptr) {}
99
context()100 URLRequestContext* context() { return context_.get(); }
service()101 SessionServiceImpl& service() { return service_; }
102
103 private:
104 crypto::ScopedMockUnexportableKeyProvider scoped_mock_key_provider_;
105 std::unique_ptr<URLRequestContext> context_;
106 SessionServiceImpl service_;
107 };
108
109 // Not implemented so test just makes sure it can run
TEST_F(SessionServiceImplTest,TestDefer)110 TEST_F(SessionServiceImplTest, TestDefer) {
111 SessionService::RefreshCompleteCallback cb1 = base::DoNothing();
112 SessionService::RefreshCompleteCallback cb2 = base::DoNothing();
113 net::TestDelegate delegate;
114 std::unique_ptr<URLRequest> request =
115 context()->CreateRequest(kTestUrl, IDLE, &delegate, kDummyAnnotation);
116 service().DeferRequestForRefresh(request.get(), Session::Id("test"),
117 std::move(cb1), std::move(cb2));
118 }
119
TEST_F(SessionServiceImplTest,RegisterSuccess)120 TEST_F(SessionServiceImplTest, RegisterSuccess) {
121 ScopedTestFetcher scoped_test_fetcher(kSessionId);
122 auto fetch_param = RegistrationFetcherParam::CreateInstanceForTesting(
123 kTestUrl, {crypto::SignatureVerifier::SignatureAlgorithm::ECDSA_SHA256},
124 kChallenge,
125 /*authorization=*/std::nullopt);
126 service().RegisterBoundSession(base::DoNothing(), std::move(fetch_param),
127 IsolationInfo::CreateTransient());
128
129 net::TestDelegate delegate;
130 std::unique_ptr<URLRequest> request =
131 context()->CreateRequest(kTestUrl, IDLE, &delegate, kDummyAnnotation);
132
133 // The request needs to be samesite for it to be considered
134 // candidate for deferral.
135 request->set_site_for_cookies(SiteForCookies::FromUrl(kTestUrl));
136
137 std::optional<Session::Id> maybe_id =
138 service().GetAnySessionRequiringDeferral(request.get());
139 ASSERT_TRUE(maybe_id);
140 EXPECT_EQ(**maybe_id, kSessionId);
141 }
142
TEST_F(SessionServiceImplTest,RegisterNoId)143 TEST_F(SessionServiceImplTest, RegisterNoId) {
144 ScopedTestFetcher scoped_test_fetcher(/*session_id=*/"");
145 auto fetch_param = RegistrationFetcherParam::CreateInstanceForTesting(
146 kTestUrl, {crypto::SignatureVerifier::SignatureAlgorithm::ECDSA_SHA256},
147 kChallenge,
148 /*authorization=*/std::nullopt);
149 service().RegisterBoundSession(base::DoNothing(), std::move(fetch_param),
150 IsolationInfo::CreateTransient());
151
152 net::TestDelegate delegate;
153 std::unique_ptr<URLRequest> request =
154 context()->CreateRequest(kTestUrl, IDLE, &delegate, kDummyAnnotation);
155
156 request->set_site_for_cookies(SiteForCookies::FromUrl(kTestUrl));
157
158 std::optional<Session::Id> maybe_id =
159 service().GetAnySessionRequiringDeferral(request.get());
160 // session_id is empty, so should not be valid
161 EXPECT_FALSE(maybe_id);
162 }
163
TEST_F(SessionServiceImplTest,RegisterNullFetcher)164 TEST_F(SessionServiceImplTest, RegisterNullFetcher) {
165 ScopedNullFetcher scopedNullFetcher;
166 auto fetch_param = RegistrationFetcherParam::CreateInstanceForTesting(
167 kTestUrl, {crypto::SignatureVerifier::SignatureAlgorithm::ECDSA_SHA256},
168 kChallenge,
169 /*authorization=*/std::nullopt);
170 service().RegisterBoundSession(base::DoNothing(), std::move(fetch_param),
171 IsolationInfo::CreateTransient());
172
173 net::TestDelegate delegate;
174 std::unique_ptr<URLRequest> request =
175 context()->CreateRequest(kTestUrl, IDLE, &delegate, kDummyAnnotation);
176
177 request->set_site_for_cookies(SiteForCookies::FromUrl(kTestUrl));
178
179 std::optional<Session::Id> maybe_id =
180 service().GetAnySessionRequiringDeferral(request.get());
181 // NullFetcher, so should not be valid
182 EXPECT_FALSE(maybe_id);
183 }
184
TEST_F(SessionServiceImplTest,SetChallengeForBoundSession)185 TEST_F(SessionServiceImplTest, SetChallengeForBoundSession) {
186 ScopedTestFetcher scoped_test_fetcher(kSessionId);
187 auto fetch_param = RegistrationFetcherParam::CreateInstanceForTesting(
188 kTestUrl, {crypto::SignatureVerifier::SignatureAlgorithm::ECDSA_SHA256},
189 kChallenge,
190 /*authorization=*/std::nullopt);
191 service().RegisterBoundSession(base::DoNothing(), std::move(fetch_param),
192 IsolationInfo::CreateTransient());
193
194 scoped_refptr<net::HttpResponseHeaders> headers =
195 HttpResponseHeaders::Builder({1, 1}, "200 OK").Build();
196 headers->AddHeader(
197 "Sec-Session-Challenge",
198 R"("challenge";id="SessionId", "challenge1";id="NonExisted")");
199 headers->AddHeader("Sec-Session-Challenge", R"("challenge2")");
200
201 std::vector<SessionChallengeParam> params =
202 SessionChallengeParam::CreateIfValid(kTestUrl, headers.get());
203
204 EXPECT_EQ(params.size(), 3U);
205
206 for (const auto& param : params) {
207 service().SetChallengeForBoundSession(base::DoNothing(), kTestUrl, param);
208 }
209
210 const Session* session =
211 service().GetSessionForTesting(SchemefulSite(kTestUrl), kSessionId);
212 ASSERT_TRUE(session);
213 EXPECT_EQ(session->cached_challenge(), "challenge");
214
215 session =
216 service().GetSessionForTesting(SchemefulSite(kTestUrl), "NonExisted");
217 ASSERT_FALSE(session);
218 }
219
TEST_F(SessionServiceImplTest,ExpiryExtendedOnUser)220 TEST_F(SessionServiceImplTest, ExpiryExtendedOnUser) {
221 ScopedTestFetcher scoped_test_fetcher(kSessionId);
222 auto fetch_param = RegistrationFetcherParam::CreateInstanceForTesting(
223 kTestUrl, {crypto::SignatureVerifier::SignatureAlgorithm::ECDSA_SHA256},
224 kChallenge,
225 /*authorization=*/std::nullopt);
226 service().RegisterBoundSession(base::DoNothing(), std::move(fetch_param),
227 IsolationInfo::CreateTransient());
228
229 Session* session =
230 service().GetSessionForTesting(SchemefulSite(kTestUrl), kSessionId);
231 ASSERT_TRUE(session);
232 session->set_expiry_date(base::Time::Now() + base::Days(1));
233
234 net::TestDelegate delegate;
235 std::unique_ptr<URLRequest> request =
236 context()->CreateRequest(kTestUrl, IDLE, &delegate, kDummyAnnotation);
237 // The request needs to be samesite for it to be considered
238 // candidate for deferral.
239 request->set_site_for_cookies(SiteForCookies::FromUrl(kTestUrl));
240
241 service().GetAnySessionRequiringDeferral(request.get());
242
243 EXPECT_GT(session->expiry_date(), base::Time::Now() + base::Days(399));
244 }
245
TEST_F(SessionServiceImplTest,NullAccessObserver)246 TEST_F(SessionServiceImplTest, NullAccessObserver) {
247 ScopedTestFetcher scoped_test_fetcher(kSessionId);
248
249 auto fetch_param = RegistrationFetcherParam::CreateInstanceForTesting(
250 kTestUrl, {crypto::SignatureVerifier::SignatureAlgorithm::ECDSA_SHA256},
251 "challenge", /*authorization=*/std::nullopt);
252 service().RegisterBoundSession(SessionService::OnAccessCallback(),
253 std::move(fetch_param),
254 IsolationInfo::CreateTransient());
255
256 // The access observer was null, so no call is expected
257 }
258
TEST_F(SessionServiceImplTest,AccessObserverCalledOnRegistration)259 TEST_F(SessionServiceImplTest, AccessObserverCalledOnRegistration) {
260 ScopedTestFetcher scoped_test_fetcher(kSessionId);
261
262 auto fetch_param = RegistrationFetcherParam::CreateInstanceForTesting(
263 kTestUrl, {crypto::SignatureVerifier::SignatureAlgorithm::ECDSA_SHA256},
264 "challenge", /*authorization=*/std::nullopt);
265 base::test::TestFuture<SessionKey> future;
266 service().RegisterBoundSession(
267 future.GetRepeatingCallback<const SessionKey&>(), std::move(fetch_param),
268 IsolationInfo::CreateTransient());
269
270 SessionKey session_key = future.Take();
271 EXPECT_EQ(session_key.site, SchemefulSite(kTestUrl));
272 EXPECT_EQ(session_key.id.value(), kSessionId);
273 }
274
TEST_F(SessionServiceImplTest,AccessObserverCalledOnDeferral)275 TEST_F(SessionServiceImplTest, AccessObserverCalledOnDeferral) {
276 ScopedTestFetcher scoped_test_fetcher(kSessionId);
277 auto fetch_param = RegistrationFetcherParam::CreateInstanceForTesting(
278 kTestUrl, {crypto::SignatureVerifier::SignatureAlgorithm::ECDSA_SHA256},
279 "challenge", /*authorization=*/std::nullopt);
280 net::TestDelegate delegate;
281 std::unique_ptr<URLRequest> request =
282 context()->CreateRequest(kTestUrl, IDLE, &delegate, kDummyAnnotation);
283 service().RegisterBoundSession(base::DoNothing(), std::move(fetch_param),
284 IsolationInfo::CreateTransient());
285
286 // The request needs to be samesite for it to be considered
287 // candidate for deferral.
288 request->set_site_for_cookies(SiteForCookies::FromUrl(kTestUrl));
289
290 base::test::TestFuture<SessionKey> future;
291 request->SetDeviceBoundSessionAccessCallback(
292 future.GetRepeatingCallback<const SessionKey&>());
293 service().GetAnySessionRequiringDeferral(request.get());
294
295 SessionKey session_key = future.Take();
296 EXPECT_EQ(session_key.site, SchemefulSite(kTestUrl));
297 EXPECT_EQ(session_key.id.value(), kSessionId);
298 }
299
TEST_F(SessionServiceImplTest,AccessObserverCalledOnSetChallenge)300 TEST_F(SessionServiceImplTest, AccessObserverCalledOnSetChallenge) {
301 ScopedTestFetcher scoped_test_fetcher(kSessionId);
302
303 auto fetch_param = RegistrationFetcherParam::CreateInstanceForTesting(
304 kTestUrl, {crypto::SignatureVerifier::SignatureAlgorithm::ECDSA_SHA256},
305 "challenge", /*authorization=*/std::nullopt);
306 service().RegisterBoundSession(base::DoNothing(), std::move(fetch_param),
307 IsolationInfo::CreateTransient());
308
309 scoped_refptr<net::HttpResponseHeaders> headers =
310 HttpResponseHeaders::Builder({1, 1}, "200 OK").Build();
311 headers->AddHeader("Sec-Session-Challenge", "\"challenge\";id=\"SessionId\"");
312
313 std::vector<SessionChallengeParam> params =
314 SessionChallengeParam::CreateIfValid(kTestUrl, headers.get());
315 ASSERT_EQ(params.size(), 1U);
316
317 base::test::TestFuture<SessionKey> future;
318 service().SetChallengeForBoundSession(
319 future.GetRepeatingCallback<const SessionKey&>(), kTestUrl, params[0]);
320
321 SessionKey session_key = future.Take();
322 EXPECT_EQ(session_key.site, SchemefulSite(kTestUrl));
323 EXPECT_EQ(session_key.id.value(), kSessionId);
324 }
325
TEST_F(SessionServiceImplTest,ReferralSessionIdentifier)326 TEST_F(SessionServiceImplTest, ReferralSessionIdentifier) {
327 // Register a session with Id `kSessionId`.
328 {
329 ScopedTestFetcher scoped_test_fetcher(kSessionId);
330
331 auto fetch_param = RegistrationFetcherParam::CreateInstanceForTesting(
332 kTestUrl, {crypto::SignatureVerifier::SignatureAlgorithm::ECDSA_SHA256},
333 "challenge", /*authorization=*/std::nullopt);
334 service().RegisterBoundSession(base::DoNothing(), std::move(fetch_param),
335 IsolationInfo::CreateTransient());
336 }
337
338 auto site = SchemefulSite(kTestUrl);
339 ASSERT_TRUE(service().GetSessionForTesting(site, kSessionId));
340
341 // Register a session with new Id to replace the former session.
342 std::string session_id("NewSessionId");
343 {
344 ScopedTestFetcher scoped_test_fetcher(session_id, kSessionId);
345
346 auto fetch_param = RegistrationFetcherParam::CreateInstanceForTesting(
347 kTestUrl, {crypto::SignatureVerifier::SignatureAlgorithm::ECDSA_SHA256},
348 "challenge", /*authorization=*/std::nullopt);
349 service().RegisterBoundSession(base::DoNothing(), std::move(fetch_param),
350 IsolationInfo::CreateTransient());
351 }
352
353 ASSERT_TRUE(service().GetSessionForTesting(site, session_id));
354 ASSERT_FALSE(service().GetSessionForTesting(site, kSessionId));
355 }
356
TEST_F(SessionServiceImplTest,GetAllSessions)357 TEST_F(SessionServiceImplTest, GetAllSessions) {
358 const std::string session_id_1("SessionId");
359 {
360 ScopedTestFetcher scoped_test_fetcher(session_id_1);
361
362 auto fetch_param = RegistrationFetcherParam::CreateInstanceForTesting(
363 kTestUrl, {crypto::SignatureVerifier::SignatureAlgorithm::ECDSA_SHA256},
364 "challenge", /*authorization=*/std::nullopt);
365 service().RegisterBoundSession(base::DoNothing(), std::move(fetch_param),
366 IsolationInfo::CreateTransient());
367 }
368
369 const std::string session_id_2("SessionId2");
370 {
371 ScopedTestFetcher scoped_test_fetcher(session_id_2);
372
373 auto fetch_param = RegistrationFetcherParam::CreateInstanceForTesting(
374 kTestUrl, {crypto::SignatureVerifier::SignatureAlgorithm::ECDSA_SHA256},
375 "challenge", /*authorization=*/std::nullopt);
376 service().RegisterBoundSession(base::DoNothing(), std::move(fetch_param),
377 IsolationInfo::CreateTransient());
378 }
379
380 base::test::TestFuture<std::vector<SessionKey>> future;
381 service().GetAllSessionsAsync(
382 future.GetCallback<const std::vector<SessionKey>&>());
383 EXPECT_THAT(future.Take(), UnorderedElementsAre(ExpectId(session_id_1),
384 ExpectId(session_id_2)));
385 }
386
TEST_F(SessionServiceImplTest,DeleteSession)387 TEST_F(SessionServiceImplTest, DeleteSession) {
388 ScopedTestFetcher scoped_test_fetcher(kSessionId);
389
390 auto fetch_param = RegistrationFetcherParam::CreateInstanceForTesting(
391 kTestUrl, {crypto::SignatureVerifier::SignatureAlgorithm::ECDSA_SHA256},
392 "challenge", /*authorization=*/std::nullopt);
393 service().RegisterBoundSession(base::DoNothing(), std::move(fetch_param),
394 IsolationInfo::CreateTransient());
395
396 ASSERT_TRUE(
397 service().GetSessionForTesting(SchemefulSite(kTestUrl), kSessionId));
398
399 service().DeleteSession(SchemefulSite(kTestUrl), Session::Id(kSessionId));
400
401 EXPECT_FALSE(
402 service().GetSessionForTesting(SchemefulSite(kTestUrl), kSessionId));
403 }
404
405 } // namespace
406
407 class SessionServiceImplWithStoreTest : public TestWithTaskEnvironment {
408 public:
SessionServiceImplWithStoreTest()409 SessionServiceImplWithStoreTest()
410 : context_(CreateTestURLRequestContextBuilder()->Build()),
411 store_(std::make_unique<StrictMock<SessionStoreMock>>()),
412 service_(*UnexportableKeyServiceFactory::GetInstance()->GetShared(),
413 context_.get(),
414 store_.get()) {}
415
service()416 SessionServiceImpl& service() { return service_; }
store()417 StrictMock<SessionStoreMock>& store() { return *store_; }
418
OnSessionsLoaded()419 void OnSessionsLoaded() {
420 service().OnLoadSessionsComplete(SessionStore::SessionsMap());
421 }
422
FinishLoadingSessions(SessionStore::SessionsMap loaded_sessions)423 void FinishLoadingSessions(SessionStore::SessionsMap loaded_sessions) {
424 service().OnLoadSessionsComplete(std::move(loaded_sessions));
425 }
426
GetSiteSessionsCount(const SchemefulSite & site)427 size_t GetSiteSessionsCount(const SchemefulSite& site) {
428 auto [begin, end] = service().GetSessionsForSite(site);
429 return std::distance(begin, end);
430 }
431
432 private:
433 crypto::ScopedMockUnexportableKeyProvider scoped_mock_key_provider_;
434 std::unique_ptr<URLRequestContext> context_;
435 std::unique_ptr<StrictMock<SessionStoreMock>> store_;
436 SessionServiceImpl service_;
437 };
438
TEST_F(SessionServiceImplWithStoreTest,UsesSessionStore)439 TEST_F(SessionServiceImplWithStoreTest, UsesSessionStore) {
440 {
441 InSequence seq;
442 EXPECT_CALL(store(), LoadSessions)
443 .Times(1)
444 .WillOnce(
445 Invoke(this, &SessionServiceImplWithStoreTest::OnSessionsLoaded));
446 EXPECT_CALL(store(), SaveSession).Times(1);
447 EXPECT_CALL(store(), DeleteSession).Times(1);
448 }
449
450 // Will invoke the store's load session method.
451 service().LoadSessionsAsync();
452
453 ScopedTestFetcher scoped_test_fetcher(kSessionId);
454 auto fetch_param = RegistrationFetcherParam::CreateInstanceForTesting(
455 kTestUrl, {crypto::SignatureVerifier::SignatureAlgorithm::ECDSA_SHA256},
456 "challenge", /*authorization=*/std::nullopt);
457 // Will invoke the store's save session method.
458 service().RegisterBoundSession(base::DoNothing(), std::move(fetch_param),
459 IsolationInfo::CreateTransient());
460
461 auto site = SchemefulSite(kTestUrl);
462 Session* session = service().GetSessionForTesting(site, kSessionId);
463 ASSERT_TRUE(session);
464 EXPECT_EQ(GetSiteSessionsCount(site), 1u);
465 session->set_expiry_date(base::Time::Now() - base::Days(1));
466 // Will invoke the store's delete session method.
467 EXPECT_EQ(GetSiteSessionsCount(site), 0u);
468 }
469
TEST_F(SessionServiceImplWithStoreTest,GetAllSessionsWaitsForSessionsToLoad)470 TEST_F(SessionServiceImplWithStoreTest, GetAllSessionsWaitsForSessionsToLoad) {
471 // Start loading
472 EXPECT_CALL(store(), LoadSessions).Times(1);
473 service().LoadSessionsAsync();
474
475 // Request sessions, which should wait until we finish loading.
476 base::test::TestFuture<std::vector<SessionKey>> future;
477 service().GetAllSessionsAsync(
478 future.GetCallback<const std::vector<SessionKey>&>());
479
480 std::unique_ptr<Session> session = Session::CreateIfValid(
481 SessionParams("session_id", "https://example.com/refresh", /*scope=*/{},
482 /*creds=*/{}),
483 kTestUrl);
484 ASSERT_TRUE(session);
485
486 // Complete loading. If we did not defer, we'd miss this session.
487 SessionStore::SessionsMap session_map;
488 session_map.insert({SchemefulSite(kTestUrl), std::move(session)});
489 FinishLoadingSessions(std::move(session_map));
490
491 // But we did defer, so we found it.
492 EXPECT_THAT(future.Take(), UnorderedElementsAre(ExpectId("session_id")));
493 }
494
495 } // namespace net::device_bound_sessions
496