blob: 06f3f4d055c9d9fd068bcbb1912d385f0cc91255 [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/containers/contains.h"
#include "base/containers/to_vector.h"
#include "base/functional/bind.h"
#include "base/metrics/histogram_functions.h"
#include "base/task/sequenced_task_runner.h"
#include "components/unexportable_keys/unexportable_key_service.h"
#include "net/base/features.h"
#include "net/base/schemeful_site.h"
#include "net/device_bound_sessions/jwk_utils.h"
#include "net/device_bound_sessions/registration_request_param.h"
#include "net/device_bound_sessions/session_store.h"
#include "net/url_request/url_request.h"
#include "net/url_request/url_request_context.h"
namespace net::device_bound_sessions {
namespace {
// Parameters for the refresh quota. We currently allow 2 refreshes in 3
// minutes. This allows sites to refresh every 5 minutes, accounting for
// proactive refreshes 2 minutes before expiry, and with some error tolerance
// (e.g. a failed refresh or user cookie clearing).
constexpr size_t kRefreshQuota = 2;
constexpr base::TimeDelta kRefreshQuotaInterval = base::Minutes(3);
bool SessionMatchesFilter(
const SchemefulSite& site,
const Session& session,
std::optional<base::Time> created_after_time,
std::optional<base::Time> created_before_time,
base::RepeatingCallback<bool(const url::Origin&, const net::SchemefulSite&)>
origin_and_site_matcher) {
if (created_before_time && *created_before_time < session.creation_date()) {
return false;
}
if (created_after_time && *created_after_time > session.creation_date()) {
return false;
}
if (!origin_and_site_matcher.is_null() &&
!origin_and_site_matcher.Run(session.origin(), site)) {
return false;
}
return true;
}
class DebugHeaderBuilder {
public:
void AddSkippedSession(SessionKey key, SessionService::RefreshResult result) {
structured_headers::Item item;
switch (result) {
case SessionService::RefreshResult::kRefreshed:
case SessionService::RefreshResult::kFatalError:
return;
case SessionService::RefreshResult::kInitializedService:
NOTREACHED();
case SessionService::RefreshResult::kUnreachable:
item = structured_headers::Item("unreachable",
structured_headers::Item::kTokenType);
break;
case SessionService::RefreshResult::kServerError:
item = structured_headers::Item("server_error",
structured_headers::Item::kTokenType);
break;
case SessionService::RefreshResult::kQuotaExceeded:
item = structured_headers::Item("quota_exceeded",
structured_headers::Item::kTokenType);
break;
}
structured_headers::Parameters params = {
{"session_identifier", structured_headers::Item(key.id.value())}};
skipped_sessions_.emplace_back(std::move(item), std::move(params));
}
std::optional<std::string> Build() {
if (skipped_sessions_.empty()) {
return std::nullopt;
}
return structured_headers::SerializeList(std::move(skipped_sessions_));
}
private:
structured_headers::List skipped_sessions_;
};
bool IsProactiveRefreshCandidate(
Session& existing_session,
const Session& new_session,
const CookieAndLineAccessResultList& maybe_stored_cookies) {
// Get the shortest lifetime of a bound cookie set by the current
// refresh request. This assumes:
// 1. The current refresh sets all bound cookies
// 2. The proactive refresh would have set the same lifetimes
// These assumptions are good enough for histogram logging, but likely
// not true for all sites.
base::Time current_time = base::Time::Now();
base::TimeDelta minimum_lifetime = base::TimeDelta::Max();
for (const CookieCraving& cookie_craving : new_session.cookies()) {
for (const CookieAndLineWithAccessResult& cookie_and_line :
maybe_stored_cookies) {
if (cookie_and_line.cookie.has_value() &&
cookie_craving.IsSatisfiedBy(cookie_and_line.cookie.value())) {
minimum_lifetime =
std::min(minimum_lifetime,
cookie_and_line.cookie->ExpiryDate() - current_time);
}
}
}
base::UmaHistogramLongTimes100(
"Net.DeviceBoundSessions.MinimumBoundCookieLifetime", minimum_lifetime);
std::optional<base::Time> last_proactive_refresh_opportunity =
existing_session.TakeLastProactiveRefreshOpportunity();
if (!last_proactive_refresh_opportunity.has_value()) {
return false;
}
return minimum_lifetime >= current_time - *last_proactive_refresh_opportunity;
}
void LogProactiveRefreshAttempt(
SessionServiceImpl::ProactiveRefreshAttempt attempt) {
base::UmaHistogramEnumeration(
"Net.DeviceBoundSessions.ProactiveRefreshAttempt", attempt);
}
} // namespace
DeferredURLRequest::DeferredURLRequest(
SessionService::RefreshCompleteCallback callback)
: callback(std::move(callback)) {}
DeferredURLRequest::DeferredURLRequest(DeferredURLRequest&& other) noexcept =
default;
DeferredURLRequest& DeferredURLRequest::operator=(
DeferredURLRequest&& other) noexcept = default;
DeferredURLRequest::~DeferredURLRequest() = default;
SessionServiceImpl::SessionServiceImpl(
unexportable_keys::UnexportableKeyService& key_service,
const URLRequestContext* request_context,
SessionStore* store)
: pending_initialization_(!!store),
key_service_(key_service),
context_(request_context),
session_store_(store) {
ignore_refresh_quota_ = !features::kDeviceBoundSessionsRefreshQuota.Get();
CHECK(context_);
}
SessionServiceImpl::~SessionServiceImpl() = default;
void SessionServiceImpl::LoadSessionsAsync() {
if (!session_store_) {
return;
}
session_store_->LoadSessions(base::BindOnce(
&SessionServiceImpl::OnLoadSessionsComplete, weak_factory_.GetWeakPtr()));
}
void SessionServiceImpl::RegisterBoundSession(
OnAccessCallback on_access_callback,
RegistrationFetcherParam registration_params,
const IsolationInfo& isolation_info,
const NetLogWithSource& net_log,
const std::optional<url::Origin>& original_request_initiator) {
Session* federated_provider_session = nullptr;
bool is_google_subdomain_for_histograms = IsSubdomainOf(
registration_params.registration_endpoint().host(), "google.com");
if (registration_params.provider_session_id().has_value()) {
if (!base::FeatureList::IsEnabled(
features::kDeviceBoundSessionsFederatedRegistration)) {
// Simply ignore headers with a provider_session_id if the flag
// isn't enabled.
return;
}
base::expected<Session*, SessionError> provider_session_or_error =
GetFederatedProviderSessionIfValid(registration_params);
if (!provider_session_or_error.has_value()) {
OnRegistrationComplete(
std::move(on_access_callback), is_google_subdomain_for_histograms,
/*fetcher=*/nullptr,
RegistrationResult(std::move(provider_session_or_error.error())));
return;
}
federated_provider_session = provider_session_or_error.value();
}
net::NetLogSource net_log_source_for_registration = net::NetLogSource(
net::NetLogSourceType::URL_REQUEST, net::NetLog::Get()->NextID());
net_log.AddEventReferencingSource(
net::NetLogEventType::DBSC_REGISTRATION_REQUEST,
net_log_source_for_registration);
const auto supported_algos = registration_params.supported_algos();
std::optional<GURL> provider_url = registration_params.provider_url();
RegistrationRequestParam request_params =
RegistrationRequestParam::CreateForRegistration(
std::move(registration_params));
std::unique_ptr<RegistrationFetcher> fetcher =
RegistrationFetcher::CreateFetcher(
request_params, *this, key_service_.get(), context_.get(),
isolation_info, net_log_source_for_registration,
original_request_initiator);
RegistrationFetcher* fetcher_raw = fetcher.get();
registration_fetchers_.insert(std::move(fetcher));
auto callback = base::BindOnce(
&SessionServiceImpl::OnRegistrationComplete, weak_factory_.GetWeakPtr(),
std::move(on_access_callback), is_google_subdomain_for_histograms);
if (federated_provider_session) {
fetcher_raw->StartFetchWithFederatedKey(
request_params, *federated_provider_session->unexportable_key_id(),
*provider_url, std::move(callback));
// `fetcher_raw` may be deleted.
} else {
fetcher_raw->StartCreateTokenAndFetch(request_params, supported_algos,
std::move(callback));
// `fetcher_raw` may be deleted.
}
}
base::expected<Session*, SessionError>
SessionServiceImpl::GetFederatedProviderSessionIfValid(
const RegistrationFetcherParam& registration_params) {
// This is a federated session registration.
GURL provider_url = *registration_params.provider_url();
if (!provider_url.is_valid() || url::Origin::Create(provider_url).opaque()) {
return base::unexpected(
SessionError(SessionError::kInvalidFederatedSessionUrl));
}
SessionKey provider_key{SchemefulSite(provider_url),
*registration_params.provider_session_id()};
Session* provider_session = GetSession(provider_key);
if (!provider_session) {
// Provider session not found, fail the registration.
return base::unexpected(SessionError(
SessionError::kInvalidFederatedSessionProviderSessionMissing));
}
if (url::Origin::Create(provider_url) != provider_session->origin()) {
return base::unexpected(SessionError(
SessionError::kInvalidFederatedSessionWrongProviderOrigin));
}
unexportable_keys::ServiceErrorOr<
crypto::SignatureVerifier::SignatureAlgorithm>
algorithm =
key_service_->GetAlgorithm(*provider_session->unexportable_key_id());
if (!algorithm.has_value()) {
return base::unexpected(SessionError(SessionError::kInvalidFederatedKey));
}
unexportable_keys::ServiceErrorOr<std::vector<uint8_t>> pub_key =
key_service_->GetSubjectPublicKeyInfo(
*provider_session->unexportable_key_id());
if (!pub_key.has_value()) {
return base::unexpected(SessionError(SessionError::kInvalidFederatedKey));
}
std::string thumbprint = CreateJwkThumbprint(*algorithm, *pub_key);
if (thumbprint != *registration_params.provider_key()) {
return base::unexpected(
SessionError(SessionError::kFederatedKeyThumbprintMismatch));
}
return provider_session;
}
SessionServiceImpl::Observer::Observer(
const GURL& url,
base::RepeatingCallback<void(const SessionAccess&)> callback)
: url(url), callback(callback) {}
SessionServiceImpl::Observer::~Observer() = default;
void SessionServiceImpl::OnLoadSessionsComplete(
SessionStore::SessionsMap sessions) {
unpartitioned_sessions_.merge(sessions);
pending_initialization_ = false;
std::vector<base::OnceClosure> queued_operations =
std::move(queued_operations_);
for (base::OnceClosure& closure : queued_operations) {
std::move(closure).Run();
}
base::UmaHistogramCounts1000(
"Net.DeviceBoundSessions.RequestsDeferredForInitialization",
requests_before_initialization_);
}
void SessionServiceImpl::OnRegistrationComplete(
OnAccessCallback on_access_callback,
bool is_google_subdomain_for_histograms,
RegistrationFetcher* fetcher,
RegistrationResult registration_result) {
if (is_google_subdomain_for_histograms) {
base::UmaHistogramBoolean(
"Net.DeviceBoundSessions.GoogleRegistrationIsFromStandard", true);
}
SessionError::ErrorType result = OnRegistrationCompleteInternal(
std::move(on_access_callback), fetcher, std::move(registration_result));
base::UmaHistogramEnumeration("Net.DeviceBoundSessions.RegistrationResult",
result);
}
std::ranges::subrange<SessionServiceImpl::SessionsMap::iterator>
SessionServiceImpl::GetSessionsForSite(const SchemefulSite& site) {
const auto now = base::Time::Now();
// Session keys are sorted by site, then identifier. So the first
// element not less than (`site`, "") is the first session for this
// site.
auto it =
unpartitioned_sessions_.lower_bound(SessionKey{site, Session::Id("")});
while (it != unpartitioned_sessions_.end() && it->first.site == site) {
auto curit = it;
++it;
if (now >= curit->second->expiry_date()) {
// Since this deletion is not due to a request, we do not need to
// provide a per-request callback here.
DeleteSessionAndNotifyInternal(DeletionReason::kExpired, curit,
base::NullCallback());
} else {
curit->second->RecordAccess();
}
}
return std::ranges::subrange<SessionsMap::iterator>(
unpartitioned_sessions_.lower_bound(SessionKey{site, Session::Id("")}),
it);
}
std::optional<SessionService::DeferralParams> SessionServiceImpl::ShouldDefer(
URLRequest* request,
HttpRequestHeaders* extra_headers,
const FirstPartySetMetadata& first_party_set_metadata) {
if (pending_initialization_) {
return DeferralParams();
}
if (request->device_bound_session_usage() < SessionUsage::kNoUsage) {
request->set_device_bound_session_usage(SessionUsage::kNoUsage);
}
SchemefulSite site(request->url());
DebugHeaderBuilder debug_header_builder;
const base::flat_map<SessionKey, RefreshResult>& previous_deferrals =
request->device_bound_session_deferrals();
for (const auto& [_, session] : GetSessionsForSite(site)) {
if (!session->IsInScope(request)) {
continue;
}
SessionKey session_key{site, session->id()};
base::TimeDelta minimum_lifetime =
session->MinimumBoundCookieLifetime(request, first_party_set_metadata);
if (minimum_lifetime.is_zero()) {
auto previous_deferrals_it = previous_deferrals.find(session_key);
if (previous_deferrals_it != previous_deferrals.end()) {
debug_header_builder.AddSkippedSession(previous_deferrals_it->first,
previous_deferrals_it->second);
continue;
}
NotifySessionAccess(request->device_bound_session_access_callback(),
SessionAccess::AccessType::kUpdate, session_key,
*session);
return DeferralParams(session->id());
}
MaybeStartProactiveRefresh(request->device_bound_session_access_callback(),
request, session_key, minimum_lifetime);
}
std::optional<std::string> debug_header = debug_header_builder.Build();
if (debug_header.has_value()) {
extra_headers->SetHeader("Secure-Session-Skipped", *debug_header);
}
return std::nullopt;
}
void SessionServiceImpl::DeferRequestForRefresh(
URLRequest* request,
DeferralParams deferral,
RefreshCompleteCallback callback) {
CHECK(callback);
CHECK(request);
if (deferral.is_pending_initialization) {
CHECK(pending_initialization_);
requests_before_initialization_++;
// Due to the need to recompute `first_party_set_metadata`, we always
// restart the request after initialization completes.
queued_operations_.push_back(base::BindOnce(
std::move(callback), RefreshResult::kInitializedService));
return;
}
SessionKey session_key{SchemefulSite(request->url()), *deferral.session_id};
// For the first deferring request, create a new vector and add the request.
auto [it, inserted] = deferred_requests_.try_emplace(session_key);
// Add the request callback to the deferred list.
it->second.emplace_back(std::move(callback));
auto* session = GetSession(session_key);
CHECK(session, base::NotFatalUntil::M147);
// TODO(crbug.com/417770933): Remove this block.
if (!session) {
// If we can't find the session, clear the `session_key` in the map
// and continue all related requests. We can call this a fatal error
// because the session has already been deleted.
UnblockDeferredRequests(session_key, RefreshResult::kFatalError);
return;
}
// Notify the request that it has been deferred for refreshed cookies.
NotifySessionAccess(request->device_bound_session_access_callback(),
SessionAccess::AccessType::kUpdate, session_key,
*session);
if (!inserted) {
return;
}
if (proactive_requests_.find(session_key) != proactive_requests_.end()) {
return;
}
if (RefreshQuotaExceeded(session_key.site)) {
UnblockDeferredRequests(session_key, RefreshResult::kQuotaExceeded);
return;
}
if (session->ShouldBackoff()) {
UnblockDeferredRequests(session_key, RefreshResult::kUnreachable);
return;
}
const Session::KeyIdOrError& key_id = session->unexportable_key_id();
if (!key_id.has_value()) {
if (key_id.error() == unexportable_keys::ServiceError::kKeyNotReady) {
// Unwrap key and then try to refresh
session_store_->RestoreSessionBindingKey(
session_key,
base::BindOnce(&SessionServiceImpl::OnSessionKeyRestored,
weak_factory_.GetWeakPtr(), request->GetWeakPtr(),
session_key,
request->device_bound_session_access_callback()));
} else {
UnblockDeferredRequests(session_key, RefreshResult::kFatalError);
DeleteSessionAndNotify(DeletionReason::kFailedToRestoreKey, session_key,
request->device_bound_session_access_callback());
}
return;
}
RefreshSessionInternal(RefreshTrigger::kMissingCookie, request, session_key,
session, *key_id);
}
void SessionServiceImpl::OnRefreshRequestCompletion(
RefreshTrigger trigger,
OnAccessCallback on_access_callback,
SessionKey session_key,
RegistrationFetcher* fetcher,
RegistrationResult registration_result) {
SessionError::ErrorType result = OnRefreshRequestCompletionInternal(
std::move(on_access_callback), session_key, fetcher,
std::move(registration_result));
Session* session = GetSession(session_key);
if (session) {
session->InformOfRefreshResult(
/*was_proactive=*/trigger == RefreshTrigger::kProactive, result);
}
std::string histogram_base = "Net.DeviceBoundSessions.RefreshResult";
std::string suffix;
switch (trigger) {
case RefreshTrigger::kProactive:
suffix = ".Proactive";
break;
case RefreshTrigger::kMissingCookie:
suffix = ".MissingCookie";
break;
}
base::UmaHistogramEnumeration(histogram_base, result);
base::UmaHistogramEnumeration(histogram_base + suffix, result);
}
// Continue or restart all deferred requests for the session and remove the
// session key in the map.
void SessionServiceImpl::UnblockDeferredRequests(
const SessionKey& session_key,
RefreshResult result,
std::optional<bool> is_proactive_refresh_candidate,
std::optional<base::TimeDelta> minimum_proactive_refresh_threshold) {
if (auto it = proactive_requests_.find(session_key);
it != proactive_requests_.end()) {
base::UmaHistogramTimes("Net.DeviceBoundSessions.ProactiveRefreshDuration",
it->second.Elapsed());
proactive_requests_.erase(it);
}
auto it = deferred_requests_.find(session_key);
if (it == deferred_requests_.end()) {
return;
}
auto requests = std::move(it->second);
deferred_requests_.erase(it);
base::UmaHistogramCounts100("Net.DeviceBoundSessions.RequestDeferredCount",
requests.size());
if (is_proactive_refresh_candidate.has_value() &&
minimum_proactive_refresh_threshold.has_value()) {
base::UmaHistogramLongTimes100(
"Net.DeviceBoundSessions.MinimumProactiveRefreshThreshold",
*minimum_proactive_refresh_threshold);
if (*is_proactive_refresh_candidate) {
base::UmaHistogramLongTimes100(
"Net.DeviceBoundSessions.MinimumProactiveRefreshThreshold.Success",
*minimum_proactive_refresh_threshold);
} else {
base::UmaHistogramLongTimes100(
"Net.DeviceBoundSessions.MinimumProactiveRefreshThreshold.Failure",
*minimum_proactive_refresh_threshold);
}
if (*is_proactive_refresh_candidate) {
if (*minimum_proactive_refresh_threshold <= base::Seconds(30)) {
base::UmaHistogramCounts100(
"Net.DeviceBoundSessions.ProactiveRefreshCandidateDeferredCount."
"ThirtySeconds",
requests.size());
for (auto& request : requests) {
base::UmaHistogramTimes(
"Net.DeviceBoundSessions."
"ProactiveRefreshCandidateRequestDeferredDuration.ThirtySeconds",
request.timer.Elapsed());
}
}
if (*minimum_proactive_refresh_threshold <= base::Minutes(1)) {
base::UmaHistogramCounts100(
"Net.DeviceBoundSessions.ProactiveRefreshCandidateDeferredCount."
"OneMinute",
requests.size());
for (auto& request : requests) {
base::UmaHistogramTimes(
"Net.DeviceBoundSessions."
"ProactiveRefreshCandidateRequestDeferredDuration.OneMinute",
request.timer.Elapsed());
}
}
if (*minimum_proactive_refresh_threshold <= base::Minutes(2)) {
base::UmaHistogramCounts100(
"Net.DeviceBoundSessions.ProactiveRefreshCandidateDeferredCount."
"TwoMinutes",
requests.size());
for (auto& request : requests) {
base::UmaHistogramTimes(
"Net.DeviceBoundSessions."
"ProactiveRefreshCandidateRequestDeferredDuration.TwoMinutes",
request.timer.Elapsed());
}
}
}
}
for (auto& request : requests) {
base::UmaHistogramTimes("Net.DeviceBoundSessions.RequestDeferredDuration",
request.timer.Elapsed());
base::UmaHistogramEnumeration("Net.DeviceBoundSessions.DeferralResult",
result);
if (request.timer.Elapsed() <= base::Milliseconds(1)) {
base::UmaHistogramEnumeration(
"Net.DeviceBoundSessions.DeferralResult.Instant", result);
} else {
base::UmaHistogramEnumeration(
"Net.DeviceBoundSessions.DeferralResult.Slow", result);
}
std::move(request.callback).Run(result);
}
}
void SessionServiceImpl::SetChallengeForBoundSession(
OnAccessCallback on_access_callback,
const URLRequest& request,
const FirstPartySetMetadata& first_party_set_metadata,
const SessionChallengeParam& param) {
if (!param.session_id()) {
return;
}
SessionKey session_key{SchemefulSite(request.url()),
Session::Id(*param.session_id())};
Session* session = GetSession(session_key);
if (!session) {
return;
}
if (features::kDeviceBoundSessionsOriginTrialFeedback.Get() &&
!session->CanSetBoundCookie(request, first_party_set_metadata)) {
return;
}
NotifySessionAccess(on_access_callback, SessionAccess::AccessType::kUpdate,
session_key, *session);
session->set_cached_challenge(param.challenge());
}
void SessionServiceImpl::GetAllSessionsAsync(
base::OnceCallback<void(const std::vector<SessionKey>&)> callback) {
if (pending_initialization_) {
queued_operations_.push_back(base::BindOnce(
&SessionServiceImpl::GetAllSessionsAsync,
// `base::Unretained` is safe because the callback is stored in
// `queued_operations_`, which is owned by `this`.
base::Unretained(this), std::move(callback)));
} else {
std::vector<SessionKey> sessions = base::ToVector(
unpartitioned_sessions_, [](const auto& pair) { return pair.first; });
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, base::BindOnce(std::move(callback), std::move(sessions)));
}
}
void SessionServiceImpl::DeleteSessionAndNotify(
DeletionReason reason,
const SessionKey& session_key,
SessionService::OnAccessCallback per_request_callback) {
auto it = unpartitioned_sessions_.find(session_key);
if (it == unpartitioned_sessions_.end()) {
return;
}
DeleteSessionAndNotifyInternal(reason, it, per_request_callback);
}
const Session* SessionServiceImpl::GetSession(
const SessionKey& session_key) const {
auto it = unpartitioned_sessions_.find(session_key);
if (it != unpartitioned_sessions_.end()) {
return it->second.get();
}
return nullptr;
}
Session* SessionServiceImpl::GetSession(const SessionKey& session_key) {
return const_cast<Session*>(std::as_const(*this).GetSession(session_key));
}
void SessionServiceImpl::AddSession(const SchemefulSite& site,
SessionParams params,
base::span<const uint8_t> wrapped_key,
base::OnceCallback<void(bool)> callback) {
key_service_->FromWrappedSigningKeySlowlyAsync(
wrapped_key, unexportable_keys::BackgroundTaskPriority::kBestEffort,
base::BindOnce(&SessionServiceImpl::OnAddSessionKeyRestored,
weak_factory_.GetWeakPtr(), site, std::move(params),
std::move(callback)));
}
void SessionServiceImpl::OnAddSessionKeyRestored(
const SchemefulSite& site,
SessionParams params,
base::OnceCallback<void(bool)> callback,
unexportable_keys::ServiceErrorOr<unexportable_keys::UnexportableKeyId>
key_or_error) {
if (!key_or_error.has_value()) {
std::move(callback).Run(false);
return;
}
params.key_id = *key_or_error;
base::expected<std::unique_ptr<net::device_bound_sessions::Session>,
net::device_bound_sessions::SessionError>
session_or_error =
net::device_bound_sessions::Session::CreateIfValid(params);
if (!session_or_error.has_value()) {
std::move(callback).Run(false);
return;
}
AddSession(site, std::move(session_or_error.value()));
std::move(callback).Run(true);
}
void SessionServiceImpl::AddSession(const SchemefulSite& site,
std::unique_ptr<Session> session) {
if (session_store_) {
session_store_->SaveSession(site, *session);
}
unpartitioned_sessions_.emplace(SessionKey{site, session->id()},
std::move(session));
}
void SessionServiceImpl::DeleteAllSessions(
DeletionReason reason,
std::optional<base::Time> created_after_time,
std::optional<base::Time> created_before_time,
base::RepeatingCallback<bool(const url::Origin&, const net::SchemefulSite&)>
origin_and_site_matcher,
base::OnceClosure completion_callback) {
for (auto it = unpartitioned_sessions_.begin();
it != unpartitioned_sessions_.end();) {
auto curit = it;
++it;
if (SessionMatchesFilter(curit->first.site, *curit->second,
created_after_time, created_before_time,
origin_and_site_matcher)) {
DeleteSessionAndNotifyInternal(reason, curit, base::NullCallback());
}
}
std::move(completion_callback).Run();
}
base::ScopedClosureRunner SessionServiceImpl::AddObserver(
const GURL& url,
base::RepeatingCallback<void(const SessionAccess&)> callback) {
auto observer = std::make_unique<Observer>(url, callback);
base::ScopedClosureRunner subscription(base::BindOnce(
&SessionServiceImpl::RemoveObserver, weak_factory_.GetWeakPtr(),
net::SchemefulSite(url), observer.get()));
observers_by_site_[net::SchemefulSite(url)].insert(std::move(observer));
return subscription;
}
void SessionServiceImpl::DeleteSessionAndNotifyInternal(
DeletionReason reason,
SessionServiceImpl::SessionsMap::iterator it,
SessionService::OnAccessCallback per_request_callback) {
base::UmaHistogramEnumeration("Net.DeviceBoundSessions.DeletionReason",
reason);
if (session_store_) {
session_store_->DeleteSession(it->first);
}
NotifySessionAccess(per_request_callback,
SessionAccess::AccessType::kTermination, it->first,
*it->second);
unpartitioned_sessions_.erase(it);
}
void SessionServiceImpl::NotifySessionAccess(
SessionService::OnAccessCallback per_request_callback,
SessionAccess::AccessType access_type,
const SessionKey& session_key,
const Session& session) {
SessionAccess access{access_type, session_key};
if (access_type == SessionAccess::AccessType::kTermination) {
access.cookies.reserve(session.cookies().size());
for (const CookieCraving& cookie : session.cookies()) {
access.cookies.push_back(cookie.Name());
}
}
if (per_request_callback) {
per_request_callback.Run(access);
}
auto observers_it = observers_by_site_.find(session_key.site);
if (observers_it == observers_by_site_.end()) {
return;
}
for (const auto& observer : observers_it->second) {
if (session.IncludesUrl(observer->url)) {
observer->callback.Run(access);
}
}
}
void SessionServiceImpl::RemoveObserver(net::SchemefulSite site,
Observer* observer) {
auto observers_it = observers_by_site_.find(site);
if (observers_it == observers_by_site_.end()) {
return;
}
ObserverSet& observers = observers_it->second;
auto it = observers.find(observer);
if (it == observers.end()) {
return;
}
observers.erase(it);
if (observers.empty()) {
observers_by_site_.erase(observers_it);
}
}
SessionError::ErrorType SessionServiceImpl::OnRegistrationCompleteInternal(
OnAccessCallback on_access_callback,
RegistrationFetcher* fetcher,
RegistrationResult registration_result) {
RemoveFetcher(fetcher);
if (registration_result.is_error()) {
// We failed to create a new session, so there's nothing to clean
// up.
return registration_result.error().type;
} else if (registration_result.is_no_session_config_change()) {
// No config changes is not allowed at registration.
return SessionError::kInvalidConfigJson;
}
std::unique_ptr<Session> session = registration_result.TakeSession();
CHECK(session);
const SchemefulSite site(session->origin());
NotifySessionAccess(on_access_callback, SessionAccess::AccessType::kCreation,
SessionKey{site, session->id()}, *session);
AddSession(site, std::move(session));
return SessionError::kSuccess;
}
SessionError::ErrorType SessionServiceImpl::OnRefreshRequestCompletionInternal(
OnAccessCallback on_access_callback,
const SessionKey& session_key,
RegistrationFetcher* fetcher,
RegistrationResult registration_result) {
RemoveFetcher(fetcher);
// If refresh succeeded:
// 1. update the session by adding a new session, replacing the old one
// 2. restart the deferred requests.
if (registration_result.is_session()) {
std::unique_ptr<Session> new_session = registration_result.TakeSession();
CHECK(new_session);
CHECK_EQ(new_session->id(), session_key.id);
Session* existing_session = GetSession(session_key);
CHECK(existing_session);
bool is_proactive_refresh_candidate =
IsProactiveRefreshCandidate(*existing_session, *new_session,
registration_result.maybe_stored_cookies());
SchemefulSite new_site(new_session->origin());
AddSession(new_site, std::move(new_session));
// The session has been refreshed, restart the request.
UnblockDeferredRequests(
session_key, RefreshResult::kRefreshed, is_proactive_refresh_candidate,
existing_session
->TakeLastProactiveRefreshOpportunityMinimumCookieLifetime());
} else if (registration_result.is_no_session_config_change()) {
Session* existing_session = GetSession(session_key);
CHECK(existing_session);
bool is_proactive_refresh_candidate =
IsProactiveRefreshCandidate(*existing_session, *existing_session,
registration_result.maybe_stored_cookies());
UnblockDeferredRequests(
session_key, RefreshResult::kRefreshed, is_proactive_refresh_candidate,
existing_session
->TakeLastProactiveRefreshOpportunityMinimumCookieLifetime());
} else if (std::optional<DeletionReason> deletion_reason =
registration_result.error().GetDeletionReason();
deletion_reason.has_value()) {
DeleteSessionAndNotify(*deletion_reason, session_key, on_access_callback);
UnblockDeferredRequests(session_key, RefreshResult::kFatalError);
} else {
// Transient error, unblock the request without cookies.
UnblockDeferredRequests(session_key,
registration_result.error().IsServerError()
? RefreshResult::kServerError
: RefreshResult::kUnreachable);
}
return registration_result.is_error() ? registration_result.error().type
: SessionError::kSuccess;
}
void SessionServiceImpl::OnSessionKeyRestored(
base::WeakPtr<URLRequest> request,
const SessionKey& session_key,
OnAccessCallback on_access_callback,
Session::KeyIdOrError key_id_or_error) {
if (!request) {
return;
}
if (!key_id_or_error.has_value()) {
UnblockDeferredRequests(session_key, RefreshResult::kFatalError);
DeleteSessionAndNotify(DeletionReason::kFailedToUnwrapKey, session_key,
on_access_callback);
return;
}
auto* session = GetSession(session_key);
if (!session) {
UnblockDeferredRequests(session_key, RefreshResult::kFatalError);
return;
}
session->set_unexportable_key_id(key_id_or_error);
RefreshSessionInternal(RefreshTrigger::kMissingCookie, request.get(),
session_key, session, *key_id_or_error);
}
void SessionServiceImpl::RefreshSessionInternal(
RefreshTrigger trigger,
URLRequest* request,
const SessionKey& session_key,
Session* session,
unexportable_keys::UnexportableKeyId key_id) {
net::NetLogSource net_log_source_for_refresh = net::NetLogSource(
net::NetLogSourceType::URL_REQUEST, net::NetLog::Get()->NextID());
request->net_log().AddEventReferencingSource(
net::NetLogEventType::DBSC_REFRESH_REQUEST, net_log_source_for_refresh);
refresh_times_[session_key.site].push_back(base::TimeTicks::Now());
auto registration_param =
RegistrationRequestParam::CreateForRefresh(*session);
auto callback = base::BindOnce(
&SessionServiceImpl::OnRefreshRequestCompletion,
weak_factory_.GetWeakPtr(), trigger,
request->device_bound_session_access_callback(), session_key);
std::unique_ptr<RegistrationFetcher> fetcher =
RegistrationFetcher::CreateFetcher(
registration_param, *this, key_service_.get(), context_.get(),
request->isolation_info(), net_log_source_for_refresh,
request->initiator());
RegistrationFetcher* fetcher_raw = fetcher.get();
registration_fetchers_.insert(std::move(fetcher));
fetcher_raw->StartFetchWithExistingKey(registration_param, key_id,
std::move(callback));
// `fetcher_raw` may be deleted.
}
bool SessionServiceImpl::RefreshQuotaExceeded(const SchemefulSite& site) {
if (ignore_refresh_quota_) {
return false;
}
auto it = refresh_times_.find(site);
if (it == refresh_times_.end()) {
return false;
}
std::erase_if(it->second, [](base::TimeTicks time) {
return base::TimeTicks::Now() - time >= kRefreshQuotaInterval;
});
size_t refresh_count = it->second.size();
if (refresh_count == 0) {
refresh_times_.erase(it);
}
return refresh_count >= kRefreshQuota;
}
void SessionServiceImpl::RemoveFetcher(RegistrationFetcher* fetcher) {
if (!fetcher) {
return;
}
auto it = registration_fetchers_.find(fetcher);
if (it == registration_fetchers_.end()) {
return;
}
registration_fetchers_.erase(it);
}
void SessionServiceImpl::MaybeStartProactiveRefresh(
SessionService::OnAccessCallback per_request_callback,
URLRequest* request,
const SessionKey& session_key,
base::TimeDelta minimum_cookie_lifetime) {
if (!base::FeatureList::IsEnabled(
features::kDeviceBoundSessionProactiveRefresh)) {
return;
}
if (minimum_cookie_lifetime >
features::kDeviceBoundSessionProactiveRefreshThreshold.Get()) {
return;
}
if (deferred_requests_.find(session_key) != deferred_requests_.end()) {
// It's not a proactive refresh if we're in the middle of a regular refresh.
LogProactiveRefreshAttempt(
ProactiveRefreshAttempt::kExistingDeferringRefresh);
return;
}
auto* session = GetSession(session_key);
CHECK(session);
if (RefreshQuotaExceeded(session_key.site)) {
LogProactiveRefreshAttempt(ProactiveRefreshAttempt::kRefreshQuota);
return;
}
if (session->ShouldBackoff()) {
LogProactiveRefreshAttempt(ProactiveRefreshAttempt::kBackoff);
return;
}
if (session->attempted_proactive_refresh_since_last_success()) {
// We only do one proactive refresh attempt before a deferral. If we
// did not do this, every refresh due to missing cookies would be
// skipped due to the refresh quota. Instead, we allow the refresh
// due to missing cookies, which will communicate its reason for
// failure in the Secure-Session-Skipped header.
LogProactiveRefreshAttempt(
ProactiveRefreshAttempt::kPreviousFailedProactiveRefresh);
return;
}
if (!session->unexportable_key_id().has_value()) {
// TODO(crbug.com/358137054): If we're otherwise ready for a proactive
// refresh, we could start restoring the key. This is lower priority
// than regular proactive refresh, since some amount of startup
// latency is unavoidable with DBSC.
LogProactiveRefreshAttempt(ProactiveRefreshAttempt::kMissingKey);
return;
}
auto [_, inserted] = proactive_requests_.try_emplace(session_key);
if (!inserted) {
// Do not proactively refresh if we've already started one proactive
// refresh.
LogProactiveRefreshAttempt(
ProactiveRefreshAttempt::kExistingProactiveRefresh);
return;
}
NotifySessionAccess(per_request_callback, SessionAccess::AccessType::kUpdate,
session_key, *session);
LogProactiveRefreshAttempt(ProactiveRefreshAttempt::kAttempted);
RefreshSessionInternal(RefreshTrigger::kProactive, request, session_key,
session, *session->unexportable_key_id());
}
} // namespace net::device_bound_sessions