blob: 2a8ae842466b47b480619a06b9ea47f528168da5 [file] [log] [blame]
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/browser/webid/federated_auth_disconnect_request.h"
#include "base/notreached.h"
#include "base/trace_event/trace_event.h"
#include "content/browser/webid/flags.h"
#include "content/browser/webid/webid_utils.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/webid/federated_identity_api_permission_context_delegate.h"
#include "content/public/browser/webid/federated_identity_permission_context_delegate.h"
#include "services/network/public/cpp/is_potentially_trustworthy.h"
#include "third_party/blink/public/mojom/devtools/console_message.mojom.h"
#include "third_party/blink/public/mojom/webid/federated_auth_request.mojom.h"
namespace content {
using FederatedApiPermissionStatus =
FederatedIdentityApiPermissionContextDelegate::PermissionStatus;
using LoginState = IdentityRequestAccount::LoginState;
using DisconnectStatusForMetrics = webid::DisconnectStatus;
using blink::mojom::DisconnectStatus;
using blink::mojom::FederatedAuthRequestResult;
// static
std::unique_ptr<FederatedAuthDisconnectRequest>
FederatedAuthDisconnectRequest::Create(
std::unique_ptr<IdpNetworkRequestManager> network_manager,
FederatedIdentityPermissionContextDelegate* permission_delegate,
RenderFrameHost* render_frame_host,
std::unique_ptr<webid::Metrics> fedcm_metrics,
blink::mojom::IdentityCredentialDisconnectOptionsPtr options) {
std::unique_ptr<FederatedAuthDisconnectRequest> request =
base::WrapUnique<FederatedAuthDisconnectRequest>(
new FederatedAuthDisconnectRequest(
std::move(network_manager), permission_delegate,
render_frame_host, std::move(fedcm_metrics), std::move(options)));
return request;
}
FederatedAuthDisconnectRequest::~FederatedAuthDisconnectRequest() {
Complete(DisconnectStatus::kError,
DisconnectStatusForMetrics::kUnhandledRequest);
}
FederatedAuthDisconnectRequest::FederatedAuthDisconnectRequest(
std::unique_ptr<IdpNetworkRequestManager> network_manager,
FederatedIdentityPermissionContextDelegate* permission_delegate,
RenderFrameHost* render_frame_host,
std::unique_ptr<webid::Metrics> fedcm_metrics,
blink::mojom::IdentityCredentialDisconnectOptionsPtr options)
: network_manager_(std::move(network_manager)),
permission_delegate_(permission_delegate),
render_frame_host_(render_frame_host),
fedcm_metrics_(std::move(fedcm_metrics)),
options_(std::move(options)),
origin_(render_frame_host->GetLastCommittedOrigin()),
start_time_(base::TimeTicks::Now()),
perfetto_track_(webid::CreatePerfettoTrackForFedCM(this)) {
RenderFrameHost* main_frame = render_frame_host->GetMainFrame();
DCHECK(main_frame->IsInPrimaryMainFrame());
embedding_origin_ = main_frame->GetLastCommittedOrigin();
}
void FederatedAuthDisconnectRequest::SetCallbackAndStart(
blink::mojom::FederatedAuthRequest::DisconnectCallback callback,
FederatedIdentityApiPermissionContextDelegate* api_permission_delegate) {
TRACE_EVENT_BEGIN("content.fedcm", "FedCM disconnect", perfetto_track_);
callback_ = std::move(callback);
url::Origin config_origin = url::Origin::Create(options_->config->config_url);
if (!network::IsOriginPotentiallyTrustworthy(config_origin)) {
Complete(DisconnectStatus::kError,
DisconnectStatusForMetrics::kIdpNotPotentiallyTrustworthy);
return;
}
FederatedApiPermissionStatus permission_status =
api_permission_delegate->GetApiPermissionStatus(embedding_origin_);
std::optional<DisconnectStatusForMetrics> error_disconnect_status;
switch (permission_status) {
case FederatedApiPermissionStatus::BLOCKED_VARIATIONS:
error_disconnect_status = DisconnectStatusForMetrics::kDisabledInFlags;
break;
case FederatedApiPermissionStatus::BLOCKED_SETTINGS:
error_disconnect_status = DisconnectStatusForMetrics::kDisabledInSettings;
break;
// We do not block disconnect on FedCM cooldown.
case FederatedApiPermissionStatus::BLOCKED_EMBARGO:
case FederatedApiPermissionStatus::GRANTED:
// Intentional fall-through.
break;
default:
NOTREACHED();
}
if (error_disconnect_status) {
Complete(DisconnectStatus::kError, *error_disconnect_status);
return;
}
// Reject if we know that there are no sharing permissions with the given IdP
// and the IdP doesn't have third party cookies access on the RP site.
if (!webid::HasSharingPermissionOrIdpHasThirdPartyCookiesAccess(
*render_frame_host_, options_->config->config_url, embedding_origin_,
origin_, /*account_id=*/std::nullopt, permission_delegate_,
api_permission_delegate)) {
Complete(DisconnectStatus::kError,
DisconnectStatusForMetrics::kNoAccountToDisconnect);
return;
}
config_fetcher_ = std::make_unique<webid::ConfigFetcher>(
*render_frame_host_, network_manager_.get());
GURL config_url = options_->config->config_url;
// TODO(crbug.com/390626180): It seems ok to ignore the well-known checks in
// all cases here. However, keeping this unchanged for now when the IDP
// registration API is not enabled since we only really need this for that
// case.
config_fetcher_->Start(
{{config_url, webid::IsIdPRegistrationEnabled()}},
blink::mojom::RpMode::kPassive, /*icon_ideal_size=*/0,
/*icon_minimum_size=*/0,
base::BindOnce(
&FederatedAuthDisconnectRequest::OnAllConfigAndWellKnownFetched,
weak_ptr_factory_.GetWeakPtr()));
}
void FederatedAuthDisconnectRequest::OnAllConfigAndWellKnownFetched(
std::vector<webid::ConfigFetcher::FetchResult> fetch_results) {
config_fetcher_.reset();
DCHECK_EQ(fetch_results.size(), 1u);
const webid::ConfigFetcher::FetchResult& fetch_result = fetch_results[0];
if (fetch_result.error) {
const webid::ConfigFetcher::FetchError& fetch_error = *fetch_result.error;
if (fetch_error.additional_console_error_message) {
render_frame_host_->AddMessageToConsole(
blink::mojom::ConsoleMessageLevel::kError,
*fetch_error.additional_console_error_message);
}
webid::DisconnectStatus status;
switch (fetch_error.result) {
case FederatedAuthRequestResult::kWellKnownHttpNotFound: {
status = webid::DisconnectStatus::kWellKnownHttpNotFound;
break;
}
case FederatedAuthRequestResult::kWellKnownNoResponse: {
status = webid::DisconnectStatus::kWellKnownNoResponse;
break;
}
case FederatedAuthRequestResult::kWellKnownInvalidResponse: {
status = webid::DisconnectStatus::kWellKnownInvalidResponse;
break;
}
case FederatedAuthRequestResult::kWellKnownListEmpty: {
status = webid::DisconnectStatus::kWellKnownListEmpty;
break;
}
case FederatedAuthRequestResult::kWellKnownInvalidContentType: {
status = webid::DisconnectStatus::kWellKnownInvalidContentType;
break;
}
case FederatedAuthRequestResult::kConfigHttpNotFound: {
status = webid::DisconnectStatus::kConfigHttpNotFound;
break;
}
case FederatedAuthRequestResult::kConfigNoResponse: {
status = webid::DisconnectStatus::kConfigNoResponse;
break;
}
case FederatedAuthRequestResult::kConfigInvalidResponse: {
status = webid::DisconnectStatus::kConfigInvalidResponse;
break;
}
case FederatedAuthRequestResult::kConfigInvalidContentType: {
status = webid::DisconnectStatus::kConfigInvalidContentType;
break;
}
case FederatedAuthRequestResult::kWellKnownTooBig: {
status = webid::DisconnectStatus::kWellKnownTooBig;
break;
}
case FederatedAuthRequestResult::kConfigNotInWellKnown: {
status = webid::DisconnectStatus::kConfigNotInWellKnown;
break;
}
default: {
// The ConfigFetcher does not return any other type of
// result.
NOTREACHED();
}
}
Complete(DisconnectStatus::kError, status);
return;
}
if (!webid::IsEndpointSameOrigin(options_->config->config_url,
fetch_result.endpoints.disconnect)) {
render_frame_host_->AddMessageToConsole(
blink::mojom::ConsoleMessageLevel::kError,
"Config is missing or has an invalid or cross-origin "
"\"disconnect_endpoint\" URL.");
Complete(DisconnectStatus::kError,
DisconnectStatusForMetrics::kDisconnectUrlIsCrossOrigin);
return;
}
disconnect_request_sent_ = true;
network_manager_->SendDisconnectRequest(
fetch_result.endpoints.disconnect, options_->account_hint,
options_->config->client_id,
base::BindOnce(&FederatedAuthDisconnectRequest::OnDisconnectResponse,
weak_ptr_factory_.GetWeakPtr()));
}
void FederatedAuthDisconnectRequest::OnDisconnectResponse(
IdpNetworkRequestManager::FetchStatus fetch_status,
const std::string& account_id) {
CHECK(callback_);
// Matches the GrantSharingPermission() call in
// RequestService::CompleteTokenRequest(). Note that the IDP origin
// cannot be an arbitrary origin, but rather needs to be a potentially
// trustworthy one.
url::Origin idp_origin = url::Origin::Create(options_->config->config_url);
if (fetch_status.parse_status !=
IdpNetworkRequestManager::ParseStatus::kSuccess) {
// Even though the response was unsuccessful, the credentialed fetch was
// sent to the IDP, so disconnect all permissions associated with the triple
// (`origin_`, `embedding_origin`, `idp_origin`).
permission_delegate_->RevokeSharingPermission(
origin_, embedding_origin_, idp_origin, /*account_id=*/"");
Complete(DisconnectStatus::kError,
DisconnectStatusForMetrics::kDisconnectFailedOnServer);
return;
}
permission_delegate_->RevokeSharingPermission(origin_, embedding_origin_,
idp_origin, account_id);
Complete(DisconnectStatus::kSuccess, DisconnectStatusForMetrics::kSuccess);
}
void FederatedAuthDisconnectRequest::Complete(
blink::mojom::DisconnectStatus status,
webid::DisconnectStatus disconnect_status_for_metrics) {
if (!callback_) {
return;
}
TRACE_EVENT_END("content.fedcm", perfetto_track_);
if (disconnect_status_for_metrics != webid::DisconnectStatus::kSuccess) {
AddConsoleErrorMessage(disconnect_status_for_metrics);
}
std::optional<base::TimeDelta> duration =
disconnect_request_sent_
? std::optional<base::TimeDelta>{base::TimeTicks::Now() - start_time_}
: std::nullopt;
fedcm_metrics_->RecordDisconnectMetrics(
disconnect_status_for_metrics, duration,
webid::ComputeRequesterFrameType(*render_frame_host_, origin_,
embedding_origin_),
options_->config->config_url);
fedcm_metrics_.reset();
std::move(callback_).Run(status);
}
void FederatedAuthDisconnectRequest::AddConsoleErrorMessage(
webid::DisconnectStatus disconnect_status_for_metrics) {
render_frame_host_->AddMessageToConsole(
blink::mojom::ConsoleMessageLevel::kError,
webid::GetDisconnectConsoleErrorMessage(disconnect_status_for_metrics));
}
} // namespace content