| // 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 "base/trace_event/trace_event.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/webid/fedcm_metrics.h" |
| #include "content/browser/webid/flags.h" |
| #include "content/browser/webid/request_page_data.h" |
| #include "content/public/browser/browser_context.h" |
| #include "content/public/browser/runtime_feature_state/runtime_feature_state_document_data.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 "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/schemeful_site.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; |
| using content::FedCmDisconnectStatus; |
| |
| namespace content::webid { |
| |
| namespace { |
| constexpr net::registry_controlled_domains::PrivateRegistryFilter |
| kDefaultPrivateRegistryFilter = |
| net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES; |
| } // namespace |
| |
| bool IsSameSiteWithAncestors(const url::Origin& origin, |
| RenderFrameHost* render_frame_host) { |
| while (render_frame_host) { |
| if (!net::SchemefulSite::IsSameSite( |
| origin, render_frame_host->GetLastCommittedOrigin())) { |
| return false; |
| } |
| render_frame_host = render_frame_host->GetParent(); |
| } |
| return true; |
| } |
| |
| void SetIdpSigninStatus(content::BrowserContext* context, |
| FrameTreeNodeId 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) { |
| frame_tree_node = FrameTreeNode::GloballyFindByID(frame_tree_node_id); |
| // If the id was valid, 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 (!IsSameSiteWithAncestors(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, std::nullopt); |
| } |
| |
| std::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 std::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); |
| const std::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) { |
| url::Origin idp_origin = url::Origin::Create(identity_provider_config_url); |
| |
| // Record metrics on effect of IDP sign-in status API. |
| const std::optional<bool> idp_signin_status = |
| permission_delegate->GetIdpSigninStatus(idp_origin); |
| FedCmMetrics::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, std::nullopt); |
| } |
| } else { |
| RecordIdpSignOutNetError(fetch_status.response_code); |
| // Ensures that we only fetch accounts unconditionally once. |
| permission_delegate->SetIdpSigninStatus(idp_origin, false, std::nullopt); |
| } |
| } |
| |
| std::string GetConsoleErrorMessageFromResult( |
| FederatedAuthRequestResult status) { |
| switch (status) { |
| case FederatedAuthRequestResult::kShouldEmbargo: { |
| return "User declined or dismissed prompt. API exponential cool down " |
| "triggered."; |
| } |
| case FederatedAuthRequestResult::kIdpNotPotentiallyTrustworthy: { |
| return "The IdP is not potentially trustworthy (are you using HTTP?)"; |
| } |
| case FederatedAuthRequestResult::kDisabledInSettings: { |
| return "FedCM was disabled either temporarily based on previous user " |
| "action or permanently via site settings. Try manage third-party " |
| "sign-in via the icon to the left of the URL bar or via site " |
| "settings."; |
| } |
| case FederatedAuthRequestResult::kDisabledInFlags: { |
| return "FedCM was disabled in flags."; |
| } |
| case FederatedAuthRequestResult::kTooManyRequests: { |
| return "Only one navigator.credentials.get request may be outstanding at " |
| "one time."; |
| } |
| case FederatedAuthRequestResult::kWellKnownHttpNotFound: { |
| return "The provider's FedCM well-known file cannot be found."; |
| } |
| case FederatedAuthRequestResult::kWellKnownNoResponse: { |
| return "The provider's FedCM well-known file fetch resulted in an " |
| "error response code."; |
| } |
| case FederatedAuthRequestResult::kWellKnownInvalidResponse: { |
| return "Provider's FedCM well-known file is invalid."; |
| } |
| case FederatedAuthRequestResult::kWellKnownListEmpty: { |
| return "Provider's FedCM well-known file has no config URLs."; |
| } |
| case FederatedAuthRequestResult::kWellKnownInvalidContentType: { |
| return "Provider's FedCM well-known content type must be a JSON content " |
| "type."; |
| } |
| case FederatedAuthRequestResult::kConfigNotInWellKnown: { |
| return "Provider's FedCM config file not listed in its well-known file."; |
| } |
| case FederatedAuthRequestResult::kWellKnownTooBig: { |
| return "Provider's FedCM well-known file contains too many config URLs."; |
| } |
| case FederatedAuthRequestResult::kConfigHttpNotFound: { |
| return "The provider's FedCM config file cannot be found."; |
| } |
| case FederatedAuthRequestResult::kConfigNoResponse: { |
| return "The provider's FedCM config file fetch resulted in an " |
| "error response code."; |
| } |
| case FederatedAuthRequestResult::kConfigInvalidResponse: { |
| return "Provider's FedCM config file is invalid."; |
| } |
| case FederatedAuthRequestResult::kConfigInvalidContentType: { |
| return "Provider's FedCM config file content type must be a JSON content " |
| "type."; |
| } |
| case FederatedAuthRequestResult::kClientMetadataHttpNotFound: { |
| return "The provider's client metadata endpoint cannot be found."; |
| } |
| case FederatedAuthRequestResult::kClientMetadataNoResponse: { |
| return "The provider's client metadata fetch resulted in an error " |
| "response code."; |
| } |
| case FederatedAuthRequestResult::kClientMetadataInvalidResponse: { |
| return "Provider's client metadata is invalid."; |
| } |
| case FederatedAuthRequestResult::kClientMetadataInvalidContentType: { |
| return "Provider's client metadata content type must be a JSON content " |
| "type."; |
| } |
| case FederatedAuthRequestResult::kAccountsHttpNotFound: { |
| return "The provider's accounts list endpoint cannot be found."; |
| } |
| case FederatedAuthRequestResult::kAccountsNoResponse: { |
| return "The provider's accounts list fetch resulted in an error response " |
| "code."; |
| } |
| case FederatedAuthRequestResult::kAccountsInvalidResponse: { |
| 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::kAccountsListEmpty: { |
| return "Provider's accounts list is empty."; |
| } |
| case FederatedAuthRequestResult::kAccountsInvalidContentType: { |
| return "Provider's accounts list endpoint content type must be a JSON " |
| "content type."; |
| } |
| case FederatedAuthRequestResult::kIdTokenHttpNotFound: { |
| return "The provider's id token endpoint cannot be found."; |
| } |
| case FederatedAuthRequestResult::kIdTokenNoResponse: { |
| return "The provider's token fetch resulted in an error response " |
| "code."; |
| } |
| case FederatedAuthRequestResult::kIdTokenInvalidResponse: { |
| return "Provider's token is invalid."; |
| } |
| case FederatedAuthRequestResult::kIdTokenIdpErrorResponse: { |
| return "Provider is unable to issue a token, but provided details on the " |
| "error that occurred."; |
| } |
| case FederatedAuthRequestResult::kIdTokenCrossSiteIdpErrorResponse: { |
| 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::kIdTokenInvalidContentType: { |
| return "Provider's token endpoint content type must be a JSON content " |
| "type."; |
| } |
| case FederatedAuthRequestResult::kCanceled: { |
| return "The request has been aborted."; |
| } |
| case FederatedAuthRequestResult::kRpPageNotVisible: { |
| return "RP page is not visible."; |
| } |
| case FederatedAuthRequestResult::kSilentMediationFailure: { |
| return "Silent mediation was requested, but the conditions to achieve it " |
| "were not met."; |
| } |
| case FederatedAuthRequestResult::kThirdPartyCookiesBlocked: { |
| 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::kMissingTransientUserActivation: { |
| return "FedCM active mode requires transient user activation."; |
| } |
| case FederatedAuthRequestResult::kReplacedByActiveMode: { |
| return "The request is replaced by a new one with active mode."; |
| } |
| case FederatedAuthRequestResult::kNotSignedInWithIdp: { |
| return "Not signed in with the identity provider."; |
| } |
| case FederatedAuthRequestResult::kInvalidFieldsSpecified: { |
| return "Invalid 'fields' were specified in the FedCM call."; |
| } |
| case FederatedAuthRequestResult::kRelyingPartyOriginIsOpaque: { |
| return "FedCM is not supported on an opaque origin."; |
| } |
| case FederatedAuthRequestResult::kTypeNotMatching: { |
| return "The requested IdP type did not match the registered IdP."; |
| } |
| case FederatedAuthRequestResult::kUiDismissedNoEmbargo: { |
| return "Prompt dismissed. API exponential cool down not " |
| "triggered."; |
| } |
| case FederatedAuthRequestResult::kError: { |
| return "Error retrieving a token."; |
| } |
| case FederatedAuthRequestResult::kCorsError: { |
| return "Server did not send the correct CORS headers."; |
| } |
| case FederatedAuthRequestResult::kSuppressedBySegmentationPlatform: { |
| return "Dialog is suppressed because historical data shows that the user " |
| "is not interested in FedCM on this RP. For testing purposes, " |
| "disable the #fedcm-segmentation-platform flag."; |
| } |
| case FederatedAuthRequestResult::kSuccess: { |
| // Should not be called with success, as we should not add a console |
| // message for success. |
| NOTREACHED(); |
| } |
| } |
| } |
| |
| std::string GetDisconnectConsoleErrorMessage( |
| FedCmDisconnectStatus disconnect_status_for_metrics) { |
| switch (disconnect_status_for_metrics) { |
| case FedCmDisconnectStatus::kSuccess: { |
| NOTREACHED(); |
| } |
| case FedCmDisconnectStatus::kTooManyRequests: { |
| return "There is a pending disconnect() call."; |
| } |
| case FedCmDisconnectStatus::kUnhandledRequest: { |
| return "The disconnect request did not finish by the time the page was " |
| "closed."; |
| } |
| case FedCmDisconnectStatus::kNoAccountToDisconnect: { |
| return "There is no account to disconnect."; |
| } |
| case FedCmDisconnectStatus::kDisconnectUrlIsCrossOrigin: { |
| return "The disconnect URL is cross origin"; |
| } |
| case FedCmDisconnectStatus::kDisconnectFailedOnServer: { |
| return "The disconnect request failed on the server"; |
| } |
| case FedCmDisconnectStatus::kConfigHttpNotFound: { |
| return "The config file cannot be found."; |
| } |
| case FedCmDisconnectStatus::kConfigNoResponse: { |
| return "The config file returned an error response code."; |
| } |
| case FedCmDisconnectStatus::kConfigInvalidResponse: { |
| return "The config file returned some invalid response."; |
| } |
| case FedCmDisconnectStatus::kDisabledInSettings: { |
| return "FedCM is disabled by user settings."; |
| } |
| case FedCmDisconnectStatus::kDisabledInFlags: { |
| return "The disconnect API is disabled by a flag."; |
| } |
| case FedCmDisconnectStatus::kWellKnownHttpNotFound: { |
| return "The well known file cannot be found."; |
| } |
| case FedCmDisconnectStatus::kWellKnownNoResponse: { |
| return "The well-known file returned an error response code."; |
| } |
| case FedCmDisconnectStatus::kWellKnownInvalidResponse: { |
| return "The well-known filed returned some invalid response."; |
| } |
| case FedCmDisconnectStatus::kWellKnownListEmpty: { |
| return "The well-known file returned an empty list."; |
| } |
| case FedCmDisconnectStatus::kConfigNotInWellKnown: { |
| return "The config file is not in the well-known file."; |
| } |
| case FedCmDisconnectStatus::kWellKnownTooBig: { |
| return "Provider's FedCM well-known file contains too many config URLs."; |
| } |
| case FedCmDisconnectStatus::kWellKnownInvalidContentType: { |
| return "Provider's well-known content type must be a JSON content type."; |
| } |
| case FedCmDisconnectStatus::kConfigInvalidContentType: { |
| return "Provider's FedCM config file content type must be a JSON content " |
| "type."; |
| } |
| case FedCmDisconnectStatus::kIdpNotPotentiallyTrustworthy: { |
| return "The provider's config file URL is not potentially trustworthy."; |
| } |
| } |
| } |
| |
| std::string FormatUrlForDisplay(const GURL& url) { |
| // 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, kDefaultPrivateRegistryFilter); |
| return base::UTF16ToUTF8(url_formatter::FormatUrlForSecurityDisplay( |
| GURL(url.scheme() + "://" + formatted_url_str), |
| url_formatter::SchemeDisplay::OMIT_HTTP_AND_HTTPS)); |
| } |
| |
| bool HasSharingPermissionOrIdpHasThirdPartyCookiesAccess( |
| RenderFrameHost& host, |
| const GURL& provider_url, |
| const url::Origin& embedder_origin, |
| const url::Origin& requester_origin, |
| const std::optional<std::string>& account_id, |
| FederatedIdentityPermissionContextDelegate* sharing_permission_delegate, |
| FederatedIdentityApiPermissionContextDelegate* api_permission_delegate) { |
| if (api_permission_delegate->HasThirdPartyCookiesAccess(host, provider_url, |
| embedder_origin)) { |
| return true; |
| } |
| if (account_id) { |
| return sharing_permission_delegate |
| ->GetLastUsedTimestamp(requester_origin, embedder_origin, |
| url::Origin::Create(provider_url), *account_id) |
| .has_value(); |
| } |
| return sharing_permission_delegate->HasSharingPermission( |
| requester_origin, embedder_origin, url::Origin::Create(provider_url)); |
| } |
| |
| RequestPageData* GetPageData(Page& page) { |
| return RequestPageData::GetOrCreateForPage(page); |
| } |
| |
| FedCmRequesterFrameType ComputeRequesterFrameType(const RenderFrameHost& rfh, |
| const url::Origin& requester, |
| const url::Origin& embedder) { |
| // Since FedCM methods are not supported in FencedFrames, we can know whether |
| // this is a main frame by calling GetParent(). |
| if (!rfh.GetParent()) { |
| return FedCmRequesterFrameType::kMainFrame; |
| } |
| return net::SchemefulSite::IsSameSite(requester, embedder) |
| ? FedCmRequesterFrameType::kSameSiteIframe |
| : FedCmRequesterFrameType::kCrossSiteIframe; |
| } |
| |
| void MaybeAddResponseCodeToConsole(RenderFrameHost& render_frame_host, |
| const char* fetch_description, |
| int response_code) { |
| std::optional<std::string> console_message = |
| webid::ComputeConsoleMessageForHttpResponseCode(fetch_description, |
| response_code); |
| if (console_message) { |
| render_frame_host.AddMessageToConsole( |
| blink::mojom::ConsoleMessageLevel::kError, *console_message); |
| } |
| } |
| |
| perfetto::NamedTrack CreatePerfettoTrackForFedCM(void* class_pointer) { |
| return perfetto::NamedTrack::ThreadScoped( |
| "FedCM", reinterpret_cast<uintptr_t>(class_pointer)); |
| } |
| |
| } // namespace content::webid |