| // Copyright 2020 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_request_impl.h" |
| |
| #include <random> |
| #include <vector> |
| |
| #include "base/command_line.h" |
| #include "base/containers/contains.h" |
| #include "base/functional/callback.h" |
| #include "base/json/json_writer.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/rand_util.h" |
| #include "base/ranges/algorithm.h" |
| #include "base/strings/escape.h" |
| #include "base/task/sequenced_task_runner.h" |
| #include "base/time/time.h" |
| #include "base/values.h" |
| #include "content/browser/bad_message.h" |
| #include "content/browser/devtools/devtools_instrumentation.h" |
| #include "content/browser/renderer_host/render_frame_host_impl.h" |
| #include "content/browser/web_contents/web_contents_impl.h" |
| #include "content/browser/webid/fake_identity_request_dialog_controller.h" |
| #include "content/browser/webid/federated_auth_disconnect_request.h" |
| #include "content/browser/webid/federated_auth_request_page_data.h" |
| #include "content/browser/webid/federated_auth_user_info_request.h" |
| #include "content/browser/webid/flags.h" |
| #include "content/browser/webid/identity_registry.h" |
| #include "content/browser/webid/webid_utils.h" |
| #include "content/public/browser/browser_context.h" |
| #include "content/public/browser/content_browser_client.h" |
| #include "content/public/browser/federated_identity_api_permission_context_delegate.h" |
| #include "content/public/browser/federated_identity_auto_reauthn_permission_context_delegate.h" |
| #include "content/public/browser/federated_identity_permission_context_delegate.h" |
| #include "content/public/browser/render_frame_host.h" |
| #include "content/public/browser/web_contents.h" |
| #include "content/public/common/content_client.h" |
| #include "content/public/common/content_switches.h" |
| #include "content/public/common/page_visibility_state.h" |
| #include "services/network/public/cpp/is_potentially_trustworthy.h" |
| #include "third_party/blink/public/mojom/devtools/inspector_issue.mojom.h" |
| #include "third_party/blink/public/mojom/webid/federated_auth_request.mojom.h" |
| |
| using base::Value; |
| using blink::mojom::DisconnectStatus; |
| using blink::mojom::FederatedAuthRequestResult; |
| using blink::mojom::IdentityProviderConfig; |
| using blink::mojom::IdentityProviderConfigPtr; |
| using blink::mojom::IdentityProviderGetParametersPtr; |
| using blink::mojom::IdentityProviderRequestOptions; |
| using blink::mojom::IdentityProviderRequestOptionsPtr; |
| using blink::mojom::RequestTokenStatus; |
| using blink::mojom::RequestUserInfoStatus; |
| using FederatedApiPermissionStatus = |
| content::FederatedIdentityApiPermissionContextDelegate::PermissionStatus; |
| using DisconnectStatusForMetrics = content::FedCmDisconnectStatus; |
| using TokenStatus = content::FedCmRequestIdTokenStatus; |
| using SignInStateMatchStatus = content::FedCmSignInStateMatchStatus; |
| using TokenResponseType = |
| content::IdpNetworkRequestManager::FedCmTokenResponseType; |
| using ErrorDialogType = content::IdpNetworkRequestManager::FedCmErrorDialogType; |
| using ErrorUrlType = content::IdpNetworkRequestManager::FedCmErrorUrlType; |
| using LoginState = content::IdentityRequestAccount::LoginState; |
| using SignInMode = content::IdentityRequestAccount::SignInMode; |
| using ErrorDialogResult = content::FedCmErrorDialogResult; |
| using CompleteRequestWithErrorCallback = |
| base::OnceCallback<void(blink::mojom::FederatedAuthRequestResult, |
| std::optional<content::FedCmRequestIdTokenStatus>, |
| std::optional<TokenError> token_error, |
| bool)>; |
| |
| namespace content { |
| |
| namespace { |
| static constexpr base::TimeDelta kTokenRequestDelay = base::Seconds(3); |
| static constexpr base::TimeDelta kMaxRejectionTime = base::Seconds(60); |
| |
| // Users spend less time on Android to dismiss the UI. Given the difference, we |
| // use two set of values. The values are calculated based on UMA data to follow |
| // lognormal distribution. |
| #if BUILDFLAG(IS_ANDROID) |
| static constexpr double kRejectionLogNormalMu = 7.4; |
| static constexpr double kRejectionLogNormalSigma = 1.24; |
| #else |
| static constexpr double kRejectionLogNormalMu = 8.6; |
| static constexpr double kRejectionLogNormalSigma = 1.4; |
| #endif // BUILDFLAG(IS_ANDROID) |
| |
| std::string ComputeUrlEncodedTokenPostData( |
| RenderFrameHost& render_frame_host, |
| const url::Origin& idp_origin, |
| const std::string& client_id, |
| const std::string& nonce, |
| const std::string& account_id, |
| bool is_sign_in, |
| bool is_auto_reauthn, |
| const RpMode& rp_mode, |
| const std::vector<std::string>& scope, |
| const std::vector<std::string>& responseType, |
| const base::flat_map<std::string, std::string>& params) { |
| std::string query; |
| if (!client_id.empty()) { |
| query += |
| "client_id=" + base::EscapeUrlEncodedData(client_id, /*use_plus=*/true); |
| } |
| |
| if (!nonce.empty()) { |
| if (!query.empty()) { |
| query += "&"; |
| } |
| query += "nonce=" + base::EscapeUrlEncodedData(nonce, /*use_plus=*/true); |
| } |
| |
| if (!account_id.empty()) { |
| if (!query.empty()) { |
| query += "&"; |
| } |
| query += "account_id=" + |
| base::EscapeUrlEncodedData(account_id, /*use_plus=*/true); |
| } |
| // For new users signing up, we show some disclosure text to remind them about |
| // data sharing between IDP and RP. For returning users signing in, such |
| // disclosure text is not necessary. This field indicates in the request |
| // whether the user has been shown such disclosure text. |
| std::string disclosure_text_shown = is_sign_in ? "false" : "true"; |
| if (!query.empty()) { |
| query += "&"; |
| } |
| query += "disclosure_text_shown=" + disclosure_text_shown; |
| |
| // Shares with IdP that whether the identity credential was automatically |
| // selected. This could help developers to better comprehend the token |
| // request and segment metrics accordingly. |
| std::string is_auto_selected = is_auto_reauthn ? "true" : "false"; |
| if (!query.empty()) { |
| query += "&"; |
| } |
| query += "is_auto_selected=" + is_auto_selected; |
| |
| // TODO(crbug.com/40284792): ButtonMode is enabled by default on the browser |
| // side to support origin trials. To avoid sending "mode=widget" for all |
| // existing traffic, we restrict it to traffic that uses the button mode for |
| // now. We should remove this restriction before shipping the button flow. |
| if (IsFedCmButtonModeEnabled() && rp_mode == RpMode::kButton) { |
| // Shares with IdP the type of the request. |
| std::string rp_mode_str = rp_mode == RpMode::kButton ? "button" : "widget"; |
| if (!query.empty()) { |
| query += "&"; |
| } |
| query += "mode=" + rp_mode_str; |
| } |
| |
| if (webid::IsFedCmAuthzEnabled(render_frame_host, idp_origin)) { |
| // We keep the scope and response_type parameters consistenct with the OIDC |
| // spec [1] to the extent that we can: |
| // |
| // - They are an arrays of strings, separated by spaces |
| // - We use the singular (e.g. "scope") as opposed to the plural |
| // (e.g. "scopes") |
| // |
| // We do, however, use a different escaping character for spaces: "+" |
| // rather than the "%20" to make it consitent with the other |
| // parameters in the FedCM spec. |
| // |
| // [1] https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims |
| if (!scope.empty()) { |
| query += "&scope=" + base::EscapeUrlEncodedData( |
| base::JoinString(scope, " "), /*use_plus=*/true); |
| } |
| if (!responseType.empty()) { |
| query += "&response_type=" + |
| base::EscapeUrlEncodedData(base::JoinString(responseType, " "), |
| /*use_plus=*/true); |
| } |
| for (const auto& pair : params) { |
| // TODO(crbug.com/40262526): Should we use a prefix with these custom |
| // parameters so that they don't collide with the standard ones? |
| query += "&" + base::EscapeUrlEncodedData(pair.first, /*use_plus=*/true) + |
| "=" + base::EscapeUrlEncodedData(pair.second, /*use_plus=*/true); |
| } |
| } |
| |
| return query; |
| } |
| |
| RequestTokenStatus FederatedAuthRequestResultToRequestTokenStatus( |
| FederatedAuthRequestResult result) { |
| // Avoids exposing to renderer detailed error messages which may leak cross |
| // site information to the API call site. |
| switch (result) { |
| case FederatedAuthRequestResult::kSuccess: { |
| return RequestTokenStatus::kSuccess; |
| } |
| case FederatedAuthRequestResult::kErrorTooManyRequests: { |
| return RequestTokenStatus::kErrorTooManyRequests; |
| } |
| case FederatedAuthRequestResult::kErrorCanceled: { |
| return RequestTokenStatus::kErrorCanceled; |
| } |
| case FederatedAuthRequestResult::kShouldEmbargo: |
| case FederatedAuthRequestResult::kErrorDisabledInSettings: |
| case FederatedAuthRequestResult::kErrorFetchingWellKnownHttpNotFound: |
| case FederatedAuthRequestResult::kErrorFetchingWellKnownNoResponse: |
| case FederatedAuthRequestResult::kErrorFetchingWellKnownInvalidResponse: |
| case FederatedAuthRequestResult::kErrorFetchingWellKnownListEmpty: |
| case FederatedAuthRequestResult::kErrorFetchingWellKnownInvalidContentType: |
| case FederatedAuthRequestResult::kErrorConfigNotInWellKnown: |
| case FederatedAuthRequestResult::kErrorWellKnownTooBig: |
| case FederatedAuthRequestResult::kErrorFetchingConfigHttpNotFound: |
| case FederatedAuthRequestResult::kErrorFetchingConfigNoResponse: |
| case FederatedAuthRequestResult::kErrorFetchingConfigInvalidResponse: |
| case FederatedAuthRequestResult::kErrorFetchingConfigInvalidContentType: |
| case FederatedAuthRequestResult::kErrorFetchingClientMetadataHttpNotFound: |
| case FederatedAuthRequestResult::kErrorFetchingClientMetadataNoResponse: |
| case FederatedAuthRequestResult:: |
| kErrorFetchingClientMetadataInvalidResponse: |
| case FederatedAuthRequestResult:: |
| kErrorFetchingClientMetadataInvalidContentType: |
| case FederatedAuthRequestResult::kErrorFetchingAccountsHttpNotFound: |
| case FederatedAuthRequestResult::kErrorFetchingAccountsNoResponse: |
| case FederatedAuthRequestResult::kErrorFetchingAccountsInvalidResponse: |
| case FederatedAuthRequestResult::kErrorFetchingAccountsListEmpty: |
| case FederatedAuthRequestResult::kErrorFetchingAccountsInvalidContentType: |
| case FederatedAuthRequestResult::kErrorFetchingIdTokenHttpNotFound: |
| case FederatedAuthRequestResult::kErrorFetchingIdTokenNoResponse: |
| case FederatedAuthRequestResult::kErrorFetchingIdTokenInvalidResponse: |
| case FederatedAuthRequestResult::kErrorFetchingIdTokenIdpErrorResponse: |
| case FederatedAuthRequestResult:: |
| kErrorFetchingIdTokenCrossSiteIdpErrorResponse: |
| case FederatedAuthRequestResult::kErrorFetchingIdTokenInvalidContentType: |
| case FederatedAuthRequestResult::kErrorRpPageNotVisible: |
| case FederatedAuthRequestResult::kErrorSilentMediationFailure: |
| case FederatedAuthRequestResult::kErrorThirdPartyCookiesBlocked: |
| case FederatedAuthRequestResult::kErrorNotSignedInWithIdp: |
| case FederatedAuthRequestResult::kErrorMissingTransientUserActivation: |
| case FederatedAuthRequestResult::kErrorReplacedByButtonMode: |
| case FederatedAuthRequestResult::kError: { |
| return RequestTokenStatus::kError; |
| } |
| } |
| } |
| |
| IdpNetworkRequestManager::MetricsEndpointErrorCode |
| FederatedAuthRequestResultToMetricsEndpointErrorCode( |
| blink::mojom::FederatedAuthRequestResult result) { |
| switch (result) { |
| case FederatedAuthRequestResult::kSuccess: { |
| return IdpNetworkRequestManager::MetricsEndpointErrorCode::kNone; |
| } |
| case FederatedAuthRequestResult::kErrorTooManyRequests: |
| case FederatedAuthRequestResult::kErrorMissingTransientUserActivation: |
| case FederatedAuthRequestResult::kErrorCanceled: { |
| return IdpNetworkRequestManager::MetricsEndpointErrorCode::kRpFailure; |
| } |
| case FederatedAuthRequestResult::kErrorFetchingAccountsInvalidResponse: |
| case FederatedAuthRequestResult::kErrorFetchingAccountsListEmpty: |
| case FederatedAuthRequestResult::kErrorFetchingAccountsInvalidContentType: { |
| return IdpNetworkRequestManager::MetricsEndpointErrorCode:: |
| kAccountsEndpointInvalidResponse; |
| } |
| case FederatedAuthRequestResult::kErrorFetchingIdTokenInvalidResponse: |
| case FederatedAuthRequestResult::kErrorFetchingIdTokenIdpErrorResponse: |
| case FederatedAuthRequestResult:: |
| kErrorFetchingIdTokenCrossSiteIdpErrorResponse: |
| case FederatedAuthRequestResult::kErrorFetchingIdTokenInvalidContentType: { |
| return IdpNetworkRequestManager::MetricsEndpointErrorCode:: |
| kTokenEndpointInvalidResponse; |
| } |
| case FederatedAuthRequestResult::kShouldEmbargo: |
| case FederatedAuthRequestResult::kErrorDisabledInSettings: |
| case FederatedAuthRequestResult::kErrorThirdPartyCookiesBlocked: |
| case FederatedAuthRequestResult::kErrorRpPageNotVisible: |
| case FederatedAuthRequestResult::kErrorReplacedByButtonMode: |
| case FederatedAuthRequestResult::kErrorNotSignedInWithIdp: { |
| return IdpNetworkRequestManager::MetricsEndpointErrorCode::kUserFailure; |
| } |
| case FederatedAuthRequestResult::kErrorFetchingWellKnownHttpNotFound: |
| case FederatedAuthRequestResult::kErrorFetchingWellKnownNoResponse: |
| case FederatedAuthRequestResult::kErrorFetchingConfigHttpNotFound: |
| case FederatedAuthRequestResult::kErrorFetchingConfigNoResponse: |
| case FederatedAuthRequestResult::kErrorFetchingClientMetadataHttpNotFound: |
| case FederatedAuthRequestResult::kErrorFetchingClientMetadataNoResponse: |
| case FederatedAuthRequestResult::kErrorFetchingAccountsHttpNotFound: |
| case FederatedAuthRequestResult::kErrorFetchingAccountsNoResponse: |
| case FederatedAuthRequestResult::kErrorFetchingIdTokenHttpNotFound: |
| case FederatedAuthRequestResult::kErrorFetchingIdTokenNoResponse: { |
| return IdpNetworkRequestManager::MetricsEndpointErrorCode:: |
| kIdpServerUnavailable; |
| } |
| case FederatedAuthRequestResult::kErrorConfigNotInWellKnown: |
| case FederatedAuthRequestResult::kErrorWellKnownTooBig: { |
| return IdpNetworkRequestManager::MetricsEndpointErrorCode::kManifestError; |
| } |
| case FederatedAuthRequestResult::kErrorFetchingWellKnownListEmpty: |
| case FederatedAuthRequestResult::kErrorFetchingWellKnownInvalidResponse: |
| case FederatedAuthRequestResult::kErrorFetchingConfigInvalidResponse: |
| case FederatedAuthRequestResult:: |
| kErrorFetchingClientMetadataInvalidResponse: |
| case FederatedAuthRequestResult::kErrorFetchingWellKnownInvalidContentType: |
| case FederatedAuthRequestResult::kErrorFetchingConfigInvalidContentType: |
| case FederatedAuthRequestResult:: |
| kErrorFetchingClientMetadataInvalidContentType: { |
| return IdpNetworkRequestManager::MetricsEndpointErrorCode:: |
| kIdpServerInvalidResponse; |
| } |
| case FederatedAuthRequestResult::kError: |
| case FederatedAuthRequestResult::kErrorSilentMediationFailure: { |
| return IdpNetworkRequestManager::MetricsEndpointErrorCode::kOther; |
| } |
| } |
| } |
| |
| // The time from when the accounts dialog is shown to when a user explicitly |
| // closes it follows normal distribution. To make the random failures |
| // indistinguishable from user declines, we use lognormal distribution to |
| // generate the random number. |
| base::TimeDelta GetRandomRejectionTime() { |
| base::RandomBitGenerator generator; |
| std::lognormal_distribution<double> distribution(kRejectionLogNormalMu, |
| kRejectionLogNormalSigma); |
| |
| base::TimeDelta rejection_time = |
| base::Seconds(distribution(generator) / 1000); |
| |
| return std::min(kMaxRejectionTime, rejection_time); |
| } |
| |
| std::string FormatOriginForDisplay(const url::Origin& origin) { |
| return webid::FormatUrlForDisplay(origin.GetURL()); |
| } |
| |
| FedCmMetrics::NumAccounts ComputeNumMatchingAccounts( |
| const IdpNetworkRequestManager::AccountList& accounts) { |
| if (accounts.empty()) { |
| return FedCmMetrics::NumAccounts::kZero; |
| } |
| if (accounts.size() == 1u) { |
| return FedCmMetrics::NumAccounts::kOne; |
| } |
| return FedCmMetrics::NumAccounts::kMultiple; |
| } |
| |
| void FilterAccountsWithLabel(const std::string& label, |
| IdpNetworkRequestManager::AccountList& accounts) { |
| if (label.empty()) { |
| return; |
| } |
| |
| // Remove all accounts whose labels do not match the requested label. |
| // Note that it is technically possible for us to end up with more than one |
| // account afterwards, in which case the multiple account chooser would be |
| // shown. |
| auto filter = [&label](const IdentityRequestAccount& account) { |
| return !base::Contains(account.labels, label); |
| }; |
| std::erase_if(accounts, filter); |
| FedCmMetrics::NumAccounts num_matching = ComputeNumMatchingAccounts(accounts); |
| base::UmaHistogramEnumeration("Blink.FedCm.AccountLabel.NumMatchingAccounts", |
| num_matching); |
| } |
| |
| void FilterAccountsWithLoginHint( |
| const std::string& login_hint, |
| IdpNetworkRequestManager::AccountList& accounts) { |
| if (login_hint.empty()) { |
| return; |
| } |
| |
| // Remove all accounts whose ID and whose email do not match the login hint. |
| // Note that it is technically possible for us to end up with more than one |
| // account afterwards, in which case the multiple account chooser would be |
| // shown. |
| auto filter = [&login_hint](const IdentityRequestAccount& account) { |
| return !base::Contains(account.login_hints, login_hint); |
| }; |
| std::erase_if(accounts, filter); |
| FedCmMetrics::NumAccounts num_matching = ComputeNumMatchingAccounts(accounts); |
| base::UmaHistogramEnumeration("Blink.FedCm.LoginHint.NumMatchingAccounts", |
| num_matching); |
| } |
| |
| void FilterAccountsWithDomainHint( |
| const std::string& domain_hint, |
| IdpNetworkRequestManager::AccountList& accounts) { |
| if (domain_hint.empty()) { |
| return; |
| } |
| |
| if (domain_hint == FederatedAuthRequestImpl::kWildcardDomainHint) { |
| auto filter = [](const IdentityRequestAccount& account) { |
| return account.domain_hints.empty(); |
| }; |
| std::erase_if(accounts, filter); |
| } else { |
| auto filter = [&domain_hint](const IdentityRequestAccount& account) { |
| return !base::Contains(account.domain_hints, domain_hint); |
| }; |
| std::erase_if(accounts, filter); |
| } |
| FedCmMetrics::NumAccounts num_matching = ComputeNumMatchingAccounts(accounts); |
| base::UmaHistogramEnumeration("Blink.FedCm.DomainHint.NumMatchingAccounts", |
| num_matching); |
| } |
| |
| std::string GetTopFrameOriginForDisplay(const url::Origin& top_frame_origin) { |
| return FormatOriginForDisplay(top_frame_origin); |
| } |
| |
| std::optional<std::string> GetIframeOriginForDisplay( |
| const url::Origin& top_frame_origin, |
| const url::Origin& iframe_origin, |
| CompleteRequestWithErrorCallback callback) { |
| // TOOD(crbug.com/1448566): clean up the logic to allow 3 domains. |
| bool exclude_iframe = true; |
| std::optional<std::string> iframe_for_display = std::nullopt; |
| |
| if (!exclude_iframe) { |
| iframe_for_display = FormatOriginForDisplay(iframe_origin); |
| |
| // TODO(crbug.com/40259453): Decide what to do if we want to include iframe |
| // domain in the dialog but iframe_for_display is opaque. |
| if (iframe_origin.opaque()) { |
| std::move(callback).Run(FederatedAuthRequestResult::kError, |
| /*token_status=*/std::nullopt, |
| /*token_error=*/std::nullopt, |
| /*should_delay_callback=*/false); |
| } |
| } |
| |
| return iframe_for_display; |
| } |
| |
| bool IsFrameActive(RenderFrameHost* frame) { |
| return frame && frame->IsActive(); |
| } |
| |
| bool IsFrameVisible(RenderFrameHost* frame) { |
| return frame && frame->IsActive() && |
| frame->GetVisibilityState() == content::PageVisibilityState::kVisible; |
| } |
| |
| void MaybeAppendQueryParameters( |
| const FederatedAuthRequestImpl::IdentityProviderLoginUrlInfo& |
| idp_login_info, |
| GURL* login_url) { |
| if (idp_login_info.login_hint.empty() && idp_login_info.domain_hint.empty()) { |
| return; |
| } |
| std::string old_query = login_url->query(); |
| if (!old_query.empty()) { |
| old_query += "&"; |
| } |
| std::string new_query_string = old_query; |
| if (!idp_login_info.login_hint.empty()) { |
| new_query_string += |
| "login_hint=" + base::EscapeUrlEncodedData(idp_login_info.login_hint, |
| /*use_plus=*/false); |
| } |
| if (!idp_login_info.domain_hint.empty()) { |
| if (!new_query_string.empty()) { |
| new_query_string += "&"; |
| } |
| new_query_string += |
| "domain_hint=" + base::EscapeUrlEncodedData(idp_login_info.domain_hint, |
| /*use_plus=*/false); |
| } |
| GURL::Replacements replacements; |
| replacements.SetQueryStr(new_query_string); |
| *login_url = login_url->ReplaceComponents(replacements); |
| } |
| |
| } // namespace |
| |
| FederatedAuthRequestImpl::FetchData::FetchData() = default; |
| FederatedAuthRequestImpl::FetchData::~FetchData() = default; |
| |
| FederatedAuthRequestImpl::IdentityProviderGetInfo::IdentityProviderGetInfo( |
| blink::mojom::IdentityProviderRequestOptionsPtr provider, |
| blink::mojom::RpContext rp_context, |
| blink::mojom::RpMode rp_mode) |
| : provider(std::move(provider)), rp_context(rp_context), rp_mode(rp_mode) {} |
| |
| FederatedAuthRequestImpl::IdentityProviderGetInfo::~IdentityProviderGetInfo() = |
| default; |
| FederatedAuthRequestImpl::IdentityProviderGetInfo::IdentityProviderGetInfo( |
| const IdentityProviderGetInfo& other) { |
| *this = other; |
| } |
| |
| FederatedAuthRequestImpl::IdentityProviderGetInfo& |
| FederatedAuthRequestImpl::IdentityProviderGetInfo::operator=( |
| const IdentityProviderGetInfo& other) { |
| provider = other.provider->Clone(); |
| rp_context = other.rp_context; |
| rp_mode = other.rp_mode; |
| return *this; |
| } |
| |
| FederatedAuthRequestImpl::IdentityProviderInfo::IdentityProviderInfo( |
| const blink::mojom::IdentityProviderRequestOptionsPtr& provider, |
| IdpNetworkRequestManager::Endpoints endpoints, |
| IdentityProviderMetadata metadata, |
| blink::mojom::RpContext rp_context, |
| blink::mojom::RpMode rp_mode) |
| : provider(provider->Clone()), |
| endpoints(std::move(endpoints)), |
| metadata(std::move(metadata)), |
| rp_context(rp_context), |
| rp_mode(rp_mode) {} |
| |
| FederatedAuthRequestImpl::IdentityProviderInfo::~IdentityProviderInfo() = |
| default; |
| FederatedAuthRequestImpl::IdentityProviderInfo::IdentityProviderInfo( |
| const IdentityProviderInfo& other) { |
| provider = other.provider->Clone(); |
| endpoints = other.endpoints; |
| metadata = other.metadata; |
| has_failing_idp_signin_status = other.has_failing_idp_signin_status; |
| rp_context = other.rp_context; |
| rp_mode = other.rp_mode; |
| data = other.data; |
| } |
| |
| FederatedAuthRequestImpl::FederatedAuthRequestImpl( |
| RenderFrameHost& host, |
| FederatedIdentityApiPermissionContextDelegate* api_permission_context, |
| FederatedIdentityAutoReauthnPermissionContextDelegate* |
| auto_reauthn_permission_context, |
| FederatedIdentityPermissionContextDelegate* permission_context, |
| IdentityRegistry* identity_registry, |
| mojo::PendingReceiver<blink::mojom::FederatedAuthRequest> receiver) |
| : DocumentService(host, std::move(receiver)), |
| api_permission_delegate_(api_permission_context), |
| auto_reauthn_permission_delegate_(auto_reauthn_permission_context), |
| permission_delegate_(permission_context), |
| identity_registry_(identity_registry) {} |
| |
| FederatedAuthRequestImpl::~FederatedAuthRequestImpl() { |
| // Ensures key data members are destructed in proper order and resolves any |
| // pending promise. |
| if (auth_request_token_callback_) { |
| CompleteRequestWithError(FederatedAuthRequestResult::kError, |
| TokenStatus::kUnhandledRequest, |
| /*token_error=*/std::nullopt, |
| /*should_delay_callback=*/false); |
| } |
| // Calls |FederatedAuthUserInfoRequest|'s destructor to complete the user |
| // info request. This is needed because otherwise some resources like |
| // `fedcm_metrics_` may no longer be usable when the destructor get invoked |
| // naturally. |
| user_info_requests_.clear(); |
| |
| // Calls |FederatedAuthDisconnectRequest|'s destructor to complete the |
| // revocation request. This is needed because otherwise some resources like |
| // `fedcm_metrics_` may no longer be usable when the destructor get invoked |
| // naturally. |
| disconnect_request_.reset(); |
| |
| // Since FederatedAuthRequestImpl is a subclass of |
| // DocumentService<blink::mojom::FederatedAuthRequest>, it only lives as long |
| // as the current document. |
| if (num_requests_ > 0) { |
| FedCmMetrics::RecordNumRequestsPerDocument( |
| render_frame_host().GetPageUkmSourceId(), num_requests_); |
| } |
| } |
| |
| // static |
| void FederatedAuthRequestImpl::Create( |
| RenderFrameHost* host, |
| mojo::PendingReceiver<blink::mojom::FederatedAuthRequest> receiver) { |
| CHECK(host); |
| |
| BrowserContext* browser_context = host->GetBrowserContext(); |
| raw_ptr<FederatedIdentityApiPermissionContextDelegate> |
| api_permission_context = |
| browser_context->GetFederatedIdentityApiPermissionContext(); |
| raw_ptr<FederatedIdentityAutoReauthnPermissionContextDelegate> |
| auto_reauthn_permission_context = |
| browser_context->GetFederatedIdentityAutoReauthnPermissionContext(); |
| raw_ptr<FederatedIdentityPermissionContextDelegate> permission_context = |
| browser_context->GetFederatedIdentityPermissionContext(); |
| raw_ptr<IdentityRegistry> identity_registry = |
| IdentityRegistry::FromWebContents(WebContents::FromRenderFrameHost(host)); |
| |
| if (!api_permission_context || !auto_reauthn_permission_context || |
| !permission_context) { |
| return; |
| } |
| |
| // FederatedAuthRequestImpl owns itself. It will self-destruct when a mojo |
| // interface error occurs, the RenderFrameHost is deleted, or the |
| // RenderFrameHost navigates to a new document. |
| new FederatedAuthRequestImpl( |
| *host, api_permission_context, auto_reauthn_permission_context, |
| permission_context, identity_registry, std::move(receiver)); |
| } |
| |
| FederatedAuthRequestImpl& FederatedAuthRequestImpl::CreateForTesting( |
| RenderFrameHost& host, |
| FederatedIdentityApiPermissionContextDelegate* api_permission_context, |
| FederatedIdentityAutoReauthnPermissionContextDelegate* |
| auto_reauthn_permission_context, |
| FederatedIdentityPermissionContextDelegate* permission_context, |
| IdentityRegistry* identity_registry, |
| mojo::PendingReceiver<blink::mojom::FederatedAuthRequest> receiver) { |
| return *new FederatedAuthRequestImpl( |
| host, api_permission_context, auto_reauthn_permission_context, |
| permission_context, identity_registry, std::move(receiver)); |
| } |
| |
| std::vector<blink::mojom::IdentityProviderRequestOptionsPtr> |
| FederatedAuthRequestImpl::MaybeAddRegisteredProviders( |
| std::vector<blink::mojom::IdentityProviderRequestOptionsPtr>& providers) { |
| std::vector<blink::mojom::IdentityProviderRequestOptionsPtr> result; |
| |
| std::vector<GURL> registered_config_urls = |
| permission_delegate_->GetRegisteredIdPs(); |
| |
| // TODO(crbug.com/40252825): we insert the registered IdPs to |
| // the list of IdPs in a reverse chronological order: |
| // first IdPs to be registered goes first. It is not clear |
| // yet what's the right order, but this seems like a reasonable |
| // starting point. |
| std::reverse(registered_config_urls.begin(), registered_config_urls.end()); |
| |
| for (auto& provider : providers) { |
| if (!provider->config->use_registered_config_urls) { |
| result.emplace_back(provider->Clone()); |
| continue; |
| } |
| |
| for (auto& configURL : registered_config_urls) { |
| blink::mojom::IdentityProviderRequestOptionsPtr idp = provider->Clone(); |
| idp->config->use_registered_config_urls = false; |
| idp->config->config_url = configURL; |
| result.emplace_back(std::move(idp)); |
| } |
| } |
| |
| // TODO(crbug.com/40252825): Consider removing duplicate |
| // IdPs in case they were present in the registry as well |
| // as added individually. |
| |
| return result; |
| } |
| |
| void FederatedAuthRequestImpl::RequestToken( |
| std::vector<IdentityProviderGetParametersPtr> idp_get_params_ptrs, |
| MediationRequirement requirement, |
| RequestTokenCallback callback) { |
| // idp_get_params_ptrs should never be empty since it is the renderer-side |
| // code which populates it. |
| if (idp_get_params_ptrs.empty()) { |
| ReportBadMessageAndDeleteThis("idp_get_params_ptrs is empty."); |
| return; |
| } |
| // This could only happen with a compromised renderer process. We ensure that |
| // the provider list size is > 0 on the renderer side at the beginning of |
| // parsing |IdentityCredentialRequestOptions|. |
| for (auto& idp_get_params_ptr : idp_get_params_ptrs) { |
| if (idp_get_params_ptr->providers.size() == 0) { |
| ReportBadMessageAndDeleteThis("The provider list should not be empty."); |
| return; |
| } |
| } |
| |
| if (render_frame_host().IsNestedWithinFencedFrame()) { |
| ReportBadMessageAndDeleteThis( |
| "FedCM should not be allowed in fenced frame trees."); |
| return; |
| } |
| |
| bool intercept = false; |
| bool should_complete_request_immediately = false; |
| devtools_instrumentation::WillSendFedCmRequest( |
| render_frame_host(), &intercept, &should_complete_request_immediately); |
| should_complete_request_immediately = |
| (intercept && should_complete_request_immediately) || |
| api_permission_delegate_->ShouldCompleteRequestImmediately(); |
| |
| // Expand the providers list with registered providers. |
| if (IsFedCmIdPRegistrationEnabled()) { |
| for (auto& idp_get_params_ptr : idp_get_params_ptrs) { |
| std::vector<blink::mojom::IdentityProviderRequestOptionsPtr> providers = |
| MaybeAddRegisteredProviders(idp_get_params_ptr->providers); |
| if (providers.empty()) { |
| render_frame_host().AddMessageToConsole( |
| blink::mojom::ConsoleMessageLevel::kError, |
| "No identity providers are registered."); |
| base::TimeDelta delay; |
| if (!should_complete_request_immediately) { |
| delay = GetRandomRejectionTime(); |
| } |
| base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask( |
| FROM_HERE, |
| base::BindOnce(std::move(callback), RequestTokenStatus::kError, |
| std::nullopt, "", |
| /*error=*/nullptr, |
| /*is_auto_selected=*/false), |
| delay); |
| return; |
| } |
| idp_get_params_ptr->providers = std::move(providers); |
| } |
| } |
| |
| if (!render_frame_host().GetPage().IsPrimary()) { |
| // This should not be possible but seems to be happening, so we log |
| // the lifecycle state for further investigation. |
| RenderFrameHostImpl* host_impl = |
| static_cast<RenderFrameHostImpl*>(&render_frame_host()); |
| FedCmLifecycleStateFailureReason reason = |
| FedCmLifecycleStateFailureReason::kOther; |
| switch (host_impl->lifecycle_state()) { |
| case RenderFrameHostImpl::LifecycleStateImpl::kSpeculative: |
| reason = FedCmLifecycleStateFailureReason::kSpeculative; |
| break; |
| case RenderFrameHostImpl::LifecycleStateImpl::kPendingCommit: |
| reason = FedCmLifecycleStateFailureReason::kPendingCommit; |
| break; |
| case RenderFrameHostImpl::LifecycleStateImpl::kPrerendering: |
| reason = FedCmLifecycleStateFailureReason::kPrerendering; |
| break; |
| case RenderFrameHostImpl::LifecycleStateImpl::kInBackForwardCache: |
| reason = FedCmLifecycleStateFailureReason::kInBackForwardCache; |
| break; |
| case RenderFrameHostImpl::LifecycleStateImpl::kRunningUnloadHandlers: |
| reason = FedCmLifecycleStateFailureReason::kRunningUnloadHandlers; |
| break; |
| case RenderFrameHostImpl::LifecycleStateImpl::kReadyToBeDeleted: |
| reason = FedCmLifecycleStateFailureReason::kReadyToBeDeleted; |
| break; |
| default: |
| break; |
| }; |
| RecordLifecycleStateFailureReason(reason); |
| std::move(callback).Run(RequestTokenStatus::kError, std::nullopt, "", |
| /*error=*/nullptr, |
| /*is_auto_selected=*/false); |
| return; |
| } |
| |
| // It should not be possible to receive multiple IDPs when the |
| // `kFedCmMultipleIdentityProviders` flag is disabled. But such a message |
| // could be received from a compromised renderer. |
| const bool is_multi_idp_input = idp_get_params_ptrs.size() > 1u || |
| idp_get_params_ptrs[0]->providers.size() > 1u; |
| if (is_multi_idp_input && !IsFedCmMultipleIdentityProvidersEnabled()) { |
| std::move(callback).Run(RequestTokenStatus::kError, std::nullopt, "", |
| /*error=*/nullptr, |
| /*is_auto_selected=*/false); |
| return; |
| } |
| |
| had_transient_user_activation_ = |
| render_frame_host().HasTransientUserActivation(); |
| |
| MaybeCreateFedCmMetrics(); |
| int old_session_id = fedcm_metrics_->session_id(); |
| fedcm_metrics_->SetSessionID(webid::GetNewSessionID()); |
| // Store the previous `idp_order_` value from this class. Note that this is {} |
| // unless there is a pending request from the same RFH. In particular, this is |
| // still {} if there is a pending request but from a different RFH. |
| std::vector<GURL> old_idp_order = std::move(idp_order_); |
| idp_order_ = {}; |
| for (auto& idp_get_params_ptr : idp_get_params_ptrs) { |
| for (auto& idp_ptr : idp_get_params_ptr->providers) { |
| idp_order_.push_back(idp_ptr->config->config_url); |
| } |
| } |
| |
| if (HasPendingRequest()) { |
| FederatedAuthRequestImpl* pending_request = |
| webid::GetPageData(&render_frame_host())->PendingWebIdentityRequest(); |
| |
| RpMode pending_request_rp_mode = pending_request->GetRpMode(); |
| RpMode new_request_rp_mode = idp_get_params_ptrs[0]->mode; |
| fedcm_metrics_->RecordMultipleRequestsRpMode( |
| pending_request_rp_mode, new_request_rp_mode, idp_order_); |
| |
| bool can_replace_pending_request = |
| IsFedCmButtonModeEnabled() && had_transient_user_activation_ && |
| new_request_rp_mode == RpMode::kButton && |
| pending_request_rp_mode != RpMode::kButton; |
| if (!can_replace_pending_request) { |
| // Cancel this new request. |
| fedcm_metrics_->RecordRequestTokenStatus( |
| TokenStatus::kTooManyRequests, requirement, idp_order_, |
| /*num_idps_mismatch=*/0, |
| /*selected_idp_config_url=*/std::nullopt, |
| (IsFedCmButtonModeEnabled() && |
| idp_get_params_ptrs[0]->mode == blink::mojom::RpMode::kButton) |
| ? RpMode::kButton |
| : RpMode::kWidget); |
| |
| AddDevToolsIssue( |
| blink::mojom::FederatedAuthRequestResult::kErrorTooManyRequests); |
| AddConsoleErrorMessage( |
| blink::mojom::FederatedAuthRequestResult::kErrorTooManyRequests); |
| |
| std::move(callback).Run(RequestTokenStatus::kErrorTooManyRequests, |
| std::nullopt, "", /*error=*/nullptr, |
| /*is_auto_selected=*/false); |
| // The old request is alive, so set the session ID to the old one. |
| fedcm_metrics_->SetSessionID(old_session_id); |
| idp_order_ = std::move(old_idp_order); |
| return; |
| } |
| |
| // Cancel the pending request before starting the new button flow request. |
| // Set the old values before completing in case the pending request |
| // corresponds to one in this object. |
| int new_session_id = fedcm_metrics_->session_id(); |
| std::vector<GURL> new_idp_order = std::move(idp_order_); |
| fedcm_metrics_->SetSessionID(old_session_id); |
| idp_order_ = std::move(old_idp_order); |
| pending_request->CompleteRequestWithError( |
| FederatedAuthRequestResult::kErrorReplacedByButtonMode, |
| TokenStatus::kReplacedByButtonMode, |
| /*token_error=*/std::nullopt, |
| /*should_delay_callback=*/false); |
| CHECK(!auth_request_token_callback_); |
| |
| // Some members were reset to false during CleanUp when replacing a widget |
| // flow from the same frame so we need to set them again. |
| had_transient_user_activation_ = true; |
| fedcm_metrics_->SetSessionID(new_session_id); |
| idp_order_ = std::move(new_idp_order); |
| } |
| |
| should_complete_request_immediately_ = should_complete_request_immediately; |
| mediation_requirement_ = requirement; |
| auth_request_token_callback_ = std::move(callback); |
| webid::GetPageData(&render_frame_host())->SetPendingWebIdentityRequest(this); |
| network_manager_ = CreateNetworkManager(); |
| request_dialog_controller_ = CreateDialogController(); |
| start_time_ = base::TimeTicks::Now(); |
| // TODO(crbug.com/40218857): handle button mode with multiple IdP. |
| if (IsFedCmButtonModeEnabled() && |
| idp_get_params_ptrs[0]->mode == blink::mojom::RpMode::kButton) { |
| rp_mode_ = RpMode::kButton; |
| std::optional<base::TimeTicks> user_info_accounts_response_time = |
| webid::GetPageData(&render_frame_host()) |
| ->ConsumeUserInfoAccountsResponseTime( |
| idp_get_params_ptrs[0]->providers[0]->config->config_url); |
| if (user_info_accounts_response_time) { |
| fedcm_metrics_->RecordTimeBetweenUserInfoAndButtonModeAPI( |
| start_time_ - user_info_accounts_response_time.value()); |
| } |
| // TODO(crbug.com/329235198): Support other mediation mode in button mode. |
| mediation_requirement_ = MediationRequirement::kRequired; |
| if (!had_transient_user_activation_) { |
| CompleteRequestWithError( |
| FederatedAuthRequestResult::kErrorMissingTransientUserActivation, |
| TokenStatus::kMissingTransientUserActivation, |
| /*token_error=*/std::nullopt, |
| /*should_delay_callback=*/false); |
| return; |
| } |
| } else { |
| rp_mode_ = RpMode::kWidget; |
| } |
| |
| FederatedApiPermissionStatus permission_status = GetApiPermissionStatus(); |
| |
| std::optional<TokenStatus> error_token_status; |
| FederatedAuthRequestResult request_result = |
| FederatedAuthRequestResult::kError; |
| |
| switch (permission_status) { |
| case FederatedApiPermissionStatus::BLOCKED_VARIATIONS: |
| error_token_status = TokenStatus::kDisabledInFlags; |
| break; |
| case FederatedApiPermissionStatus::BLOCKED_SETTINGS: |
| error_token_status = TokenStatus::kDisabledInSettings; |
| request_result = FederatedAuthRequestResult::kErrorDisabledInSettings; |
| break; |
| case FederatedApiPermissionStatus::BLOCKED_EMBARGO: |
| error_token_status = TokenStatus::kDisabledEmbargo; |
| request_result = FederatedAuthRequestResult::kErrorDisabledInSettings; |
| break; |
| case FederatedApiPermissionStatus::GRANTED: |
| // Intentional fall-through. |
| break; |
| default: |
| NOTREACHED(); |
| break; |
| } |
| |
| if (error_token_status && rp_mode_ == RpMode::kWidget) { |
| CompleteRequestWithError(request_result, *error_token_status, |
| /*token_error=*/std::nullopt, |
| /*should_delay_callback=*/true); |
| return; |
| } |
| |
| ++num_requests_; |
| |
| std::set<GURL> unique_idps; |
| for (auto& idp_get_params_ptr : idp_get_params_ptrs) { |
| for (auto& idp_ptr : idp_get_params_ptr->providers) { |
| // Throw an error if duplicate IDPs are specified. |
| const bool is_unique_idp = |
| unique_idps.insert(idp_ptr->config->config_url).second; |
| if (!is_unique_idp) { |
| CompleteRequestWithError(FederatedAuthRequestResult::kError, |
| /*token_status=*/std::nullopt, |
| /*token_error=*/std::nullopt, |
| /*should_delay_callback=*/false); |
| return; |
| } |
| |
| url::Origin idp_origin = url::Origin::Create(idp_ptr->config->config_url); |
| if (!network::IsOriginPotentiallyTrustworthy(idp_origin)) { |
| CompleteRequestWithError(FederatedAuthRequestResult::kError, |
| TokenStatus::kIdpNotPotentiallyTrustworthy, |
| /*token_error=*/std::nullopt, |
| /*should_delay_callback=*/false); |
| return; |
| } |
| } |
| } |
| |
| bool any_idp_has_custom_scopes = false; |
| bool any_idp_has_parameters = false; |
| for (auto& idp_get_params_ptr : idp_get_params_ptrs) { |
| for (auto& idp_ptr : idp_get_params_ptr->providers) { |
| bool has_failing_idp_signin_status = |
| webid::ShouldFailAccountsEndpointRequestBecauseNotSignedInWithIdp( |
| render_frame_host(), idp_ptr->config->config_url, |
| permission_delegate_); |
| |
| url::Origin idp_origin = url::Origin::Create(idp_ptr->config->config_url); |
| if (has_failing_idp_signin_status && |
| webid::GetIdpSigninStatusMode(render_frame_host(), idp_origin) == |
| FedCmIdpSigninStatusMode::ENABLED) { |
| if (idp_get_params_ptr->mode == blink::mojom::RpMode::kWidget) { |
| if (IsFedCmMultipleIdentityProvidersEnabled()) { |
| // In the multi IDP case, we do not want to complete the request |
| // right away as there are other IDPs which may be logged in. But we |
| // also do not want to fetch this IDP. |
| unique_idps.erase(idp_ptr->config->config_url); |
| continue; |
| } |
| // If the user is known to be signed-out and the RP is request |
| // a widget, we fail the request early before fetching anything. |
| CompleteRequestWithError( |
| FederatedAuthRequestResult::kErrorNotSignedInWithIdp, |
| TokenStatus::kNotSignedInWithIdp, |
| /*token_error=*/std::nullopt, |
| /*should_delay_callback=*/true); |
| return; |
| } else if (idp_get_params_ptr->mode == blink::mojom::RpMode::kButton) { |
| // Only a compromised renderer can set mode = button without the |
| // ButtonMode enabled (which controls the JS WebIDL), so we crash |
| // here if we ever get to this situation. |
| if (!IsFedCmButtonModeEnabled()) { |
| ReportBadMessageAndDeleteThis("FedCM button mode is not enabled."); |
| return; |
| } |
| // We fail sooner before, but just to double check, we assert that |
| // we are inside a user gesture here again. |
| CHECK(had_transient_user_activation_); |
| } |
| } |
| if (ShouldFailBeforeFetchingAccounts(idp_ptr->config->config_url)) { |
| if (IsFedCmMultipleIdentityProvidersEnabled()) { |
| // In the multi IDP case, we do not want to complete the request right |
| // away as there are other IDPs which may be logged in. But we also do |
| // not want to fetch this IDP. |
| unique_idps.erase(idp_ptr->config->config_url); |
| continue; |
| } |
| CompleteRequestWithError( |
| FederatedAuthRequestResult::kErrorSilentMediationFailure, |
| TokenStatus::kSilentMediationFailure, |
| /*token_error=*/std::nullopt, |
| /*should_delay_callback=*/false); |
| return; |
| } |
| |
| if (webid::IsFedCmAuthzEnabled(render_frame_host(), idp_origin)) { |
| any_idp_has_custom_scopes = |
| any_idp_has_custom_scopes || !ShouldMediateAuthzFor(*idp_ptr); |
| any_idp_has_parameters = |
| any_idp_has_parameters || !idp_ptr->params.empty(); |
| } |
| |
| blink::mojom::RpContext rp_context = idp_get_params_ptr->context; |
| blink::mojom::RpMode rp_mode = idp_get_params_ptr->mode; |
| const GURL& idp_config_url = idp_ptr->config->config_url; |
| token_request_get_infos_.emplace( |
| idp_config_url, |
| IdentityProviderGetInfo(std::move(idp_ptr), rp_context, rp_mode)); |
| } |
| } |
| if (any_idp_has_parameters || any_idp_has_custom_scopes) { |
| FedCmRpParameters parameters; |
| if (any_idp_has_custom_scopes && any_idp_has_parameters) { |
| parameters = FedCmRpParameters::kHasParametersAndNonDefaultScope; |
| } else if (any_idp_has_parameters) { |
| parameters = FedCmRpParameters::kHasParameters; |
| } else { |
| DCHECK(any_idp_has_custom_scopes); |
| parameters = FedCmRpParameters::kHasNonDefaultScope; |
| } |
| fedcm_metrics_->RecordRpParameters(parameters); |
| } |
| |
| if (IsFedCmMultipleIdentityProvidersEnabled() && unique_idps.empty()) { |
| // At this point either all IDPs are signed out or mediation:silent was used |
| // and there are no returning accounts. |
| bool should_delay_callback = |
| mediation_requirement_ == MediationRequirement::kSilent ? false : true; |
| auto result = mediation_requirement_ == MediationRequirement::kSilent |
| ? FederatedAuthRequestResult::kErrorSilentMediationFailure |
| : FederatedAuthRequestResult::kErrorNotSignedInWithIdp; |
| auto token_status = mediation_requirement_ == MediationRequirement::kSilent |
| ? TokenStatus::kSilentMediationFailure |
| : TokenStatus::kNotSignedInWithIdp; |
| CompleteRequestWithError(result, token_status, /*token_error=*/std::nullopt, |
| should_delay_callback); |
| return; |
| } |
| |
| // Show loading dialog while fetching endpoints if it is a button flow. This |
| // is needed even if the LoginStatus is "logged-out" because we need to fetch |
| // the config file to get the login_url which may take some time. |
| if (rp_mode_ == RpMode::kButton) { |
| CHECK(idp_order_.size() > 0); |
| // TODO(crbug.com/40218857): Handle button mode with multiple IdP. |
| const GURL& idp_config_url = idp_order_[0]; |
| auto get_info_it = token_request_get_infos_.find(idp_config_url); |
| CHECK(get_info_it != token_request_get_infos_.end()); |
| request_dialog_controller_->ShowLoadingDialog( |
| GetTopFrameOriginForDisplay(GetEmbeddingOrigin()), |
| FormatOriginForDisplay(url::Origin::Create(idp_config_url)), |
| get_info_it->second.rp_context, rp_mode_, |
| base::BindOnce(&FederatedAuthRequestImpl::OnDialogDismissed, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| CHECK(!unique_idps.empty()); |
| FetchEndpointsForIdps(std::move(unique_idps), /*for_idp_signin=*/false); |
| } |
| |
| void FederatedAuthRequestImpl::RequestUserInfo( |
| blink::mojom::IdentityProviderConfigPtr provider, |
| RequestUserInfoCallback callback) { |
| if (!render_frame_host().GetPage().IsPrimary()) { |
| ReportBadMessageAndDeleteThis( |
| "FedCM should not be allowed in nested frame trees."); |
| return; |
| } |
| // FedCMMetrics class is currently not used for UserInfo API. If we log UKM |
| // metrics later on, we should call MaybeCreateFedCmMetrics() here. |
| |
| auto network_manager = IdpNetworkRequestManager::Create( |
| static_cast<RenderFrameHostImpl*>(&render_frame_host())); |
| auto user_info_request = FederatedAuthUserInfoRequest::Create( |
| std::move(network_manager), permission_delegate_, |
| api_permission_delegate_, &render_frame_host(), std::move(provider)); |
| FederatedAuthUserInfoRequest* user_info_request_ptr = user_info_request.get(); |
| user_info_requests_.insert(std::move(user_info_request)); |
| |
| user_info_request_ptr->SetCallbackAndStart( |
| base::BindOnce(&FederatedAuthRequestImpl::CompleteUserInfoRequest, |
| weak_ptr_factory_.GetWeakPtr(), user_info_request_ptr, |
| std::move(callback))); |
| } |
| |
| void FederatedAuthRequestImpl::CancelTokenRequest() { |
| if (!auth_request_token_callback_) { |
| // TODO(crbug.com/40940748): this should only happen with a compromised |
| // renderer process but for some reason that is not the case. We should |
| // investigate what could go wrong about the abort controller. |
| return; |
| } |
| |
| // Dialog will be hidden by the destructor for request_dialog_controller_, |
| // triggered by CompleteRequest. |
| |
| CompleteRequestWithError(FederatedAuthRequestResult::kErrorCanceled, |
| TokenStatus::kAborted, |
| /*token_error=*/std::nullopt, |
| /*should_delay_callback=*/false); |
| } |
| |
| void FederatedAuthRequestImpl::ResolveTokenRequest( |
| const std::optional<std::string>& account_id, |
| const std::string& token, |
| ResolveTokenRequestCallback callback) { |
| if (!webid::IsFedCmAuthzEnabled(render_frame_host(), origin())) { |
| std::move(callback).Run(false); |
| return; |
| } |
| |
| // TODO(crbug.com/40273061): notify Android UI about token request being |
| // resolved. |
| if (!identity_registry_) { |
| std::move(callback).Run(false); |
| return; |
| } |
| |
| bool accepted = |
| identity_registry_->NotifyResolve(origin(), account_id, token); |
| std::move(callback).Run(accepted); |
| } |
| |
| void FederatedAuthRequestImpl::SetIdpSigninStatus( |
| const url::Origin& idp_origin, |
| blink::mojom::IdpSigninStatus status) { |
| if (render_frame_host().IsNestedWithinFencedFrame()) { |
| RecordSetLoginStatusIgnoredReason( |
| FedCmSetLoginStatusIgnoredReason::kInFencedFrame); |
| return; |
| } |
| // We only allow setting the IDP signin status when the subresource is loaded |
| // from the same site as the document, and the document is same site with |
| // all ancestors. This is to protect from an RP embedding a tracker resource |
| // that would set this signin status for the tracker, enabling the FedCM |
| // request. |
| if (!webid::IsSameSiteWithAncestors(idp_origin, &render_frame_host())) { |
| RecordSetLoginStatusIgnoredReason( |
| FedCmSetLoginStatusIgnoredReason::kCrossOrigin); |
| return; |
| } |
| permission_delegate_->SetIdpSigninStatus( |
| idp_origin, status == blink::mojom::IdpSigninStatus::kSignedIn); |
| } |
| |
| void FederatedAuthRequestImpl::RegisterIdP(const GURL& idp, |
| RegisterIdPCallback callback) { |
| if (!IsFedCmIdPRegistrationEnabled()) { |
| std::move(callback).Run(false); |
| return; |
| } |
| |
| if (!origin().IsSameOriginWith(url::Origin::Create(idp))) { |
| std::move(callback).Run(false); |
| return; |
| } |
| |
| if (!render_frame_host().HasTransientUserActivation()) { |
| std::move(callback).Run(false); |
| return; |
| } |
| |
| if (!request_dialog_controller_) { |
| request_dialog_controller_ = CreateDialogController(); |
| } |
| |
| request_dialog_controller_->RequestIdPRegistrationPermision( |
| origin(), |
| base::BindOnce(&FederatedAuthRequestImpl::OnRegisterIdPPermissionResponse, |
| weak_ptr_factory_.GetWeakPtr(), std::move(callback), idp)); |
| } |
| |
| void FederatedAuthRequestImpl::OnRegisterIdPPermissionResponse( |
| RegisterIdPCallback callback, |
| const GURL& idp, |
| bool accepted) { |
| if (accepted) { |
| permission_delegate_->RegisterIdP(idp); |
| } |
| std::move(callback).Run(accepted); |
| } |
| |
| void FederatedAuthRequestImpl::UnregisterIdP(const GURL& idp, |
| UnregisterIdPCallback callback) { |
| if (!IsFedCmIdPRegistrationEnabled()) { |
| std::move(callback).Run(false); |
| return; |
| } |
| if (!origin().IsSameOriginWith(url::Origin::Create(idp))) { |
| std::move(callback).Run(false); |
| return; |
| } |
| permission_delegate_->UnregisterIdP(idp); |
| std::move(callback).Run(true); |
| } |
| |
| void FederatedAuthRequestImpl::OnIdpSigninStatusReceived( |
| const url::Origin& idp_config_origin, |
| bool idp_signin_status) { |
| if (!idp_signin_status) { |
| return; |
| } |
| |
| // Since the user has gone through the IDP login flow with this IDP, the next |
| // accounts dialog will only include this IDP. |
| idp_order_.clear(); |
| for (const auto& [get_idp_config_url, get_info] : token_request_get_infos_) { |
| if (url::Origin::Create(get_idp_config_url) == idp_config_origin) { |
| permission_delegate_->RemoveIdpSigninStatusObserver(this); |
| idp_order_.push_back(get_idp_config_url); |
| FetchEndpointsForIdps({get_idp_config_url}, /*for_idp_signin=*/true); |
| break; |
| } |
| } |
| } |
| |
| bool FederatedAuthRequestImpl::HasPendingRequest() const { |
| bool has_pending_request = |
| webid::GetPageData(&render_frame_host())->PendingWebIdentityRequest() != |
| nullptr; |
| DCHECK(has_pending_request || !auth_request_token_callback_); |
| return has_pending_request; |
| } |
| |
| void FederatedAuthRequestImpl::FetchEndpointsForIdps( |
| const std::set<GURL>& idp_config_urls, |
| bool for_idp_signin) { |
| int icon_ideal_size = request_dialog_controller_->GetBrandIconIdealSize(); |
| int icon_minimum_size = request_dialog_controller_->GetBrandIconMinimumSize(); |
| |
| { |
| std::set<GURL> pending_idps = std::move(fetch_data_.pending_idps); |
| pending_idps.insert(idp_config_urls.begin(), idp_config_urls.end()); |
| fetch_data_ = FetchData(); |
| fetch_data_.pending_idps = std::move(pending_idps); |
| fetch_data_.for_idp_signin = for_idp_signin; |
| } |
| |
| provider_fetcher_ = std::make_unique<FederatedProviderFetcher>( |
| render_frame_host(), network_manager_.get()); |
| provider_fetcher_->Start( |
| fetch_data_.pending_idps, rp_mode_, icon_ideal_size, icon_minimum_size, |
| base::BindOnce(&FederatedAuthRequestImpl::OnAllConfigAndWellKnownFetched, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void FederatedAuthRequestImpl::OnAllConfigAndWellKnownFetched( |
| std::vector<FederatedProviderFetcher::FetchResult> fetch_results) { |
| provider_fetcher_.reset(); |
| |
| well_known_and_config_fetched_time_ = base::TimeTicks::Now(); |
| fedcm_metrics_->RecordWellKnownAndConfigFetchTime( |
| well_known_and_config_fetched_time_ - start_time_); |
| |
| for (const FederatedProviderFetcher::FetchResult& fetch_result : |
| fetch_results) { |
| const GURL& identity_provider_config_url = |
| fetch_result.identity_provider_config_url; |
| auto get_info_it = |
| token_request_get_infos_.find(identity_provider_config_url); |
| CHECK(get_info_it != token_request_get_infos_.end()); |
| |
| metrics_endpoints_[identity_provider_config_url] = |
| fetch_result.endpoints.metrics; |
| |
| std::unique_ptr<IdentityProviderInfo> idp_info = |
| std::make_unique<IdentityProviderInfo>( |
| get_info_it->second.provider, std::move(fetch_result.endpoints), |
| fetch_result.metadata ? std::move(*fetch_result.metadata) |
| : IdentityProviderMetadata(), |
| get_info_it->second.rp_context, get_info_it->second.rp_mode); |
| |
| if (fetch_result.error) { |
| const FederatedProviderFetcher::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); |
| } |
| OnFetchDataForIdpFailed( |
| std::move(idp_info), fetch_error.result, fetch_error.token_status, |
| /*should_delay_callback=*/rp_mode_ == RpMode::kWidget); |
| continue; |
| } |
| // The login url should be valid unless IdP login status API is disabled. |
| if (idp_info->metadata.idp_login_url.is_valid()) { |
| idp_login_infos_[idp_info->metadata.idp_login_url] = { |
| idp_info->provider->login_hint, idp_info->provider->domain_hint}; |
| } |
| |
| // Make sure that we don't fetch accounts if the IDP sign-in bit is reset to |
| // false during the API call. e.g. by the login/logout HEADER. |
| // In the button flow we get here even if the IDP sign-in bit was false |
| // originally, because we need the well-known and config files to find the |
| // login URL. |
| idp_info->has_failing_idp_signin_status = |
| webid::ShouldFailAccountsEndpointRequestBecauseNotSignedInWithIdp( |
| render_frame_host(), identity_provider_config_url, |
| permission_delegate_); |
| if (idp_info->has_failing_idp_signin_status && |
| webid::GetIdpSigninStatusMode( |
| render_frame_host(), |
| url::Origin::Create(identity_provider_config_url)) == |
| FedCmIdpSigninStatusMode::ENABLED) { |
| // If the user is logged out and we are in a button-mode, allow the user |
| // to sign-in to the IdP and return early. |
| if (rp_mode_ == blink::mojom::RpMode::kButton) { |
| MaybeShowButtonModeModalDialog(identity_provider_config_url, |
| idp_info->metadata.idp_login_url); |
| return; |
| } |
| // Do not send metrics for IDP where the user is not signed-in in order |
| // to prevent IDP from using the user IP to make a probabilistic model |
| // of which websites a user visits. |
| idp_info->endpoints.metrics = GURL(); |
| |
| OnFetchDataForIdpFailed( |
| std::move(idp_info), |
| FederatedAuthRequestResult::kErrorNotSignedInWithIdp, |
| TokenStatus::kNotSignedInWithIdp, |
| /*should_delay_callback=*/true); |
| continue; |
| } |
| |
| GURL accounts_endpoint = idp_info->endpoints.accounts; |
| std::string client_id = idp_info->provider->config->client_id; |
| const GURL& config_url = idp_info->provider->config->config_url; |
| network_manager_->SendAccountsRequest( |
| accounts_endpoint, client_id, |
| base::BindOnce(&FederatedAuthRequestImpl::OnAccountsResponseReceived, |
| weak_ptr_factory_.GetWeakPtr(), std::move(idp_info))); |
| fedcm_metrics_->RecordAccountsRequestSent(config_url); |
| } |
| } |
| |
| void FederatedAuthRequestImpl::CompleteDisconnectRequest( |
| DisconnectCallback callback, |
| blink::mojom::DisconnectStatus status) { |
| // `disconnect_request_` may be null here if the completion is invoked from |
| // the FederatedAuthRequestImpl destructor, which destroys |
| // `disconnect_request_`. The FederatedAuthDisconnectRequest destructor would |
| // trigger the callback. |
| if (!disconnect_request_ && |
| status == blink::mojom::DisconnectStatus::kSuccess) { |
| NOTREACHED() << "The successful disconnect request is nowhere to be found"; |
| return; |
| } |
| std::move(callback).Run(status); |
| disconnect_request_.reset(); |
| } |
| |
| void FederatedAuthRequestImpl::OnClientMetadataResponseReceived( |
| std::unique_ptr<IdentityProviderInfo> idp_info, |
| const IdpNetworkRequestManager::AccountList& accounts, |
| IdpNetworkRequestManager::FetchStatus status, |
| IdpNetworkRequestManager::ClientMetadata client_metadata) { |
| client_metadata_fetched_time_ = base::TimeTicks::Now(); |
| |
| // TODO(yigu): Clean up the client metadata related errors for metrics and |
| // console logs. |
| if (!idp_info->metadata.brand_background_color && |
| idp_info->metadata.brand_text_color) { |
| idp_info->metadata.brand_text_color = std::nullopt; |
| render_frame_host().AddMessageToConsole( |
| blink::mojom::ConsoleMessageLevel::kWarning, |
| "The FedCM text color is ignored because background color was not " |
| "provided"); |
| } |
| if (idp_info->metadata.brand_background_color && |
| idp_info->metadata.brand_text_color) { |
| float text_contrast_ratio = color_utils::GetContrastRatio( |
| *idp_info->metadata.brand_background_color, |
| *idp_info->metadata.brand_text_color); |
| if (text_contrast_ratio < color_utils::kMinimumReadableContrastRatio) { |
| idp_info->metadata.brand_text_color = std::nullopt; |
| render_frame_host().AddMessageToConsole( |
| blink::mojom::ConsoleMessageLevel::kWarning, |
| "The FedCM text color is ignored because it does not contrast enough " |
| "with the provided background color"); |
| } |
| } |
| OnFetchDataForIdpSucceeded(std::move(idp_info), accounts, client_metadata); |
| } |
| |
| bool HasScope(const std::vector<std::string>& scope, std::string name) { |
| auto it = std::find(std::begin(scope), std::end(scope), name); |
| if (it == std::end(scope)) { |
| return false; |
| } |
| return true; |
| } |
| |
| bool FederatedAuthRequestImpl::ShouldMediateAuthzFor( |
| const blink::mojom::IdentityProviderRequestOptions& provider) { |
| url::Origin idp_origin = url::Origin::Create(provider.config->config_url); |
| if (!webid::IsFedCmAuthzEnabled(render_frame_host(), idp_origin)) { |
| return true; |
| } |
| |
| const auto& scope = provider.scope; |
| if (scope.size() == 0) { |
| // If "scope" is not passed, defaults the parameter to |
| // ["sub", "name", "email" and "picture"]. |
| return true; |
| } |
| |
| if (scope.size() == 2) { |
| return HasScope(scope, "profile") && HasScope(scope, "email"); |
| } |
| |
| return false; |
| } |
| |
| bool FederatedAuthRequestImpl::CanShowContinueOnPopup() const { |
| if (mediation_requirement_ == MediationRequirement::kSilent) { |
| return false; |
| } |
| |
| if (mediation_requirement_ == MediationRequirement::kRequired) { |
| // In this case, we always have a user gesture (the user had to choose |
| // an account), so we can show a popup. |
| return true; |
| } |
| |
| if (identity_selection_type_ == kExplicit || |
| identity_selection_type_ == kAutoButton) { |
| return true; |
| } |
| DCHECK_EQ(identity_selection_type_, kAutoWidget); |
| return had_transient_user_activation_; |
| } |
| |
| void FederatedAuthRequestImpl::OnFetchDataForIdpSucceeded( |
| std::unique_ptr<IdentityProviderInfo> idp_info, |
| const IdpNetworkRequestManager::AccountList& accounts, |
| const IdpNetworkRequestManager::ClientMetadata& client_metadata) { |
| fetch_data_.did_succeed_for_at_least_one_idp = true; |
| |
| const GURL& idp_config_url = idp_info->provider->config->config_url; |
| |
| bool request_permission = ShouldMediateAuthzFor(*idp_info->provider); |
| |
| const std::string idp_for_display = |
| webid::FormatUrlForDisplay(idp_config_url); |
| idp_info->data = IdentityProviderData( |
| idp_for_display, accounts, idp_info->metadata, |
| ClientMetadata{client_metadata.terms_of_service_url, |
| client_metadata.privacy_policy_url, |
| client_metadata.brand_icon_url}, |
| idp_info->rp_context, /*request_permission=*/request_permission, |
| /*has_login_status_mismatch=*/false); |
| idp_infos_[idp_config_url] = std::move(idp_info); |
| |
| fetch_data_.pending_idps.erase(idp_config_url); |
| MaybeShowAccountsDialog(); |
| } |
| |
| void FederatedAuthRequestImpl::OnFetchDataForIdpFailed( |
| const std::unique_ptr<IdentityProviderInfo> idp_info, |
| blink::mojom::FederatedAuthRequestResult result, |
| std::optional<TokenStatus> token_status, |
| bool should_delay_callback) { |
| const GURL& idp_config_url = idp_info->provider->config->config_url; |
| fetch_data_.pending_idps.erase(idp_config_url); |
| |
| if (fetch_data_.pending_idps.empty() && |
| !fetch_data_.did_succeed_for_at_least_one_idp) { |
| CompleteRequestWithError(result, token_status, |
| /*token_error=*/std::nullopt, |
| should_delay_callback); |
| return; |
| } |
| |
| AddDevToolsIssue(result); |
| AddConsoleErrorMessage(result); |
| |
| if (IsFedCmMetricsEndpointEnabled()) { |
| SendFailedTokenRequestMetrics(idp_info->endpoints.metrics, result); |
| } |
| |
| metrics_endpoints_.erase(idp_config_url); |
| |
| // We do not call both OnFetchDataForIdpFailed() after OnFetchDataSucceeded() |
| // for the same IDP. |
| DCHECK(idp_infos_.find(idp_config_url) == idp_infos_.end()); |
| MaybeShowAccountsDialog(); |
| } |
| |
| void FederatedAuthRequestImpl::MaybeShowAccountsDialog() { |
| if (!fetch_data_.pending_idps.empty()) { |
| return; |
| } |
| |
| // The accounts fetch could be delayed for legitimate reasons. A user may be |
| // able to disable FedCM API (e.g. via settings or dismissing another FedCM UI |
| // on the same RP origin) before the browser receives the accounts response. |
| // We should exit early without showing any UI. |
| // Note that for the button flow is not affected by the permission status. |
| if (GetApiPermissionStatus() != FederatedApiPermissionStatus::GRANTED && |
| rp_mode_ != RpMode::kButton) { |
| CompleteRequestWithError( |
| FederatedAuthRequestResult::kErrorDisabledInSettings, |
| TokenStatus::kDisabledInSettings, |
| /*token_error=*/std::nullopt, |
| /*should_delay_callback=*/true); |
| return; |
| } |
| |
| // This map may have contents already if we came here through the "Add |
| // Account" flow or the IDP login mismatch in multiple IDP case. |
| idp_data_for_display_.clear(); |
| |
| // If this method call occurs after a login, we'd like to show the account |
| // that was logged in. |
| std::optional<IdentityProviderData> new_account_idp; |
| for (const auto& idp : idp_order_) { |
| auto idp_info_it = idp_infos_.find(idp); |
| if (idp_info_it != idp_infos_.end() && idp_info_it->second->data) { |
| idp_data_for_display_.push_back(*idp_info_it->second->data); |
| } |
| if (idp_infos_.size() > 1u || |
| IsFedCmUseOtherAccountEnabled(rp_mode_ == RpMode::kButton)) { |
| if (!login_url_.is_empty() && |
| login_url_ == idp_info_it->second->metadata.idp_login_url) { |
| for (const auto& account : idp_info_it->second->data->accounts) { |
| if (!account_ids_before_login_.contains(account.id)) { |
| // Even though it is theoretically possible for more than one |
| // account to be new, just show the first one we encounter. |
| new_account_idp = idp_info_it->second->data; |
| new_account_idp->accounts = {account}; |
| break; |
| } |
| } |
| account_ids_before_login_.clear(); |
| } |
| } |
| } |
| |
| // We want to show IDPs in the following order in the UI: |
| // 1. IDPs for which there was a mismatch. |
| // 2. IDPs for which there were returning accounts. |
| // 3. IDPs for which there weren't returning accounts. |
| base::ranges::stable_sort(idp_data_for_display_, [](const auto& idp1, |
| const auto& idp2) { |
| if (idp1.has_login_status_mismatch != idp2.has_login_status_mismatch) { |
| // The IDP with mismatch should go first. |
| return idp1.has_login_status_mismatch > idp2.has_login_status_mismatch; |
| } |
| LoginState state1 = idp1.accounts.empty() ? LoginState::kSignIn |
| : *idp1.accounts[0].login_state; |
| LoginState state2 = idp2.accounts.empty() ? LoginState::kSignIn |
| : *idp2.accounts[0].login_state; |
| // LoginState::kSignIn should go first. |
| return state1 < state2; |
| }); |
| |
| // TODO(crbug.com/40246099): Handle auto_reauthn_ for multi IDP. |
| bool auto_reauthn_enabled = |
| mediation_requirement_ != MediationRequirement::kRequired; |
| |
| dialog_type_ = auto_reauthn_enabled ? kAutoReauth : kSelectAccount; |
| bool is_auto_reauthn_setting_enabled = false; |
| bool is_auto_reauthn_embargoed = false; |
| std::optional<base::TimeDelta> time_from_embargo; |
| bool requires_user_mediation = false; |
| const IdentityProviderData* auto_reauthn_idp = nullptr; |
| const IdentityRequestAccount* auto_reauthn_account = nullptr; |
| bool has_single_returning_account = false; |
| if (auto_reauthn_enabled) { |
| is_auto_reauthn_setting_enabled = |
| auto_reauthn_permission_delegate_->IsAutoReauthnSettingEnabled(); |
| is_auto_reauthn_embargoed = |
| auto_reauthn_permission_delegate_->IsAutoReauthnEmbargoed( |
| GetEmbeddingOrigin()); |
| if (is_auto_reauthn_embargoed) { |
| time_from_embargo = |
| base::Time::Now() - |
| auto_reauthn_permission_delegate_->GetAutoReauthnEmbargoStartTime( |
| GetEmbeddingOrigin()); |
| |
| // See `kFederatedIdentityAutoReauthnEmbargoDuration`. |
| render_frame_host().AddMessageToConsole( |
| blink::mojom::ConsoleMessageLevel::kInfo, |
| "Auto re-authn was previously triggered less than 10 minutes ago. " |
| "Only one auto re-authn request can be made every 10 minutes."); |
| } |
| requires_user_mediation = RequiresUserMediation(); |
| // Auto signs in returning users if they have a single returning account and |
| // are signing in. |
| has_single_returning_account = |
| GetAccountForAutoReauthn(&auto_reauthn_idp, &auto_reauthn_account); |
| if (dialog_type_ == kAutoReauth && |
| (requires_user_mediation || !is_auto_reauthn_setting_enabled || |
| is_auto_reauthn_embargoed || !has_single_returning_account)) { |
| dialog_type_ = kSelectAccount; |
| } |
| if (!has_single_returning_account && |
| mediation_requirement_ == MediationRequirement::kSilent) { |
| fedcm_metrics_->RecordAutoReauthnMetrics( |
| has_single_returning_account, auto_reauthn_account, |
| dialog_type_ == kAutoReauth, !is_auto_reauthn_setting_enabled, |
| is_auto_reauthn_embargoed, time_from_embargo, |
| requires_user_mediation); |
| |
| // By this moment we know that the user has granted permission in the past |
| // for the RP/IdP. Because otherwise we have returned already in |
| // `ShouldFailBeforeFetchingAccounts`. It means that we can do the |
| // following without privacy cost: |
| // 1. Reject the promise immediately without delay |
| // 2. Not to show any UI to respect `mediation: silent` |
| // TODO(crbug.com/40266561): validate the statement above with |
| // stakeholders |
| render_frame_host().AddMessageToConsole( |
| blink::mojom::ConsoleMessageLevel::kError, |
| "Silent mediation issue: the user has used FedCM with multiple " |
| "accounts on this site."); |
| CompleteRequestWithError( |
| FederatedAuthRequestResult::kErrorSilentMediationFailure, |
| TokenStatus::kSilentMediationFailure, |
| /*token_error=*/std::nullopt, |
| /*should_delay_callback=*/false); |
| return; |
| } |
| |
| if (dialog_type_ == kAutoReauth) { |
| IdentityRequestAccount account{*auto_reauthn_account}; |
| IdentityProviderData idp{*auto_reauthn_idp}; |
| idp.accounts = {account}; |
| idp_data_for_display_ = {idp}; |
| } |
| } |
| |
| if (dialog_type_ != kAutoReauth) { |
| identity_selection_type_ = kExplicit; |
| } else if (!IsFedCmButtonModeEnabled() || |
| rp_mode_ == blink::mojom::RpMode::kWidget) { |
| identity_selection_type_ = kAutoWidget; |
| } else { |
| identity_selection_type_ = kAutoButton; |
| } |
| |
| if (auto_reauthn_enabled) { |
| fedcm_metrics_->RecordAutoReauthnMetrics( |
| has_single_returning_account, auto_reauthn_account, |
| dialog_type_ == kAutoReauth, !is_auto_reauthn_setting_enabled, |
| is_auto_reauthn_embargoed, time_from_embargo, requires_user_mediation); |
| } |
| |
| if (identity_selection_type_ == kAutoButton) { |
| OnAccountSelected(auto_reauthn_idp->idp_metadata.config_url, |
| auto_reauthn_account->id, /*is_sign_in=*/true); |
| return; |
| } |
| |
| // The RenderFrameHost may be alive but not visible in the following |
| // situations: |
| // Situation #1: User switched tabs |
| // Situation #2: User navigated the page with bfcache |
| // |
| // - If this fetch is as a result of an IdP sign-in status change, the FedCM |
| // dialog is either visible or temporarily hidden. Update the contents of |
| // the dialog. |
| // - If the FedCM dialog has not already been shown, do not show the dialog |
| // if the RenderFrameHost is hidden because the user does not seem interested |
| // in the contents of the current page. |
| if (!fetch_data_.for_idp_signin) { |
| bool is_active = IsFrameActive(render_frame_host().GetMainFrame()); |
| fedcm_metrics_->RecordWebContentsStatusUponReadyToShowDialog( |
| IsFrameVisible(render_frame_host().GetMainFrame()), is_active); |
| |
| if (!is_active) { |
| CompleteRequestWithError( |
| FederatedAuthRequestResult::kErrorRpPageNotVisible, |
| TokenStatus::kRpPageNotVisible, |
| /*token_error=*/std::nullopt, |
| /*should_delay_callback=*/true); |
| return; |
| } |
| |
| ready_to_display_accounts_dialog_time_ = base::TimeTicks::Now(); |
| fedcm_metrics_->RecordShowAccountsDialogTime( |
| idp_data_for_display_, |
| ready_to_display_accounts_dialog_time_ - start_time_); |
| |
| fedcm_metrics_->RecordShowAccountsDialogTimeBreakdown( |
| well_known_and_config_fetched_time_ - start_time_, |
| accounts_fetched_time_ - well_known_and_config_fetched_time_, |
| client_metadata_fetched_time_ != base::TimeTicks() |
| ? client_metadata_fetched_time_ - accounts_fetched_time_ |
| : base::TimeDelta()); |
| } |
| bool did_succeed_for_at_least_one_idp = |
| fetch_data_.did_succeed_for_at_least_one_idp; |
| |
| fetch_data_ = FetchData(); |
| |
| // RenderFrameHost should be in the primary page (ex not in the BFCache). |
| DCHECK(render_frame_host().GetPage().IsPrimary()); |
| |
| bool intercept = false; |
| // In tests (content_shell or when --use-fake-ui-for-fedcm is used), the |
| // dialog controller will immediately select an account. But if browser |
| // automation is enabled, we don't want that to happen because automation |
| // should be able to choose which account to select or to cancel. |
| // So we use this call to see whether interception is enabled. |
| // It is not needed in regular Chrome even when automation is used because |
| // there, the dialog will wait for user input anyway. |
| devtools_instrumentation::WillShowFedCmDialog(render_frame_host(), |
| &intercept); |
| // Since we don't reuse the controller for each request, and intercept |
| // defaults to false, we only need to call this if intercept is true. |
| if (intercept) { |
| request_dialog_controller_->SetIsInterceptionEnabled(intercept); |
| } |
| |
| std::optional<std::string> iframe_for_display = GetIframeOriginForDisplay( |
| GetEmbeddingOrigin(), origin(), |
| base::BindOnce(&FederatedAuthRequestImpl::CompleteRequestWithError, |
| weak_ptr_factory_.GetWeakPtr())); |
| |
| // TODO(crbug.com/40245853): Handle UI where some IDPs are successful and some |
| // IDPs are failing in the multi IDP case. |
| // Note that ShowAccountsDialog() may result in the request being completed |
| // immediately (for instance on Android when we cannot create a BottomSheet), |
| // so invocations after this method should assume that the members may have |
| // been cleaned up. |
| // TODO(crbug.com/329261790): Make ShowAccountsDialog() return a boolean and |
| // use that to know when to bail out early from this method. |
| request_dialog_controller_->ShowAccountsDialog( |
| GetTopFrameOriginForDisplay(GetEmbeddingOrigin()), iframe_for_display, |
| idp_data_for_display_, |
| identity_selection_type_ == kExplicit ? SignInMode::kExplicit |
| : SignInMode::kAuto, |
| rp_mode_, new_account_idp, |
| base::BindOnce(&FederatedAuthRequestImpl::OnAccountSelected, |
| weak_ptr_factory_.GetWeakPtr()), |
| base::BindRepeating(&FederatedAuthRequestImpl::LoginToIdP, |
| weak_ptr_factory_.GetWeakPtr(), |
| /*can_append_hints=*/false), |
| base::BindOnce(&FederatedAuthRequestImpl::OnDialogDismissed, |
| weak_ptr_factory_.GetWeakPtr()), |
| base::BindOnce(&FederatedAuthRequestImpl::OnAccountsDisplayed, |
| weak_ptr_factory_.GetWeakPtr())); |
| devtools_instrumentation::DidShowFedCmDialog(render_frame_host()); |
| |
| if (identity_selection_type_ == kExplicit && |
| did_succeed_for_at_least_one_idp) { |
| // We omit recording the accounts dialog shown metric for auto re-authn |
| // because the metric is used to detect IDPs flashing UI. Auto re-authn |
| // verifying UI cannot be flashed since it is destroyed automatically after |
| // 3 seconds and cannot be destroyed earlier for a11y reasons. |
| accounts_dialog_shown_time_ = base::TimeTicks::Now(); |
| } |
| |
| // Note that accounts dialog shown after mismatch dialog is also recorded. |
| // Although not useful for catching malicious IDPs, it should only be a very |
| // small percentage of the samples recorded. |
| fedcm_metrics_->RecordAccountsDialogShown(idp_data_for_display_); |
| } |
| |
| void FederatedAuthRequestImpl::OnAccountsDisplayed() { |
| accounts_dialog_display_time_ = base::TimeTicks::Now(); |
| } |
| |
| void FederatedAuthRequestImpl::HandleAccountsFetchFailure( |
| std::unique_ptr<IdentityProviderInfo> idp_info, |
| std::optional<bool> old_idp_signin_status, |
| blink::mojom::FederatedAuthRequestResult result, |
| std::optional<TokenStatus> token_status) { |
| url::Origin idp_origin = |
| url::Origin::Create(idp_info->provider->config->config_url); |
| FedCmIdpSigninStatusMode signin_status_mode = |
| webid::GetIdpSigninStatusMode(render_frame_host(), idp_origin); |
| if (!old_idp_signin_status.has_value() || |
| signin_status_mode == FedCmIdpSigninStatusMode::METRICS_ONLY) { |
| if (rp_mode_ == blink::mojom::RpMode::kButton) { |
| MaybeShowButtonModeModalDialog(idp_info->provider->config->config_url, |
| idp_info->metadata.idp_login_url); |
| return; |
| } |
| OnFetchDataForIdpFailed(std::move(idp_info), result, token_status, |
| /*should_delay_callback=*/true); |
| return; |
| } |
| |
| if (!IsFrameActive(render_frame_host().GetMainFrame())) { |
| CompleteRequestWithError(FederatedAuthRequestResult::kErrorRpPageNotVisible, |
| TokenStatus::kRpPageNotVisible, |
| /*token_error=*/std::nullopt, |
| /*should_delay_callback=*/true); |
| return; |
| } |
| |
| if (mediation_requirement_ == MediationRequirement::kSilent) { |
| // By this moment we know that the user has granted permission in the past |
| // for the RP/IdP. Because otherwise we have returned already in |
| // `ShouldFailBeforeFetchingAccounts`. It means that we can do the |
| // following without privacy cost: |
| // 1. Reject the promise immediately without delay |
| // 2. Not to show any UI to respect `mediation: silent` |
| // TODO(crbug.com/40266561): validate the statement above with stakeholders |
| OnFetchDataForIdpFailed( |
| std::move(idp_info), |
| FederatedAuthRequestResult::kErrorSilentMediationFailure, |
| TokenStatus::kSilentMediationFailure, |
| /*should_delay_callback=*/false); |
| return; |
| } |
| |
| OnIdpMismatch(std::move(idp_info)); |
| } |
| |
| void FederatedAuthRequestImpl::OnIdpMismatch( |
| std::unique_ptr<IdentityProviderInfo> idp_info) { |
| const GURL& idp_config_url = idp_info->provider->config->config_url; |
| fetch_data_.pending_idps.erase(idp_config_url); |
| |
| const std::string idp_for_display = |
| webid::FormatUrlForDisplay(idp_config_url); |
| idp_info->data = IdentityProviderData( |
| idp_for_display, std::vector<IdentityRequestAccount>(), |
| idp_info->metadata, ClientMetadata{GURL(), GURL(), GURL()}, |
| idp_info->rp_context, |
| /*request_permission=*/ShouldMediateAuthzFor(*idp_info->provider), |
| /*has_login_status_mismatch=*/true); |
| idp_infos_[idp_config_url] = std::move(idp_info); |
| |
| if (!fetch_data_.pending_idps.empty()) { |
| return; |
| } |
| |
| // Invoke the accounts dialog flow if there is at least one account or more |
| // than one IDP for which we should show the mismatch dialog. |
| // TODO(crbug.com/331426009): make this code clearer by creating a separate |
| // method for showing multiple mismatch UI. |
| if (fetch_data_.did_succeed_for_at_least_one_idp || idp_infos_.size() > 1u) { |
| MaybeShowAccountsDialog(); |
| // If there are no successful IDPs, this is the multi IDP case where all are |
| // mismatch. |
| if (!fetch_data_.did_succeed_for_at_least_one_idp) { |
| mismatch_dialog_shown_time_ = base::TimeTicks::Now(); |
| has_shown_mismatch_ = true; |
| devtools_instrumentation::DidShowFedCmDialog(render_frame_host()); |
| } |
| return; |
| } |
| |
| if (rp_mode_ == RpMode::kButton) { |
| MaybeShowButtonModeModalDialog( |
| idp_config_url, idp_infos_[idp_config_url]->metadata.idp_login_url); |
| return; |
| } |
| |
| ShowSingleIdpFailureDialog(); |
| } |
| |
| void FederatedAuthRequestImpl::ShowSingleIdpFailureDialog() { |
| CHECK_EQ(idp_infos_.size(), 1u); |
| IdentityProviderInfo* idp_info = idp_infos_.begin()->second.get(); |
| url::Origin idp_origin = |
| url::Origin::Create(idp_info->provider->config->config_url); |
| // RenderFrameHost should be in the primary page (ex not in the BFCache). |
| DCHECK(render_frame_host().GetPage().IsPrimary()); |
| |
| fetch_data_ = FetchData(); |
| |
| // Set `idp_data_for_display_` so it is always the case that we can rely on it |
| // to know which IDPs have been seen in the UI. |
| CHECK(idp_info->data.has_value()); |
| idp_data_for_display_ = {*idp_info->data}; |
| |
| std::optional<std::string> iframe_for_display = GetIframeOriginForDisplay( |
| GetEmbeddingOrigin(), origin(), |
| base::BindOnce(&FederatedAuthRequestImpl::CompleteRequestWithError, |
| weak_ptr_factory_.GetWeakPtr())); |
| |
| // If IdP login status mismatch dialog is already visible, calling |
| // ShowFailureDialog() a 2nd time should notify the user that login |
| // failed. |
| dialog_type_ = kConfirmIdpLogin; |
| config_url_ = idp_info->provider->config->config_url; |
| login_url_ = idp_info->metadata.idp_login_url; |
| |
| // Store variables used in RecordMismatchDialogShown since they may be cleaned |
| // up in ShowFailureDialog(). |
| bool has_shown_mismatch = has_shown_mismatch_; |
| bool has_hints = !idp_info->provider->login_hint.empty() || |
| !idp_info->provider->domain_hint.empty() || |
| !idp_info->metadata.requested_label.empty(); |
| // TODO(crbug.com/329261790): Make ShowFailureDialog() return boolean and use |
| // the value to know when to bail out of this method. |
| request_dialog_controller_->ShowFailureDialog( |
| GetTopFrameOriginForDisplay(GetEmbeddingOrigin()), iframe_for_display, |
| FormatOriginForDisplay(idp_origin), idp_info->rp_context, rp_mode_, |
| idp_info->metadata, |
| base::BindOnce(&FederatedAuthRequestImpl::OnDismissFailureDialog, |
| weak_ptr_factory_.GetWeakPtr()), |
| base::BindRepeating(&FederatedAuthRequestImpl::LoginToIdP, |
| weak_ptr_factory_.GetWeakPtr(), |
| /*can_append_hints=*/true)); |
| |
| // ShowFailureDialog() may have completed the request synchronously, in which |
| // case we did not really show any failure dialog. |
| CHECK(idp_data_for_display_.size() == 1u); |
| fedcm_metrics_->RecordSingleIdpMismatchDialogShown( |
| idp_data_for_display_[0], has_shown_mismatch, has_hints); |
| mismatch_dialog_shown_time_ = base::TimeTicks::Now(); |
| has_shown_mismatch_ = true; |
| devtools_instrumentation::DidShowFedCmDialog(render_frame_host()); |
| } |
| |
| void FederatedAuthRequestImpl::CloseModalDialogView() { |
| #if BUILDFLAG(IS_ANDROID) |
| // On android, invoke OnClose on the modal dialog, as the UI code needs to |
| // then notify the opener. |
| OnClose(); |
| #else |
| // On desktop, invoke NotifyClose on the opener. |
| if (identity_registry_) { |
| identity_registry_->NotifyClose(origin()); |
| } |
| #endif // BUILDFLAG(IS_ANDROID) |
| } |
| |
| void FederatedAuthRequestImpl::OnAccountsResponseReceived( |
| std::unique_ptr<IdentityProviderInfo> idp_info, |
| IdpNetworkRequestManager::FetchStatus status, |
| IdpNetworkRequestManager::AccountList accounts) { |
| accounts_fetched_time_ = base::TimeTicks::Now(); |
| |
| GURL idp_config_url = idp_info->provider->config->config_url; |
| const std::optional<bool> old_idp_signin_status = |
| permission_delegate_->GetIdpSigninStatus( |
| url::Origin::Create(idp_config_url)); |
| webid::UpdateIdpSigninStatusForAccountsEndpointResponse( |
| render_frame_host(), idp_config_url, status, |
| idp_info->has_failing_idp_signin_status, permission_delegate_); |
| |
| constexpr char kAccountsUrl[] = "accounts endpoint"; |
| switch (status.parse_status) { |
| case IdpNetworkRequestManager::ParseStatus::kHttpNotFoundError: { |
| MaybeAddResponseCodeToConsole(kAccountsUrl, status.response_code); |
| HandleAccountsFetchFailure( |
| std::move(idp_info), old_idp_signin_status, |
| FederatedAuthRequestResult::kErrorFetchingAccountsHttpNotFound, |
| TokenStatus::kAccountsHttpNotFound); |
| return; |
| } |
| case IdpNetworkRequestManager::ParseStatus::kNoResponseError: { |
| MaybeAddResponseCodeToConsole(kAccountsUrl, status.response_code); |
| HandleAccountsFetchFailure( |
| std::move(idp_info), old_idp_signin_status, |
| FederatedAuthRequestResult::kErrorFetchingAccountsNoResponse, |
| TokenStatus::kAccountsNoResponse); |
| return; |
| } |
| case IdpNetworkRequestManager::ParseStatus::kInvalidResponseError: { |
| MaybeAddResponseCodeToConsole(kAccountsUrl, status.response_code); |
| HandleAccountsFetchFailure( |
| std::move(idp_info), old_idp_signin_status, |
| FederatedAuthRequestResult::kErrorFetchingAccountsInvalidResponse, |
| TokenStatus::kAccountsInvalidResponse); |
| return; |
| } |
| case IdpNetworkRequestManager::ParseStatus::kEmptyListError: { |
| MaybeAddResponseCodeToConsole(kAccountsUrl, status.response_code); |
| HandleAccountsFetchFailure( |
| std::move(idp_info), old_idp_signin_status, |
| FederatedAuthRequestResult::kErrorFetchingAccountsListEmpty, |
| TokenStatus::kAccountsListEmpty); |
| return; |
| } |
| case IdpNetworkRequestManager::ParseStatus::kInvalidContentTypeError: { |
| MaybeAddResponseCodeToConsole(kAccountsUrl, status.response_code); |
| HandleAccountsFetchFailure( |
| std::move(idp_info), old_idp_signin_status, |
| FederatedAuthRequestResult::kErrorFetchingAccountsInvalidContentType, |
| TokenStatus::kAccountsInvalidContentType); |
| return; |
| } |
| case IdpNetworkRequestManager::ParseStatus::kSuccess: { |
| RecordRawAccountsSize(accounts.size()); |
| if (webid::IsFedCmAuthzEnabled(render_frame_host(), |
| url::Origin::Create(idp_config_url))) { |
| FilterAccountsWithLabel(idp_info->metadata.requested_label, accounts); |
| if (accounts.empty()) { |
| render_frame_host().AddMessageToConsole( |
| blink::mojom::ConsoleMessageLevel::kError, |
| "Accounts were received, but none matched the label."); |
| // If there are no accounts after filtering based on the label, |
| // treat this exactly the same as if we had received an empty accounts |
| // list, i.e. IdpNetworkRequestManager::ParseStatus::kEmptyListError. |
| HandleAccountsFetchFailure( |
| std::move(idp_info), old_idp_signin_status, |
| FederatedAuthRequestResult::kErrorFetchingAccountsListEmpty, |
| TokenStatus::kAccountsListEmpty); |
| return; |
| } |
| } |
| FilterAccountsWithLoginHint(idp_info->provider->login_hint, accounts); |
| if (accounts.empty()) { |
| render_frame_host().AddMessageToConsole( |
| blink::mojom::ConsoleMessageLevel::kError, |
| "Accounts were received, but none matched the loginHint."); |
| // If there are no accounts after filtering based on the login hint, |
| // treat this exactly the same as if we had received an empty accounts |
| // list, i.e. IdpNetworkRequestManager::ParseStatus::kEmptyListError. |
| HandleAccountsFetchFailure( |
| std::move(idp_info), old_idp_signin_status, |
| FederatedAuthRequestResult::kErrorFetchingAccountsListEmpty, |
| TokenStatus::kAccountsListEmpty); |
| return; |
| } |
| FilterAccountsWithDomainHint(idp_info->provider->domain_hint, accounts); |
| if (accounts.empty()) { |
| render_frame_host().AddMessageToConsole( |
| blink::mojom::ConsoleMessageLevel::kError, |
| "Accounts were received, but none matched the domainHint."); |
| // If there are no accounts after filtering based on the domain hint, |
| // treat this exactly the same as if we had received an empty accounts |
| // list, i.e. IdpNetworkRequestManager::ParseStatus::kEmptyListError. |
| HandleAccountsFetchFailure( |
| std::move(idp_info), old_idp_signin_status, |
| FederatedAuthRequestResult::kErrorFetchingAccountsListEmpty, |
| TokenStatus::kAccountsListEmpty); |
| return; |
| } |
| RecordReadyToShowAccountsSize(accounts.size()); |
| ComputeLoginStateAndReorderAccounts( |
| idp_info->provider->config->config_url, accounts); |
| |
| bool need_client_metadata = false; |
| |
| if (ShouldMediateAuthzFor(*idp_info->provider)) { |
| for (const IdentityRequestAccount& account : accounts) { |
| // ComputeLoginStateAndReorderAccounts() should have populated |
| // IdentityRequestAccount::login_state. |
| DCHECK(account.login_state); |
| if (*account.login_state == LoginState::kSignUp) { |
| need_client_metadata = true; |
| break; |
| } |
| } |
| } |
| |
| if (need_client_metadata && |
| webid::IsEndpointSameOrigin(idp_info->provider->config->config_url, |
| idp_info->endpoints.client_metadata)) { |
| // Copy OnClientMetadataResponseReceived() parameters because `idp_info` |
| // is moved. |
| GURL client_metadata_endpoint = idp_info->endpoints.client_metadata; |
| std::string client_id = idp_info->provider->config->client_id; |
| network_manager_->FetchClientMetadata( |
| client_metadata_endpoint, client_id, |
| base::BindOnce( |
| &FederatedAuthRequestImpl::OnClientMetadataResponseReceived, |
| weak_ptr_factory_.GetWeakPtr(), std::move(idp_info), |
| std::move(accounts))); |
| } else { |
| OnFetchDataForIdpSucceeded(std::move(idp_info), accounts, |
| IdpNetworkRequestManager::ClientMetadata()); |
| } |
| } |
| } |
| } |
| |
| void FederatedAuthRequestImpl::ComputeLoginStateAndReorderAccounts( |
| const GURL& idp_config_url, |
| IdpNetworkRequestManager::AccountList& accounts) { |
| url::Origin idp_origin = url::Origin::Create(idp_config_url); |
| // Populate the accounts login state. |
| for (auto& account : accounts) { |
| // Record when IDP and browser have different user sign-in states. |
| bool idp_claimed_sign_in = account.login_state == LoginState::kSignIn; |
| bool browser_observed_sign_in = permission_delegate_->HasSharingPermission( |
| origin(), GetEmbeddingOrigin(), idp_origin, account.id); |
| |
| if (idp_claimed_sign_in == browser_observed_sign_in) { |
| fedcm_metrics_->RecordSignInStateMatchStatus( |
| idp_config_url, SignInStateMatchStatus::kMatch); |
| } else if (idp_claimed_sign_in) { |
| fedcm_metrics_->RecordSignInStateMatchStatus( |
| idp_config_url, SignInStateMatchStatus::kIdpClaimedSignIn); |
| } else { |
| fedcm_metrics_->RecordSignInStateMatchStatus( |
| idp_config_url, SignInStateMatchStatus::kBrowserObservedSignIn); |
| } |
| |
| // We set the login state based on the IDP response if it sends |
| // back an approved_clients list. If it does not, we need to set |
| // it here based on browser state. |
| if (account.login_state) { |
| continue; |
| } |
| LoginState login_state = LoginState::kSignUp; |
| // Consider this a sign-in if we have seen a successful sign-up for |
| // this account before. |
| if (browser_observed_sign_in) { |
| login_state = LoginState::kSignIn; |
| } |
| account.login_state = login_state; |
| } |
| |
| // Populate the accounts' browser trusted login state. This bit may be useful |
| // to widget flow in the future and we can drop the condition if needed. So |
| // far the bit for the widget flow will always be kSignUp. |
| if (rp_mode_ == RpMode::kButton) { |
| for (auto& account : accounts) { |
| account.browser_trusted_login_state = LoginState::kSignUp; |
| if (webid::HasSharingPermissionOrIdpHasThirdPartyCookiesAccess( |
| render_frame_host(), /*provider_url=*/idp_config_url, |
| GetEmbeddingOrigin(), origin(), account.id, permission_delegate_, |
| api_permission_delegate_)) { |
| // At this moment we can trust login_state even though it's controlled |
| // by IdP. If it's kSignUp, it could mean that the browser's sharing |
| // permission is obsolete. |
| account.browser_trusted_login_state = account.login_state.value(); |
| } |
| } |
| } |
| |
| // Now that the login states have been computed, order accounts so that the |
| // returning accounts go first and the other accounts go afterwards. Since the |
| // number of accounts is likely very small, sorting by login_state should be |
| // fast. |
| std::sort(accounts.begin(), accounts.end(), [](const auto& a, const auto& b) { |
| return a.login_state < b.login_state; |
| }); |
| } |
| |
| void FederatedAuthRequestImpl::OnAccountSelected(const GURL& idp_config_url, |
| const std::string& account_id, |
| bool is_sign_in) { |
| DCHECK(!account_id.empty()); |
| const IdentityProviderInfo& idp_info = *idp_infos_[idp_config_url]; |
| |
| // Check if the user has disabled the FedCM API after the FedCM UI is |
| // displayed. This ensures that requests are not wrongfully sent to IDPs when |
| // settings are changed while an existing FedCM UI is displayed. Ideally, we |
| // should enforce this check before all requests but users typically won't |
| // have time to disable the FedCM API in other types of requests. |
| // Note that for the button flow is not affected by the permission status. |
| if (GetApiPermissionStatus() != FederatedApiPermissionStatus::GRANTED && |
| rp_mode_ != RpMode::kButton) { |
| CompleteRequestWithError( |
| FederatedAuthRequestResult::kErrorDisabledInSettings, |
| TokenStatus::kDisabledInSettings, |
| /*token_error=*/std::nullopt, |
| /*should_delay_callback=*/true); |
| return; |
| } |
| |
| if (identity_selection_type_ != kExplicit) { |
| // Embargo auto re-authn to mitigate a deadloop where an auto |
| // re-authenticated user gets auto re-authenticated again soon after logging |
| // out of the active session. |
| auto_reauthn_permission_delegate_->RecordEmbargoForAutoReauthn( |
| GetEmbeddingOrigin()); |
| } else { |
| // Once a user has explicitly selected an account, there is no need to block |
| // auto re-authn with embargo. |
| auto_reauthn_permission_delegate_->RemoveEmbargoForAutoReauthn( |
| GetEmbeddingOrigin()); |
| } |
| |
| fedcm_metrics_->RecordIsSignInUser(is_sign_in); |
| |
| api_permission_delegate_->RemoveEmbargoAndResetCounts(GetEmbeddingOrigin()); |
| |
| account_id_ = account_id; |
| select_account_time_ = base::TimeTicks::Now(); |
| fedcm_metrics_->RecordContinueOnPopupTime( |
| idp_config_url, select_account_time_ - accounts_dialog_display_time_); |
| |
| url::Origin idp_origin = url::Origin::Create(idp_config_url); |
| |
| IdpNetworkRequestManager::ContinueOnCallback continue_on; |
| if (webid::IsFedCmAuthzEnabled(render_frame_host(), idp_origin)) { |
| continue_on = base::BindOnce( |
| &FederatedAuthRequestImpl::OnContinueOnResponseReceived, |
| weak_ptr_factory_.GetWeakPtr(), idp_info.provider->Clone()); |
| } |
| |
| network_manager_->SendTokenRequest( |
| idp_info.endpoints.token, account_id_, |
| ComputeUrlEncodedTokenPostData( |
| render_frame_host(), idp_origin, idp_info.provider->config->client_id, |
| idp_info.provider->nonce, account_id, is_sign_in, |
| identity_selection_type_ != kExplicit, rp_mode_, |
| idp_info.provider->scope, idp_info.provider->responseType, |
| idp_info.provider->params), |
| base::BindOnce(&FederatedAuthRequestImpl::OnTokenResponseReceived, |
| weak_ptr_factory_.GetWeakPtr(), |
| idp_info.provider->Clone()), |
| std::move(continue_on), |
| base::BindOnce(&FederatedAuthRequestImpl::RecordErrorMetrics, |
| weak_ptr_factory_.GetWeakPtr(), |
| idp_info.provider->Clone())); |
| } |
| |
| void FederatedAuthRequestImpl::OnDismissFailureDialog( |
| IdentityRequestDialogController::DismissReason dismiss_reason) { |
| // Clicking the close button and swiping away the account chooser are more |
| // intentional than other ways of dismissing the account chooser such as |
| // the virtual keyboard showing on Android. Dismissal through closing the |
| // pop-up window is not embargoed since the user has taken some action to |
| // continue to open the pop-up window. |
| bool should_embargo = false; |
| switch (dismiss_reason) { |
| case IdentityRequestDialogController::DismissReason::kCloseButton: |
| case IdentityRequestDialogController::DismissReason::kSwipe: |
| should_embargo = true; |
| break; |
| default: |
| break; |
| } |
| |
| fedcm_metrics_->RecordCancelReason(dismiss_reason); |
| |
| should_embargo &= rp_mode_ == RpMode::kWidget; |
| if (should_embargo) { |
| api_permission_delegate_->RecordDismissAndEmbargo(GetEmbeddingOrigin()); |
| } |
| |
| CompleteRequestWithError(should_embargo |
| ? FederatedAuthRequestResult::kShouldEmbargo |
| : FederatedAuthRequestResult::kError, |
| should_embargo ? TokenStatus::kShouldEmbargo |
| : TokenStatus::kNotSignedInWithIdp, |
| /*token_error=*/std::nullopt, |
| /*should_delay_callback=*/false); |
| } |
| |
| void FederatedAuthRequestImpl::OnDismissErrorDialog( |
| const GURL& idp_config_url, |
| IdpNetworkRequestManager::FetchStatus status, |
| std::optional<TokenError> token_error, |
| IdentityRequestDialogController::DismissReason dismiss_reason) { |
| bool has_url = token_error && !token_error->url.is_empty(); |
| ErrorDialogResult result; |
| switch (dismiss_reason) { |
| case IdentityRequestDialogController::DismissReason::kCloseButton: |
| result = has_url ? ErrorDialogResult::kCloseWithMoreDetails |
| : ErrorDialogResult::kCloseWithoutMoreDetails; |
| break; |
| case IdentityRequestDialogController::DismissReason::kSwipe: |
| result = has_url ? ErrorDialogResult::kSwipeWithMoreDetails |
| : ErrorDialogResult::kSwipeWithoutMoreDetails; |
| break; |
| case IdentityRequestDialogController::DismissReason::kGotItButton: |
| result = has_url ? ErrorDialogResult::kGotItWithMoreDetails |
| : ErrorDialogResult::kGotItWithoutMoreDetails; |
| break; |
| case IdentityRequestDialogController::DismissReason::kMoreDetailsButton: |
| result = ErrorDialogResult::kMoreDetails; |
| break; |
| default: |
| result = has_url ? ErrorDialogResult::kOtherWithMoreDetails |
| : ErrorDialogResult::kOtherWithoutMoreDetails; |
| break; |
| } |
| fedcm_metrics_->RecordErrorDialogResult(result, idp_config_url); |
| |
| CompleteTokenRequest(idp_config_url, status, /*token=*/std::nullopt, |
| token_error, /*should_delay_callback=*/false); |
| } |
| |
| void FederatedAuthRequestImpl::OnDialogDismissed( |
| IdentityRequestDialogController::DismissReason dismiss_reason) { |
| if (dialog_type_ == kContinueOnPopup) { |
| fedcm_metrics_->RecordContinueOnPopupResult( |
| FedCmContinueOnPopupResult::kWindowClosed); |
| // Popups always get dismissed with reason kOther, so we never embargo. |
| CompleteRequestWithError(FederatedAuthRequestResult::kError, |
| TokenStatus::kContinuationPopupClosedByUser, |
| /*token_error=*/std::nullopt, |
| /*should_delay_callback=*/false); |
| return; |
| } |
| |
| // Clicking the close button and swiping away the account chooser are more |
| // intentional than other ways of dismissing the account chooser such as |
| // the virtual keyboard showing on Android. |
| bool should_embargo = false; |
| switch (dismiss_reason) { |
| case IdentityRequestDialogController::DismissReason::kCloseButton: |
| case IdentityRequestDialogController::DismissReason::kSwipe: |
| should_embargo = true; |
| break; |
| default: |
| break; |
| } |
| |
| if (should_embargo) { |
| base::TimeTicks dismiss_dialog_time = base::TimeTicks::Now(); |
| fedcm_metrics_->RecordCancelOnDialogTime( |
| idp_data_for_display_, |
| dismiss_dialog_time - accounts_dialog_display_time_); |
| } |
| fedcm_metrics_->RecordCancelReason(dismiss_reason); |
| |
| should_embargo &= rp_mode_ == RpMode::kWidget; |
| if (should_embargo) { |
| api_permission_delegate_->RecordDismissAndEmbargo(GetEmbeddingOrigin()); |
| } |
| |
| // Reject the promise immediately if the UI is dismissed without selecting |
| // an account. Meanwhile, we fuzz the rejection time for other failures to |
| // make it indistinguishable. |
| CompleteRequestWithError(should_embargo |
| ? FederatedAuthRequestResult::kShouldEmbargo |
| : FederatedAuthRequestResult::kError, |
| should_embargo ? TokenStatus::kShouldEmbargo |
| : TokenStatus::kNotSelectAccount, |
| /*token_error=*/std::nullopt, |
| /*should_delay_callback=*/false); |
| } |
| |
| void FederatedAuthRequestImpl::ShowModalDialog(DialogType dialog_type, |
| const GURL& idp_config_url, |
| const GURL& url_to_show) { |
| // Reset dialog type, since we are typically not showing a FedCM dialog while |
| // the popup window is open. When using the button flow the dialog may |
| // still be up in some cases, but we do not expect that browser automation |
| // needs to interact with the account chooser in this case. |
| if (dialog_type_ != kNone) { |
| // This call ensures that we send a dialogClosed event if an account |
| // chooser or mismatch dialog is open. |
| devtools_instrumentation::DidCloseFedCmDialog(render_frame_host()); |
| } |
| // TODO(crbug.com/336815315): Should we notify browser automation of this |
| // dialog? |
| dialog_type_ = dialog_type; |
| |
| WebContents* web_contents = request_dialog_controller_->ShowModalDialog( |
| url_to_show, base::BindOnce(&FederatedAuthRequestImpl::OnDialogDismissed, |
| weak_ptr_factory_.GetWeakPtr())); |
| // This may be null on Android, as the method cannot return the WebContents of |
| // the CCT that will be created. |
| if (web_contents) { |
| IdentityRegistry::CreateForWebContents( |
| web_contents, weak_ptr_factory_.GetWeakPtr(), idp_config_url); |
| } |
| |
| // Samples are at most 10 minutes. This metric is used to determine a |
| // reasonable minimum duration for the mismatch dialog to be shown to prevent |
| // abuse through flashing UI. When users trigger the IDP sign-in flow, the |
| // mismatch dialog is hidden so we record this metric upon user triggering the |
| // flow. |
| if (mismatch_dialog_shown_time_.has_value()) { |
| fedcm_metrics_->RecordMismatchDialogShownDuration( |
| idp_data_for_display_, |
| base::TimeTicks::Now() - mismatch_dialog_shown_time_.value()); |
| mismatch_dialog_shown_time_ = std::nullopt; |
| } |
| } |
| |
| void FederatedAuthRequestImpl::OnContinueOnResponseReceived( |
| IdentityProviderRequestOptionsPtr idp, |
| IdpNetworkRequestManager::FetchStatus status, |
| const GURL& continue_on) { |
| url::Origin idp_origin = url::Origin::Create(idp->config->config_url); |
| // This is enforced by OnAccountSelected when we call SendTokenRequest. |
| DCHECK(webid::IsFedCmAuthzEnabled(render_frame_host(), idp_origin)); |
| |
| id_assertion_response_time_ = base::TimeTicks::Now(); |
| |
| GetContentClient()->browser()->LogWebFeatureForCurrentPage( |
| &render_frame_host(), blink::mojom::WebFeature::kFedCmContinueOnResponse); |
| |
| // We only allow loading continue_on urls that are same-origin |
| // with the IdP. |
| // This isn't necessarily final, but seemed like a safer |
| // and sufficient default for now. |
| // This behavior may change in https://crbug.com/1429083 |
| bool is_same_origin = |
| url::Origin::Create(continue_on).IsSameOriginWith(idp_origin); |
| |
| bool can_show_popup = CanShowContinueOnPopup(); |
| if (!is_same_origin || !can_show_popup) { |
| if (!is_same_origin && !can_show_popup) { |
| fedcm_metrics_->RecordContinueOnPopupStatus( |
| FedCmContinueOnPopupStatus::kUrlNotSameOriginAndPopupNotAllowed); |
| } else if (!is_same_origin) { |
| fedcm_metrics_->RecordContinueOnPopupStatus( |
| FedCmContinueOnPopupStatus::kUrlNotSameOrigin); |
| } else if (!can_show_popup) { |
| fedcm_metrics_->RecordContinueOnPopupStatus( |
| FedCmContinueOnPopupStatus::kPopupNotAllowed); |
| } |
| |
| CompleteRequestWithError( |
| FederatedAuthRequestResult::kErrorFetchingIdTokenInvalidResponse, |
| TokenStatus::kIdTokenInvalidResponse, |
| /*token_error=*/std::nullopt, |
| /*should_delay_callback=*/false); |
| return; |
| } |
| |
| fedcm_metrics_->RecordContinueOnPopupStatus( |
| FedCmContinueOnPopupStatus::kPopupOpened); |
| ShowModalDialog(kContinueOnPopup, idp->config->config_url, continue_on); |
| } |
| |
| void FederatedAuthRequestImpl::ShowErrorDialog( |
| const GURL& idp_config_url, |
| IdpNetworkRequestManager::FetchStatus status, |
| std::optional<TokenError> token_error) { |
| CHECK(idp_infos_.find(idp_config_url) != idp_infos_.end()); |
| |
| std::optional<std::string> iframe_for_display = GetIframeOriginForDisplay( |
| GetEmbeddingOrigin(), origin(), |
| base::BindOnce(&FederatedAuthRequestImpl::CompleteRequestWithError, |
| weak_ptr_factory_.GetWeakPtr())); |
| |
| dialog_type_ = kError; |
| config_url_ = idp_config_url; |
| token_request_status_ = status; |
| token_error_ = token_error; |
| |
| // TODO(crbug.com/40282657): Refactor IdentityCredentialTokenError |
| request_dialog_controller_->ShowErrorDialog( |
| GetTopFrameOriginForDisplay(GetEmbeddingOrigin()), iframe_for_display, |
| FormatOriginForDisplay(url::Origin::Create(idp_config_url)), |
| idp_infos_[idp_config_url]->rp_context, rp_mode_, |
| idp_infos_[idp_config_url]->metadata, token_error, |
| base::BindOnce(&FederatedAuthRequestImpl::OnDismissErrorDialog, |
| weak_ptr_factory_.GetWeakPtr(), idp_config_url, status, |
| token_error), |
| token_error && !token_error->url.is_empty() |
| ? base::BindOnce(&FederatedAuthRequestImpl::ShowModalDialog, |
| weak_ptr_factory_.GetWeakPtr(), kErrorUrlPopup, |
| config_url_, token_error->url) |
| : base::NullCallback()); |
| devtools_instrumentation::DidShowFedCmDialog(render_frame_host()); |
| } |
| |
| void FederatedAuthRequestImpl::OnTokenResponseReceived( |
| IdentityProviderRequestOptionsPtr idp, |
| IdpNetworkRequestManager::FetchStatus status, |
| IdpNetworkRequestManager::TokenResult result) { |
| CHECK(result.token.empty() || !result.error); |
| |
| bool should_show_error_ui = |
| result.error || |
| status.parse_status != IdpNetworkRequestManager::ParseStatus::kSuccess; |
| auto complete_request_callback = |
| should_show_error_ui |
| ? base::BindOnce(&FederatedAuthRequestImpl::ShowErrorDialog, |
| weak_ptr_factory_.GetWeakPtr(), |
| idp->config->config_url, status, result.error) |
| : base::BindOnce(&FederatedAuthRequestImpl::CompleteTokenRequest, |
| weak_ptr_factory_.GetWeakPtr(), |
| idp->config->config_url, status, result.token, |
| /*token_error=*/std::nullopt, |
| /*should_delay_callback=*/false); |
| |
| // When fetching id tokens we show a "Verify" sheet to users in case fetching |
| // takes a long time due to latency etc. In case that the fetching process is |
| // fast, we still want to show the "Verify" sheet for at least |
| // `kTokenRequestDelay` seconds for better UX. |
| // Note that for button flow we can complete without delay because the UI |
| // difference between the verifying UI and its predecessors are minor. |
| id_assertion_response_time_ = base::TimeTicks::Now(); |
| base::TimeDelta fetch_time = |
| id_assertion_response_time_ - select_account_time_; |
| if (should_complete_request_immediately_ || rp_mode_ == RpMode::kButton || |
| fetch_time >= kTokenRequestDelay) { |
| std::move(complete_request_callback).Run(); |
| return; |
| } |
| |
| base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask( |
| FROM_HERE, std::move(complete_request_callback), |
| kTokenRequestDelay - fetch_time); |
| } |
| |
| void FederatedAuthRequestImpl::CompleteTokenRequest( |
| const GURL& idp_config_url, |
| IdpNetworkRequestManager::FetchStatus status, |
| std::optional<std::string> token, |
| std::optional<TokenError> token_error, |
| bool should_delay_callback) { |
| DCHECK(!start_time_.is_null()); |
| constexpr char kIdAssertionUrl[] = "id assertion endpoint"; |
| switch (status.parse_status) { |
| case IdpNetworkRequestManager::ParseStatus::kHttpNotFoundError: { |
| MaybeAddResponseCodeToConsole(kIdAssertionUrl, status.response_code); |
| CompleteRequestWithError( |
| FederatedAuthRequestResult::kErrorFetchingIdTokenHttpNotFound, |
| TokenStatus::kIdTokenHttpNotFound, token_error, |
| should_delay_callback); |
| return; |
| } |
| case IdpNetworkRequestManager::ParseStatus::kNoResponseError: { |
| MaybeAddResponseCodeToConsole(kIdAssertionUrl, status.response_code); |
| CompleteRequestWithError( |
| FederatedAuthRequestResult::kErrorFetchingIdTokenNoResponse, |
| TokenStatus::kIdTokenNoResponse, token_error, should_delay_callback); |
| return; |
| } |
| case IdpNetworkRequestManager::ParseStatus::kInvalidResponseError: { |
| MaybeAddResponseCodeToConsole(kIdAssertionUrl, status.response_code); |
| CompleteRequestWithError( |
| FederatedAuthRequestResult::kErrorFetchingIdTokenInvalidResponse, |
| TokenStatus::kIdTokenInvalidResponse, token_error, |
| should_delay_callback); |
| return; |
| } |
| case IdpNetworkRequestManager::ParseStatus::kInvalidContentTypeError: { |
| MaybeAddResponseCodeToConsole(kIdAssertionUrl, status.response_code); |
| CompleteRequestWithError( |
| FederatedAuthRequestResult::kErrorFetchingIdTokenInvalidContentType, |
| TokenStatus::kIdTokenInvalidContentType, token_error, |
| should_delay_callback); |
| return; |
| } |
| case IdpNetworkRequestManager::ParseStatus::kEmptyListError: { |
| NOTREACHED() << "kEmptyListError is undefined for CompleteTokenRequest"; |
| return; |
| } |
| case IdpNetworkRequestManager::ParseStatus::kSuccess: { |
| if (token_error) { |
| MaybeAddResponseCodeToConsole(kIdAssertionUrl, status.response_code); |
| if (error_url_type_ && *error_url_type_ == ErrorUrlType::kCrossSite) { |
| CompleteRequestWithError( |
| FederatedAuthRequestResult:: |
| kErrorFetchingIdTokenCrossSiteIdpErrorResponse, |
| TokenStatus::kIdTokenCrossSiteIdpErrorResponse, token_error, |
| should_delay_callback); |
| return; |
| } |
| CompleteRequestWithError( |
| FederatedAuthRequestResult::kErrorFetchingIdTokenIdpErrorResponse, |
| TokenStatus::kIdTokenIdpErrorResponse, token_error, |
| should_delay_callback); |
| return; |
| } |
| |
| // Auto re-authentication can only be triggered when there's already a |
| // sharing permission OR the IdP is exempted with 3PC access. Either way |
| // we shouldn't explicitly grant permission here. |
| CHECK(!account_id_.empty()); |
| if (identity_selection_type_ == kExplicit) { |
| permission_delegate_->GrantSharingPermission( |
| origin(), GetEmbeddingOrigin(), url::Origin::Create(idp_config_url), |
| account_id_); |
| } else { |
| permission_delegate_->RefreshExistingSharingPermission( |
| origin(), GetEmbeddingOrigin(), url::Origin::Create(idp_config_url), |
| account_id_); |
| } |
| |
| SetRequiresUserMediation(false); |
| |
| fedcm_metrics_->RecordTokenResponseAndTurnaroundTime( |
| idp_config_url, id_assertion_response_time_ - select_account_time_, |
| id_assertion_response_time_ - start_time_ - |
| (accounts_dialog_display_time_ - |
| ready_to_display_accounts_dialog_time_)); |
| |
| if (IsFedCmMetricsEndpointEnabled()) { |
| for (const auto& metrics_endpoint_kv : metrics_endpoints_) { |
| const GURL& metrics_endpoint = metrics_endpoint_kv.second; |
| if (!metrics_endpoint.is_valid()) { |
| continue; |
| } |
| |
| if (metrics_endpoint_kv.first == idp_config_url) { |
| network_manager_->SendSuccessfulTokenRequestMetrics( |
| metrics_endpoint, |
| ready_to_display_accounts_dialog_time_ - start_time_, |
| select_account_time_ - accounts_dialog_display_time_, |
| id_assertion_response_time_ - select_account_time_, |
| id_assertion_response_time_ - start_time_ - |
| (accounts_dialog_display_time_ - |
| ready_to_display_accounts_dialog_time_)); |
| } else { |
| // Send kUserFailure so that IDP cannot tell difference between user |
| // selecting a different IDP and user dismissing dialog without |
| // selecting any IDP. |
| network_manager_->SendFailedTokenRequestMetrics( |
| metrics_endpoint, IdpNetworkRequestManager:: |
| MetricsEndpointErrorCode::kUserFailure); |
| } |
| } |
| } |
| |
| CompleteRequest(FederatedAuthRequestResult::kSuccess, |
| TokenStatus::kSuccessUsingTokenInHttpResponse, |
| /*token_error=*/std::nullopt, idp_config_url, |
| token.value(), |
| /*should_delay_callback=*/false); |
| return; |
| } |
| } |
| } |
| |
| void FederatedAuthRequestImpl::CompleteRequestWithError( |
| blink::mojom::FederatedAuthRequestResult result, |
| std::optional<TokenStatus> token_status, |
| std::optional<TokenError> token_error, |
| bool should_delay_callback) { |
| CompleteRequest(result, token_status, token_error, |
| /*selected_idp_config_url=*/std::nullopt, |
| /*token=*/"", should_delay_callback); |
| } |
| |
| void FederatedAuthRequestImpl::CompleteRequest( |
| blink::mojom::FederatedAuthRequestResult result, |
| std::optional<TokenStatus> token_status, |
| std::optional<TokenError> token_error, |
| const std::optional<GURL>& selected_idp_config_url, |
| const std::string& id_token, |
| bool should_delay_callback) { |
| DCHECK(result == FederatedAuthRequestResult::kSuccess || id_token.empty()); |
| |
| if (accounts_dialog_shown_time_.has_value()) { |
| fedcm_metrics_->RecordAccountsDialogShownDuration( |
| idp_data_for_display_, |
| base::TimeTicks::Now() - accounts_dialog_shown_time_.value()); |
| accounts_dialog_shown_time_ = std::nullopt; |
| } |
| |
| if (mismatch_dialog_shown_time_.has_value()) { |
| fedcm_metrics_->RecordMismatchDialogShownDuration( |
| idp_data_for_display_, |
| base::TimeTicks::Now() - mismatch_dialog_shown_time_.value()); |
| mismatch_dialog_shown_time_ = std::nullopt; |
| } |
| |
| if (!auth_request_token_callback_) { |
| return; |
| } |
| |
| if (token_status) { |
| int num_idps_mismatch = std::count_if( |
| idp_data_for_display_.begin(), idp_data_for_display_.end(), |
| [](auto& provider) { return provider.has_login_status_mismatch; }); |
| fedcm_metrics_->RecordRequestTokenStatus( |
| *token_status, mediation_requirement_, idp_order_, num_idps_mismatch, |
| selected_idp_config_url, rp_mode_); |
| } |
| |
| if (!errors_logged_to_console_ && |
| result != FederatedAuthRequestResult::kSuccess) { |
| errors_logged_to_console_ = true; |
| |
| AddDevToolsIssue(result); |
| AddConsoleErrorMessage(result); |
| |
| if (IsFedCmMetricsEndpointEnabled()) { |
| for (const auto& metrics_endpoint_kv : metrics_endpoints_) { |
| SendFailedTokenRequestMetrics(metrics_endpoint_kv.second, result); |
| } |
| } |
| } |
| |
| bool is_auto_selected = identity_selection_type_ != kExplicit; |
| |
| if (ShouldNotifyDevtoolsForDialogType(dialog_type_)) { |
| devtools_instrumentation::DidCloseFedCmDialog(render_frame_host()); |
| } |
| |
| if (!should_delay_callback || should_complete_request_immediately_) { |
| CleanUp(); |
| webid::GetPageData(&render_frame_host()) |
| ->SetPendingWebIdentityRequest(nullptr); |
| errors_logged_to_console_ = false; |
| |
| blink::mojom::TokenErrorPtr error; |
| if (token_error) { |
| error = blink::mojom::TokenError::New(); |
| error->code = token_error->code; |
| error->url = token_error->url.spec(); |
| } |
| RequestTokenStatus status = |
| FederatedAuthRequestResultToRequestTokenStatus(result); |
| std::move(auth_request_token_callback_) |
| .Run(status, selected_idp_config_url, id_token, std::move(error), |
| is_auto_selected); |
| auth_request_token_callback_.Reset(); |
| } else { |
| base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask( |
| FROM_HERE, |
| base::BindOnce(&FederatedAuthRequestImpl::OnRejectRequest, |
| weak_ptr_factory_.GetWeakPtr()), |
| GetRandomRejectionTime()); |
| } |
| } |
| |
| void FederatedAuthRequestImpl::SendFailedTokenRequestMetrics( |
| const GURL& metrics_endpoint, |
| blink::mojom::FederatedAuthRequestResult result) { |
| DCHECK(IsFedCmMetricsEndpointEnabled()); |
| if (!metrics_endpoint.is_valid()) { |
| return; |
| } |
| |
| network_manager_->SendFailedTokenRequestMetrics( |
| metrics_endpoint, |
| FederatedAuthRequestResultToMetricsEndpointErrorCode(result)); |
| } |
| |
| void FederatedAuthRequestImpl::CleanUp() { |
| weak_ptr_factory_.InvalidateWeakPtrs(); |
| |
| permission_delegate_->RemoveIdpSigninStatusObserver(this); |
| |
| request_dialog_controller_.reset(); |
| network_manager_.reset(); |
| // Given that |request_dialog_controller_| has reference to this web content |
| // instance we destroy that first. |
| provider_fetcher_.reset(); |
| account_id_ = std::string(); |
| start_time_ = base::TimeTicks(); |
| well_known_and_config_fetched_time_ = base::TimeTicks(); |
| accounts_fetched_time_ = base::TimeTicks(); |
| client_metadata_fetched_time_ = base::TimeTicks(); |
| ready_to_display_accounts_dialog_time_ = base::TimeTicks(); |
| accounts_dialog_display_time_ = base::TimeTicks(); |
| select_account_time_ = base::TimeTicks(); |
| id_assertion_response_time_ = base::TimeTicks(); |
| accounts_dialog_shown_time_ = std::nullopt; |
| mismatch_dialog_shown_time_ = std::nullopt; |
| has_shown_mismatch_ = false; |
| idp_login_infos_.clear(); |
| idp_infos_.clear(); |
| idp_data_for_display_.clear(); |
| account_ids_before_login_.clear(); |
| fetch_data_ = FetchData(); |
| idp_order_.clear(); |
| metrics_endpoints_.clear(); |
| token_request_get_infos_.clear(); |
| login_url_ = GURL(); |
| config_url_ = GURL(); |
| token_error_ = std::nullopt; |
| dialog_type_ = kNone; |
| identity_selection_type_ = kExplicit; |
| had_transient_user_activation_ = false; |
| rp_mode_ = RpMode::kWidget; |
| } |
| |
| void FederatedAuthRequestImpl::AddDevToolsIssue( |
| FederatedAuthRequestResult result) { |
| DCHECK_NE(result, FederatedAuthRequestResult::kSuccess); |
| |
| // It would be possible to add this inspector issue on the renderer, which |
| // will receive the callback. However, it is preferable to do so on the |
| // browser because this is closer to the source, which means adding |
| // additional metadata is easier. In addition, in the future we may only |
| // need to pass a small amount of information to the renderer in the case of |
| // an error, so it would be cleaner to do this by reporting the inspector |
| // issue from the browser. |
| auto details = blink::mojom::InspectorIssueDetails::New(); |
| auto federated_auth_request_details = |
| blink::mojom::FederatedAuthRequestIssueDetails::New(result); |
| details->federated_auth_request_details = |
| std::move(federated_auth_request_details); |
| render_frame_host().ReportInspectorIssue( |
| blink::mojom::InspectorIssueInfo::New( |
| blink::mojom::InspectorIssueCode::kFederatedAuthRequestIssue, |
| std::move(details))); |
| } |
| |
| void FederatedAuthRequestImpl::AddConsoleErrorMessage( |
| FederatedAuthRequestResult result) { |
| render_frame_host().AddMessageToConsole( |
| blink::mojom::ConsoleMessageLevel::kError, |
| webid::GetConsoleErrorMessageFromResult(result)); |
| } |
| |
| void FederatedAuthRequestImpl::MaybeAddResponseCodeToConsole( |
| 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); |
| } |
| } |
| |
| url::Origin FederatedAuthRequestImpl::GetEmbeddingOrigin() const { |
| return render_frame_host().GetMainFrame()->GetLastCommittedOrigin(); |
| } |
| |
| void FederatedAuthRequestImpl::CompleteUserInfoRequest( |
| FederatedAuthUserInfoRequest* request, |
| RequestUserInfoCallback callback, |
| blink::mojom::RequestUserInfoStatus status, |
| std::optional<std::vector<blink::mojom::IdentityUserInfoPtr>> user_info) { |
| auto it = std::find_if( |
| user_info_requests_.begin(), user_info_requests_.end(), |
| [request](const std::unique_ptr<FederatedAuthUserInfoRequest>& ptr) { |
| return ptr.get() == request; |
| }); |
| // The request may not be found if the completion is invoked from |
| // FederatedAuthRequestImpl destructor. The destructor clears |
| // `user_info_requests_`, which destroys the FederatedAuthUserInfoRequests it |
| // contains. The FederatedAuthUserInfoRequest destructor invokes this |
| // callback. |
| if (it == user_info_requests_.end() && |
| status == blink::mojom::RequestUserInfoStatus::kSuccess) { |
| NOTREACHED() << "The successful user info request is nowhere to be found"; |
| return; |
| } |
| std::move(callback).Run(status, std::move(user_info)); |
| if (it != user_info_requests_.end()) { |
| user_info_requests_.erase(it); |
| } |
| } |
| |
| std::unique_ptr<IdpNetworkRequestManager> |
| FederatedAuthRequestImpl::CreateNetworkManager() { |
| if (mock_network_manager_) { |
| return std::move(mock_network_manager_); |
| } |
| |
| return IdpNetworkRequestManager::Create( |
| static_cast<RenderFrameHostImpl*>(&render_frame_host())); |
| } |
| |
| std::unique_ptr<IdentityRequestDialogController> |
| FederatedAuthRequestImpl::CreateDialogController() { |
| if (mock_dialog_controller_) { |
| return std::move(mock_dialog_controller_); |
| } |
| |
| WebContents* web_contents = |
| WebContents::FromRenderFrameHost(&render_frame_host()); |
| if (base::CommandLine::ForCurrentProcess()->HasSwitch( |
| switches::kUseFakeUIForFedCM)) { |
| std::string selected_account = |
| base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII( |
| switches::kUseFakeUIForFedCM); |
| return std::make_unique<FakeIdentityRequestDialogController>( |
| selected_account.empty() ? std::nullopt |
| : std::optional<std::string>(selected_account), |
| web_contents); |
| } |
| |
| return GetContentClient()->browser()->CreateIdentityRequestDialogController( |
| web_contents); |
| } |
| |
| void FederatedAuthRequestImpl::SetNetworkManagerForTests( |
| std::unique_ptr<IdpNetworkRequestManager> manager) { |
| mock_network_manager_ = std::move(manager); |
| } |
| |
| void FederatedAuthRequestImpl::SetDialogControllerForTests( |
| std::unique_ptr<IdentityRequestDialogController> controller) { |
| mock_dialog_controller_ = std::move(controller); |
| } |
| |
| void FederatedAuthRequestImpl::OnClose() { |
| #if BUILDFLAG(IS_ANDROID) |
| // We invoke this method on the modal dialog on Android, so we may need to |
| // create the controller at this point. |
| if (!request_dialog_controller_) { |
| request_dialog_controller_ = CreateDialogController(); |
| } |
| #endif // BUILDFLAG(IS_ANDROID) |
| CHECK(request_dialog_controller_); |
| request_dialog_controller_->CloseModalDialog(); |
| } |
| |
| bool FederatedAuthRequestImpl::OnResolve( |
| GURL idp_config_url, |
| const std::optional<std::string>& account_id, |
| const std::string& token) { |
| // Close the pop-up window post user permission. |
| OnClose(); |
| |
| permission_delegate_->GrantSharingPermission( |
| origin(), GetEmbeddingOrigin(), url::Origin::Create(idp_config_url), |
| account_id.value_or(account_id_)); |
| |
| fedcm_metrics_->RecordContinueOnResponseAndTurnaroundTime( |
| id_assertion_response_time_ - select_account_time_, |
| base::TimeTicks::Now() - start_time_ - |
| (accounts_dialog_display_time_ - |
| ready_to_display_accounts_dialog_time_)); |
| fedcm_metrics_->RecordContinueOnPopupResult( |
| FedCmContinueOnPopupResult::kTokenReceived); |
| |
| CompleteRequest(FederatedAuthRequestResult::kSuccess, |
| TokenStatus::kSuccessUsingIdentityProviderResolve, |
| /*token_error=*/std::nullopt, idp_config_url, token, |
| /*should_delay_callback=*/false); |
| // TODO(crbug.com/40262526): handle the corner cases where CompleteRequest |
| // can't actually fulfill the request. |
| return true; |
| } |
| |
| void FederatedAuthRequestImpl::OnRejectRequest() { |
| if (!auth_request_token_callback_) { |
| return; |
| } |
| DCHECK(errors_logged_to_console_); |
| CompleteRequestWithError(FederatedAuthRequestResult::kError, |
| /*token_status=*/std::nullopt, |
| /*token_error=*/std::nullopt, |
| /*should_delay_callback=*/false); |
| } |
| |
| FederatedApiPermissionStatus |
| FederatedAuthRequestImpl::GetApiPermissionStatus() { |
| DCHECK(api_permission_delegate_); |
| return api_permission_delegate_->GetApiPermissionStatus(GetEmbeddingOrigin()); |
| } |
| |
| bool FederatedAuthRequestImpl::ShouldNotifyDevtoolsForDialogType( |
| DialogType type) { |
| return type != kNone && type != kLoginToIdpPopup && |
| type != kContinueOnPopup && type != kErrorUrlPopup; |
| } |
| |
| void FederatedAuthRequestImpl::AcceptAccountsDialogForDevtools( |
| const GURL& config_url, |
| const IdentityRequestAccount& account) { |
| bool is_sign_in = |
| account.login_state == IdentityRequestAccount::LoginState::kSignIn; |
| OnAccountSelected(config_url, account.id, is_sign_in); |
| } |
| |
| void FederatedAuthRequestImpl::DismissAccountsDialogForDevtools( |
| bool should_embargo) { |
| // We somewhat arbitrarily pick a reason that does/does not trigger |
| // cooldown. |
| IdentityRequestDialogController::DismissReason reason = |
| should_embargo |
| ? IdentityRequestDialogController::DismissReason::kCloseButton |
| : IdentityRequestDialogController::DismissReason::kOther; |
| OnDialogDismissed(reason); |
| } |
| |
| void FederatedAuthRequestImpl::AcceptConfirmIdpLoginDialogForDevtools() { |
| DCHECK(login_url_.is_valid()); |
| LoginToIdP(/*can_append_hints=*/true, config_url_, login_url_); |
| } |
| |
| void FederatedAuthRequestImpl::DismissConfirmIdpLoginDialogForDevtools() { |
| // These values match what HandleAccountsFetchFailure passes. |
| OnDismissFailureDialog( |
| IdentityRequestDialogController::DismissReason::kOther); |
| } |
| |
| bool FederatedAuthRequestImpl::UseAnotherAccountForDevtools( |
| const IdentityProviderData& provider) { |
| if (!provider.idp_metadata.supports_add_account) { |
| return false; |
| } |
| LoginToIdP(/*can_append_hints=*/true, provider.idp_metadata.config_url, |
| provider.idp_metadata.idp_login_url); |
| return true; |
| } |
| |
| bool FederatedAuthRequestImpl::HasMoreDetailsButtonForDevtools() { |
| return token_error_ && token_error_->url.is_valid(); |
| } |
| |
| void FederatedAuthRequestImpl::ClickErrorDialogGotItForDevtools() { |
| DCHECK(token_error_); |
| OnDismissErrorDialog( |
| config_url_, token_request_status_, token_error_, |
| IdentityRequestDialogController::DismissReason::kGotItButton); |
| } |
| |
| void FederatedAuthRequestImpl::ClickErrorDialogMoreDetailsForDevtools() { |
| DCHECK(token_error_ && token_error_->url.is_valid()); |
| ShowModalDialog(kErrorUrlPopup, config_url_, token_error_->url); |
| OnDismissErrorDialog( |
| config_url_, token_request_status_, token_error_, |
| IdentityRequestDialogController::DismissReason::kMoreDetailsButton); |
| } |
| |
| void FederatedAuthRequestImpl::DismissErrorDialogForDevtools() { |
| OnDismissErrorDialog(config_url_, token_request_status_, token_error_, |
| IdentityRequestDialogController::DismissReason::kOther); |
| } |
| |
| bool FederatedAuthRequestImpl::GetAccountForAutoReauthn( |
| const IdentityProviderData** out_idp_data, |
| const IdentityRequestAccount** out_account) { |
| for (const auto& idp_info : idp_infos_) { |
| if (idp_info.second->data->has_login_status_mismatch) { |
| // If we need to show IDP login status mismatch UI, we cannot |
| // auto-reauthenticate a user even if there really is a single returning |
| // account. |
| return false; |
| } |
| for (const auto& account : idp_info.second->data->accounts) { |
| if (account.login_state == LoginState::kSignUp) { |
| continue; |
| } |
| // account.login_state could be set to kSignIn if the client is on the |
| // `approved_clients` list provided by IDP. However, in this case we have |
| // to trust the browser observed sign-in unless the IDP can be exempted. |
| // For example, they have third party cookies access on the RP site. |
| if (!webid::HasSharingPermissionOrIdpHasThirdPartyCookiesAccess( |
| render_frame_host(), /*provider_url=*/idp_info.first, |
| GetEmbeddingOrigin(), origin(), account.id, permission_delegate_, |
| api_permission_delegate_)) { |
| continue; |
| } |
| |
| if (*out_account) { |
| return false; |
| } |
| *out_idp_data = &(*idp_info.second->data); |
| *out_account = &account; |
| } |
| } |
| |
| if (*out_account) { |
| return true; |
| } |
| |
| return false; |
| } |
| |
| bool FederatedAuthRequestImpl::ShouldFailBeforeFetchingAccounts( |
| const GURL& config_url) { |
| if (mediation_requirement_ != MediationRequirement::kSilent) { |
| return false; |
| } |
| |
| bool is_auto_reauthn_setting_enabled = |
| auto_reauthn_permission_delegate_->IsAutoReauthnSettingEnabled(); |
| if (!is_auto_reauthn_setting_enabled) { |
| render_frame_host().AddMessageToConsole( |
| blink::mojom::ConsoleMessageLevel::kError, |
| "Silent mediation issue: the user has disabled auto re-authn."); |
| } |
| |
| bool is_auto_reauthn_embargoed = |
| auto_reauthn_permission_delegate_->IsAutoReauthnEmbargoed( |
| GetEmbeddingOrigin()); |
| std::optional<base::TimeDelta> time_from_embargo; |
| if (is_auto_reauthn_embargoed) { |
| time_from_embargo = |
| base::Time::Now() - |
| auto_reauthn_permission_delegate_->GetAutoReauthnEmbargoStartTime( |
| GetEmbeddingOrigin()); |
| render_frame_host().AddMessageToConsole( |
| blink::mojom::ConsoleMessageLevel::kError, |
| "Silent mediation issue: auto re-authn is in quiet period because it " |
| "was recently used on this site."); |
| } |
| |
| bool has_sharing_permission_for_any_account = |
| webid::HasSharingPermissionOrIdpHasThirdPartyCookiesAccess( |
| render_frame_host(), config_url, GetEmbeddingOrigin(), origin(), |
| /*account_id=*/std::nullopt, permission_delegate_, |
| api_permission_delegate_); |
| |
| if (!has_sharing_permission_for_any_account) { |
| render_frame_host().AddMessageToConsole( |
| blink::mojom::ConsoleMessageLevel::kError, |
| "Silent mediation issue: the user has not used FedCM on this site with " |
| "this identity provider."); |
| } |
| |
| bool requires_user_mediation = RequiresUserMediation(); |
| if (requires_user_mediation) { |
| render_frame_host().AddMessageToConsole( |
| blink::mojom::ConsoleMessageLevel::kError, |
| "Silent mediation issue: preventSilentAccess() has been invoked on the " |
| "site."); |
| } |
| |
| if (requires_user_mediation || !is_auto_reauthn_setting_enabled || |
| is_auto_reauthn_embargoed || !has_sharing_permission_for_any_account) { |
| // Record the relevant auto reauthn metrics before aborting the FedCM flow. |
| fedcm_metrics_->RecordAutoReauthnMetrics( |
| /*has_single_returning_account=*/std::nullopt, |
| /*auto_signin_account=*/nullptr, |
| /*auto_reauthn_success=*/false, !is_auto_reauthn_setting_enabled, |
| is_auto_reauthn_embargoed, time_from_embargo, requires_user_mediation); |
| return true; |
| } |
| return false; |
| } |
| |
| bool FederatedAuthRequestImpl::RequiresUserMediation() { |
| return auto_reauthn_permission_delegate_->RequiresUserMediation(origin()); |
| } |
| |
| void FederatedAuthRequestImpl::SetRequiresUserMediation( |
| bool requires_user_mediation) { |
| auto_reauthn_permission_delegate_->SetRequiresUserMediation( |
| origin(), requires_user_mediation); |
| } |
| |
| void FederatedAuthRequestImpl::LoginToIdP(bool can_append_hints, |
| const GURL& idp_config_url, |
| GURL login_url) { |
| const auto& it = idp_login_infos_.find(login_url); |
| CHECK(it != idp_login_infos_.end()); |
| if (can_append_hints) { |
| // Before invoking UI, append the query parameters to the `idp_login_url` if |
| // needed. |
| MaybeAppendQueryParameters(it->second, &login_url); |
| } |
| permission_delegate_->AddIdpSigninStatusObserver(this); |
| |
| if (idp_infos_.size() > 1u || |
| IsFedCmUseOtherAccountEnabled(rp_mode_ == RpMode::kButton)) { |
| account_ids_before_login_.clear(); |
| for (const auto& idp_data : idp_data_for_display_) { |
| if (idp_data.idp_metadata.idp_login_url == login_url) { |
| for (const auto& account : idp_data.accounts) { |
| account_ids_before_login_.insert(account.id); |
| } |
| break; |
| } |
| } |
| } |
| |
| login_url_ = login_url; |
| ShowModalDialog(kLoginToIdpPopup, idp_config_url, login_url); |
| } |
| |
| void FederatedAuthRequestImpl::MaybeShowButtonModeModalDialog( |
| const GURL& idp_config_url, |
| const GURL& idp_login_url) { |
| if (IsFedCmMultipleIdentityProvidersEnabled() && idp_infos_.size() > 1) { |
| // TODO(crbug.com/40283218): handle the button flow and the |
| // Multi IdP API (what should happen if you are logged in to some |
| // IdPs but not to others). |
| // TODO(crbug.com/326987150): This is temporary so we should degrade |
| // gracefully. |
| return; |
| } |
| |
| // We fail sooner before, but just to double check, we assert that |
| // we are inside a user gesture here again. |
| CHECK(had_transient_user_activation_); |
| // TODO(crbug.com/40283219): we should probably make idp_login_url |
| // optional instead of empty. |
| LoginToIdP(/*can_append_hints=*/false, idp_config_url, idp_login_url); |
| return; |
| } |
| |
| void FederatedAuthRequestImpl::PreventSilentAccess( |
| PreventSilentAccessCallback callback) { |
| SetRequiresUserMediation(true); |
| if (permission_delegate_->HasSharingPermission(GetEmbeddingOrigin())) { |
| RecordPreventSilentAccess(render_frame_host(), origin(), |
| GetEmbeddingOrigin()); |
| } |
| |
| // Send acknowledge response back. |
| std::move(callback).Run(); |
| } |
| |
| void FederatedAuthRequestImpl::Disconnect( |
| blink::mojom::IdentityCredentialDisconnectOptionsPtr options, |
| DisconnectCallback callback) { |
| MaybeCreateFedCmMetrics(); |
| // FedCMMetrics is used to record disconnect metrics, but does not use the |
| // session_id_, which belongs to token request calls. |
| if (disconnect_request_) { |
| // Since we do not send any fetches in this case, consider the request to be |
| // instant, e.g. duration is 0. |
| render_frame_host().AddMessageToConsole( |
| blink::mojom::ConsoleMessageLevel::kError, |
| webid::GetDisconnectConsoleErrorMessage( |
| FedCmDisconnectStatus::kTooManyRequests)); |
| fedcm_metrics_->RecordDisconnectMetrics( |
| FedCmDisconnectStatus::kTooManyRequests, std::nullopt, |
| render_frame_host(), origin(), GetEmbeddingOrigin(), |
| options->config->config_url, webid::GetNewSessionID()); |
| std::move(callback).Run(DisconnectStatus::kErrorTooManyRequests); |
| return; |
| } |
| |
| bool intercept = false; |
| bool should_complete_request_immediately = false; |
| devtools_instrumentation::WillSendFedCmRequest( |
| render_frame_host(), &intercept, &should_complete_request_immediately); |
| |
| auto network_manager = CreateNetworkManager(); |
| |
| disconnect_request_ = FederatedAuthDisconnectRequest::Create( |
| std::move(network_manager), permission_delegate_, &render_frame_host(), |
| fedcm_metrics_.get(), std::move(options)); |
| FederatedAuthDisconnectRequest* disconnect_request_ptr = |
| disconnect_request_.get(); |
| |
| disconnect_request_ptr->SetCallbackAndStart( |
| base::BindOnce(&FederatedAuthRequestImpl::CompleteDisconnectRequest, |
| weak_ptr_factory_.GetWeakPtr(), std::move(callback)), |
| api_permission_delegate_); |
| } |
| |
| void FederatedAuthRequestImpl::RecordErrorMetrics( |
| IdentityProviderRequestOptionsPtr idp, |
| TokenResponseType token_response_type, |
| std::optional<ErrorDialogType> error_dialog_type, |
| std::optional<ErrorUrlType> error_url_type) { |
| fedcm_metrics_->RecordErrorMetricsBeforeShowingErrorDialog( |
| token_response_type, error_dialog_type, error_url_type, |
| idp->config->config_url); |
| |
| if (error_url_type) { |
| // This is used to determine if we need to use the cross-site specific |
| // devtools issue when failing the request. |
| error_url_type_ = error_url_type; |
| } |
| } |
| |
| void FederatedAuthRequestImpl::MaybeCreateFedCmMetrics() { |
| if (!fedcm_metrics_) { |
| // Ensure the lifecycle state as GetPageUkmSourceId doesn't support the |
| // prerendering page. As FederatedAithRequest runs behind the |
| // BrowserInterfaceBinders, the service doesn't receive any request while |
| // prerendering, and the CHECK should always meet the condition. |
| CHECK(!render_frame_host().IsInLifecycleState( |
| RenderFrameHost::LifecycleState::kPrerendering)); |
| |
| fedcm_metrics_ = std::make_unique<FedCmMetrics>( |
| render_frame_host().GetPageUkmSourceId()); |
| } |
| } |
| |
| } // namespace content |