blob: bdeaebe9efe079b34a6ed14214a9bb20b2fb4c45 [file] [log] [blame]
// Copyright 2021 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/webid_utils.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "components/url_formatter/elide_url.h"
#include "components/url_formatter/url_formatter.h"
#include "content/browser/renderer_host/frame_tree_node.h"
#include "content/browser/runtime_feature_state/runtime_feature_state_document_data.h"
#include "content/browser/webid/fedcm_metrics.h"
#include "content/browser/webid/flags.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/federated_identity_api_permission_context_delegate.h"
#include "content/public/browser/federated_identity_permission_context_delegate.h"
#include "content/public/common/web_identity.h"
#include "net/base/net_errors.h"
#include "net/base/registry_controlled_domains/registry_controlled_domain.h"
#include "net/base/url_util.h"
#include "third_party/blink/public/mojom/devtools/inspector_issue.mojom.h"
#include "third_party/blink/public/mojom/webid/federated_auth_request.mojom.h"
#include "url/origin.h"
using blink::mojom::FederatedAuthRequestResult;
namespace content::webid {
bool IsSameOriginWithAncestors(const url::Origin& origin,
RenderFrameHost* render_frame_host) {
while (render_frame_host) {
if (!origin.IsSameOriginWith(render_frame_host->GetLastCommittedOrigin())) {
return false;
}
render_frame_host = render_frame_host->GetParent();
}
return true;
}
void SetIdpSigninStatus(content::BrowserContext* context,
int frame_tree_node_id,
const url::Origin& origin,
blink::mojom::IdpSigninStatus status) {
FrameTreeNode* frame_tree_node = nullptr;
// frame_tree_node_id may be invalid if we are loading the first frame
// of the tab.
if (frame_tree_node_id != FrameTreeNode::kFrameTreeNodeInvalidId) {
frame_tree_node = FrameTreeNode::GloballyFindByID(frame_tree_node_id);
// If the id was not kFrameTreeNodeInvalidId, but the lookup failed, we
// ignore the load because we cannot do same-origin checks.
if (!frame_tree_node) {
RecordSetLoginStatusIgnoredReason(
FedCmSetLoginStatusIgnoredReason::kFrameTreeLookupFailed);
return;
}
}
// Make sure we're same-origin with our ancestors.
if (frame_tree_node) {
if (frame_tree_node->IsInFencedFrameTree()) {
RecordSetLoginStatusIgnoredReason(
FedCmSetLoginStatusIgnoredReason::kInFencedFrame);
return;
}
if (!IsSameOriginWithAncestors(origin, frame_tree_node->parent())) {
RecordSetLoginStatusIgnoredReason(
FedCmSetLoginStatusIgnoredReason::kCrossOrigin);
return;
}
}
auto* delegate = context->GetFederatedIdentityPermissionContext();
if (!delegate) {
// The embedder may not have a delegate (e.g. webview)
return;
}
delegate->SetIdpSigninStatus(
origin, status == blink::mojom::IdpSigninStatus::kSignedIn);
}
absl::optional<std::string> ComputeConsoleMessageForHttpResponseCode(
const char* endpoint_name,
int http_response_code) {
// Do not add error message for OK response status.
if (http_response_code >= 200 && http_response_code <= 299)
return absl::nullopt;
if (http_response_code < 0) {
// In this case, the |response_code| represents a NET_ERROR, so we should
// use a helper function to ensure we use a meaningful message.
return base::StringPrintf(
"The fetch of the %s resulted in a network error: %s", endpoint_name,
net::ErrorToShortString(http_response_code).c_str());
}
// In this case, the |response_code| represents an HTTP error code, which is
// standard and hence the number by itself should be understood.
return base::StringPrintf(
"When fetching the %s, a %d HTTP response code was received.",
endpoint_name, http_response_code);
}
bool IsEndpointSameOrigin(const GURL& identity_provider_config_url,
const GURL& endpoint_url) {
return url::Origin::Create(identity_provider_config_url)
.IsSameOriginWith(endpoint_url);
}
bool ShouldFailAccountsEndpointRequestBecauseNotSignedInWithIdp(
RenderFrameHost& host,
const GURL& identity_provider_config_url,
FederatedIdentityPermissionContextDelegate* permission_delegate) {
const url::Origin idp_origin =
url::Origin::Create(identity_provider_config_url);
if (webid::GetIdpSigninStatusMode(host, idp_origin) ==
FedCmIdpSigninStatusMode::DISABLED) {
return false;
}
const absl::optional<bool> idp_signin_status =
permission_delegate->GetIdpSigninStatus(idp_origin);
return !idp_signin_status.value_or(true);
}
void UpdateIdpSigninStatusForAccountsEndpointResponse(
RenderFrameHost& host,
const GURL& identity_provider_config_url,
IdpNetworkRequestManager::FetchStatus fetch_status,
bool does_idp_have_failing_signin_status,
FederatedIdentityPermissionContextDelegate* permission_delegate,
FedCmMetrics* metrics) {
url::Origin idp_origin = url::Origin::Create(identity_provider_config_url);
if (webid::GetIdpSigninStatusMode(host, idp_origin) ==
FedCmIdpSigninStatusMode::DISABLED) {
return;
}
// Record metrics on effect of IDP sign-in status API.
const absl::optional<bool> idp_signin_status =
permission_delegate->GetIdpSigninStatus(idp_origin);
metrics->RecordIdpSigninMatchStatus(idp_signin_status,
fetch_status.parse_status);
if (fetch_status.parse_status ==
IdpNetworkRequestManager::ParseStatus::kSuccess) {
// `does_idp_have_failing_signin_status` fails the request prior to fetching
// the accounts endpoint for FedCmIdpSigninStatusMode::ENABLED mode but not
// FedCmIdpSigninStatusMode::METRICS_ONLY mode. Do not set the IdP sign-in
// status here if `does_idp_have_failing_signin_status` in
// FedCmIdpSigninStatusMode::METRICS_ONLY mode in order to better emulate
// FedCmIdpSigninStatusMode::ENABLED behavior.
if (!does_idp_have_failing_signin_status) {
permission_delegate->SetIdpSigninStatus(idp_origin, true);
}
} else {
RecordIdpSignOutNetError(fetch_status.response_code);
// Ensures that we only fetch accounts unconditionally once.
permission_delegate->SetIdpSigninStatus(idp_origin, false);
}
}
std::string GetConsoleErrorMessageFromResult(
FederatedAuthRequestResult status) {
switch (status) {
case FederatedAuthRequestResult::kShouldEmbargo: {
return "User declined or dismissed prompt. API exponential cool down "
"triggered.";
}
case FederatedAuthRequestResult::kErrorDisabledInSettings: {
return "Third-party sign in was disabled in browser Site Settings.";
}
case FederatedAuthRequestResult::kErrorTooManyRequests: {
return "Only one navigator.credentials.get request may be outstanding at "
"one time.";
}
case FederatedAuthRequestResult::kErrorFetchingWellKnownHttpNotFound: {
return "The provider's FedCM well-known file cannot be found.";
}
case FederatedAuthRequestResult::kErrorFetchingWellKnownNoResponse: {
return "The provider's FedCM well-known file fetch resulted in an "
"error response code.";
}
case FederatedAuthRequestResult::kErrorFetchingWellKnownInvalidResponse: {
return "Provider's FedCM well-known file is invalid.";
}
case FederatedAuthRequestResult::kErrorFetchingWellKnownListEmpty: {
return "Provider's FedCM well-known file has no config URLs.";
}
case FederatedAuthRequestResult::
kErrorFetchingWellKnownInvalidContentType: {
return "Provider's FedCM well-known content type must be a JSON content "
"type.";
}
case FederatedAuthRequestResult::kErrorConfigNotInWellKnown: {
return "Provider's FedCM config file not listed in its well-known file.";
}
case FederatedAuthRequestResult::kErrorWellKnownTooBig: {
return "Provider's FedCM well-known file contains too many config URLs.";
}
case FederatedAuthRequestResult::kErrorFetchingConfigHttpNotFound: {
return "The provider's FedCM config file cannot be found.";
}
case FederatedAuthRequestResult::kErrorFetchingConfigNoResponse: {
return "The provider's FedCM config file fetch resulted in an "
"error response code.";
}
case FederatedAuthRequestResult::kErrorFetchingConfigInvalidResponse: {
return "Provider's FedCM config file is invalid.";
}
case FederatedAuthRequestResult::kErrorFetchingConfigInvalidContentType: {
return "Provider's FedCM config file content type must be a JSON content "
"type.";
}
case FederatedAuthRequestResult::kErrorFetchingClientMetadataHttpNotFound: {
return "The provider's client metadata endpoint cannot be found.";
}
case FederatedAuthRequestResult::kErrorFetchingClientMetadataNoResponse: {
return "The provider's client metadata fetch resulted in an error "
"response code.";
}
case FederatedAuthRequestResult::
kErrorFetchingClientMetadataInvalidResponse: {
return "Provider's client metadata is invalid.";
}
case FederatedAuthRequestResult::
kErrorFetchingClientMetadataInvalidContentType: {
return "Provider's client metadata content type must be a JSON content "
"type.";
}
case FederatedAuthRequestResult::kErrorFetchingAccountsHttpNotFound: {
return "The provider's accounts list endpoint cannot be found.";
}
case FederatedAuthRequestResult::kErrorFetchingAccountsNoResponse: {
return "The provider's accounts list fetch resulted in an error response "
"code.";
}
case FederatedAuthRequestResult::kErrorFetchingAccountsInvalidResponse: {
return "Provider's accounts list is invalid. Should have received an "
"\"accounts\" list, where each account must have at least \"id\", "
"\"name\", and \"email\".";
}
case FederatedAuthRequestResult::kErrorFetchingAccountsListEmpty: {
return "Provider's accounts list is empty.";
}
case FederatedAuthRequestResult::kErrorFetchingAccountsInvalidContentType: {
return "Provider's accounts list endpoint content type must be a JSON "
"content type.";
}
case FederatedAuthRequestResult::kErrorFetchingIdTokenHttpNotFound: {
return "The provider's id token endpoint cannot be found.";
}
case FederatedAuthRequestResult::kErrorFetchingIdTokenNoResponse: {
return "The provider's token fetch resulted in an error response "
"code.";
}
case FederatedAuthRequestResult::kErrorFetchingIdTokenInvalidResponse: {
return "Provider's token is invalid.";
}
case FederatedAuthRequestResult::kErrorFetchingIdTokenIdpErrorResponse: {
return "Provider is unable to issue a token, but provided details on the "
"error that occurred.";
}
case FederatedAuthRequestResult::
kErrorFetchingIdTokenCrossSiteIdpErrorResponse: {
return "Provider is unable to issue a token, but provided details on the "
"error that occurred. The error URL must be same-site with the "
"config URL.";
}
case FederatedAuthRequestResult::kErrorFetchingIdTokenInvalidContentType: {
return "Provider's token endpoint content type must be a JSON content "
"type.";
}
case FederatedAuthRequestResult::kErrorCanceled: {
return "The request has been aborted.";
}
case FederatedAuthRequestResult::kErrorRpPageNotVisible: {
return "RP page is not visible.";
}
case FederatedAuthRequestResult::kErrorSilentMediationFailure: {
return "Silent mediation was requested, but the conditions to achieve it "
"were not met.";
}
case FederatedAuthRequestResult::kErrorThirdPartyCookiesBlocked: {
return "Third party cookies are blocked. Right now the Chromium "
"implementation of FedCM API requires third party cookies and "
"this restriction will be removed soon. In the interim, to test "
"FedCM without third-party cookies, enable the "
"#fedcm-without-third-party-cookies flag.";
}
case FederatedAuthRequestResult::kErrorNotSignedInWithIdp: {
return "Not signed in with the identity provider.";
}
case FederatedAuthRequestResult::kError: {
return "Error retrieving a token.";
}
case FederatedAuthRequestResult::kSuccess: {
// Should not be called with success, as we should not add a console
// message for success.
DCHECK(false);
return "";
}
}
}
FedCmIdpSigninStatusMode GetIdpSigninStatusMode(RenderFrameHost& host,
const url::Origin& idp_origin) {
// TODO(crbug.com/1487668): Remove this function in favor of
// GetFedCmIdpSigninStatusFlag.
return GetFedCmIdpSigninStatusFlag();
}
std::string FormatUrlWithDomain(const GURL& url, bool for_display) {
// We do not use url_formatter::FormatUrlForSecurityDisplay() directly because
// our UI intentionally shows only the eTLD+1, as it makes for a shorter text
// that is also clearer to users. The identity provider's well-known file is
// in the root of the eTLD+1, and sign-in status within identity provider and
// relying party can be domain-wide because it relies on cookies.
std::string formatted_url_str =
net::IsLocalhost(url)
? url.host()
: net::registry_controlled_domains::GetDomainAndRegistry(
url,
net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES);
if (for_display) {
return base::UTF16ToUTF8(url_formatter::FormatUrlForSecurityDisplay(
GURL(url.scheme() + "://" + formatted_url_str),
url_formatter::SchemeDisplay::OMIT_HTTP_AND_HTTPS));
}
// We want defaults but we need to keep the scheme.
url_formatter::FormatUrlTypes types =
url_formatter::kFormatUrlOmitDefaults &
~(url_formatter::kFormatUrlOmitHTTP | url_formatter::kFormatUrlOmitHTTPS |
url_formatter::kFormatUrlOmitFileScheme);
return base::UTF16ToUTF8(url_formatter::FormatUrl(
GURL(url.scheme() + "://" + formatted_url_str), types,
base::UnescapeRule::SPACES, nullptr, nullptr, nullptr));
}
bool HasSharingPermissionOrIdpHasThirdPartyCookiesAccess(
RenderFrameHost& host,
const GURL& provider_url,
const url::Origin& embedder_origin,
const url::Origin& requester_origin,
const absl::optional<std::string>& account_id,
FederatedIdentityPermissionContextDelegate* sharing_permission_delegate,
FederatedIdentityApiPermissionContextDelegate* api_permission_delegate) {
bool has_access = IsFedCmExemptIdpWithThirdPartyCookiesEnabled() &&
api_permission_delegate->HasThirdPartyCookiesAccess(
host, provider_url, embedder_origin);
return sharing_permission_delegate->HasSharingPermission(
requester_origin, embedder_origin,
url::Origin::Create(provider_url), account_id) ||
has_access;
}
} // namespace content::webid