| // 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 "base/callback.h" |
| #include "base/command_line.h" |
| #include "base/rand_util.h" |
| #include "base/ranges/algorithm.h" |
| #include "base/strings/escape.h" |
| #include "base/strings/string_piece.h" |
| #include "base/task/sequenced_task_runner.h" |
| #include "base/time/time.h" |
| #include "components/url_formatter/elide_url.h" |
| #include "content/browser/bad_message.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_request_page_data.h" |
| #include "content/browser/webid/flags.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_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 "net/base/registry_controlled_domains/registry_controlled_domain.h" |
| #include "net/base/url_util.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" |
| #include "ui/accessibility/ax_mode.h" |
| #include "url/url_constants.h" |
| |
| using blink::mojom::FederatedAuthRequestResult; |
| using blink::mojom::IdentityProviderConfig; |
| using blink::mojom::IdentityProviderConfigPtr; |
| using blink::mojom::IdentityProviderGetParametersPtr; |
| using blink::mojom::LogoutRpsStatus; |
| using blink::mojom::RequestTokenStatus; |
| using FederatedApiPermissionStatus = |
| content::FederatedIdentityApiPermissionContextDelegate::PermissionStatus; |
| using TokenStatus = content::FedCmRequestIdTokenStatus; |
| using SignInStateMatchStatus = content::FedCmSignInStateMatchStatus; |
| using LoginState = content::IdentityRequestAccount::LoginState; |
| using SignInMode = content::IdentityRequestAccount::SignInMode; |
| |
| namespace content { |
| |
| namespace { |
| static constexpr base::TimeDelta kDefaultTokenRequestDelay = base::Seconds(3); |
| static constexpr base::TimeDelta kMaxRejectionTime = base::Seconds(60); |
| |
| std::string ComputeUrlEncodedTokenPostData(const std::string& client_id, |
| const std::string& nonce, |
| const std::string& account_id, |
| bool is_sign_in) { |
| 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 += "&disclosure_text_shown=" + disclosure_text_shown; |
| |
| return query; |
| } |
| |
| std::string GetConsoleErrorMessage(FederatedAuthRequestResult status) { |
| switch (status) { |
| case FederatedAuthRequestResult::kShouldEmbargo: { |
| return "User declined or dismissed prompt. API exponential cool down " |
| "triggered."; |
| } |
| case FederatedAuthRequestResult::kErrorDisabledInSettings: { |
| return "Third-party sign in was disabled in browser Site Settings."; |
| } |
| case FederatedAuthRequestResult::kErrorTooManyRequests: { |
| return "Only one navigator.credentials.get request may be outstanding at " |
| "one time."; |
| } |
| case FederatedAuthRequestResult::kErrorFetchingWellKnownHttpNotFound: { |
| return "The provider's FedCM well-known file cannot be found."; |
| } |
| case FederatedAuthRequestResult::kErrorFetchingWellKnownNoResponse: { |
| return "The provider's FedCM well-known file fetch resulted in an " |
| "error response code."; |
| } |
| case FederatedAuthRequestResult::kErrorFetchingWellKnownInvalidResponse: { |
| return "Provider's FedCM well-known file is invalid."; |
| } |
| case FederatedAuthRequestResult::kErrorConfigNotInWellKnown: { |
| return "Provider's FedCM config file not listed in its well-known file."; |
| } |
| case FederatedAuthRequestResult::kErrorWellKnownTooBig: { |
| return "Provider's FedCM well-known file contains too many config URLs."; |
| } |
| case FederatedAuthRequestResult::kErrorFetchingConfigHttpNotFound: { |
| return "The provider's FedCM config file cannot be found."; |
| } |
| case FederatedAuthRequestResult::kErrorFetchingConfigNoResponse: { |
| return "The provider's FedCM config file fetch resulted in an " |
| "error response code."; |
| } |
| case FederatedAuthRequestResult::kErrorFetchingConfigInvalidResponse: { |
| return "Provider's FedCM config file is invalid."; |
| } |
| case FederatedAuthRequestResult::kErrorFetchingClientMetadataHttpNotFound: { |
| return "The provider's client metadata endpoint cannot be found."; |
| } |
| case FederatedAuthRequestResult::kErrorFetchingClientMetadataNoResponse: { |
| return "The provider's client metadata fetch resulted in an error " |
| "response code."; |
| } |
| case FederatedAuthRequestResult:: |
| kErrorFetchingClientMetadataInvalidResponse: { |
| return "Provider's client metadata is invalid."; |
| } |
| case FederatedAuthRequestResult::kErrorFetchingAccountsHttpNotFound: { |
| return "The provider's accounts list endpoint cannot be found."; |
| } |
| case FederatedAuthRequestResult::kErrorFetchingAccountsNoResponse: { |
| return "The provider's accounts list fetch resulted in an error response " |
| "code."; |
| } |
| case FederatedAuthRequestResult::kErrorFetchingAccountsInvalidResponse: { |
| return "Provider's accounts list is invalid. Should have received an " |
| "\"accounts\" list, where each account must have at least \"id\", " |
| "\"name\", and \"email\"."; |
| } |
| case FederatedAuthRequestResult::kErrorFetchingIdTokenHttpNotFound: { |
| return "The provider's id token endpoint cannot be found."; |
| } |
| case FederatedAuthRequestResult::kErrorFetchingIdTokenNoResponse: { |
| return "The provider's token fetch resulted in an error response " |
| "code."; |
| } |
| case FederatedAuthRequestResult::kErrorFetchingIdTokenInvalidResponse: { |
| return "Provider's token is invalid."; |
| } |
| case FederatedAuthRequestResult::kErrorCanceled: { |
| return "The request has been aborted."; |
| } |
| case FederatedAuthRequestResult::kErrorRpPageNotVisible: { |
| return "RP page is not visible."; |
| } |
| case FederatedAuthRequestResult::kError: { |
| return "Error retrieving a token."; |
| } |
| case FederatedAuthRequestResult::kSuccess: { |
| DCHECK(false); |
| return ""; |
| } |
| } |
| } |
| |
| 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::kErrorConfigNotInWellKnown: |
| case FederatedAuthRequestResult::kErrorWellKnownTooBig: |
| case FederatedAuthRequestResult::kErrorFetchingConfigHttpNotFound: |
| case FederatedAuthRequestResult::kErrorFetchingConfigNoResponse: |
| case FederatedAuthRequestResult::kErrorFetchingConfigInvalidResponse: |
| case FederatedAuthRequestResult::kErrorFetchingClientMetadataHttpNotFound: |
| case FederatedAuthRequestResult::kErrorFetchingClientMetadataNoResponse: |
| case FederatedAuthRequestResult:: |
| kErrorFetchingClientMetadataInvalidResponse: |
| case FederatedAuthRequestResult::kErrorFetchingAccountsHttpNotFound: |
| case FederatedAuthRequestResult::kErrorFetchingAccountsNoResponse: |
| case FederatedAuthRequestResult::kErrorFetchingAccountsInvalidResponse: |
| case FederatedAuthRequestResult::kErrorFetchingIdTokenHttpNotFound: |
| case FederatedAuthRequestResult::kErrorFetchingIdTokenNoResponse: |
| case FederatedAuthRequestResult::kErrorFetchingIdTokenInvalidResponse: |
| case FederatedAuthRequestResult::kErrorRpPageNotVisible: |
| 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::kErrorCanceled: { |
| return IdpNetworkRequestManager::MetricsEndpointErrorCode::kRpFailure; |
| } |
| case FederatedAuthRequestResult::kErrorFetchingAccountsInvalidResponse: { |
| return IdpNetworkRequestManager::MetricsEndpointErrorCode:: |
| kAccountsEndpointInvalidResponse; |
| } |
| case FederatedAuthRequestResult::kErrorFetchingIdTokenInvalidResponse: { |
| return IdpNetworkRequestManager::MetricsEndpointErrorCode:: |
| kTokenEndpointInvalidResponse; |
| } |
| case FederatedAuthRequestResult::kShouldEmbargo: |
| case FederatedAuthRequestResult::kErrorDisabledInSettings: |
| case FederatedAuthRequestResult::kErrorRpPageNotVisible: { |
| 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::kErrorFetchingWellKnownInvalidResponse: |
| case FederatedAuthRequestResult::kErrorFetchingConfigInvalidResponse: |
| case FederatedAuthRequestResult:: |
| kErrorFetchingClientMetadataInvalidResponse: { |
| return IdpNetworkRequestManager::MetricsEndpointErrorCode:: |
| kIdpServerInvalidResponse; |
| } |
| case FederatedAuthRequestResult::kError: { |
| return IdpNetworkRequestManager::MetricsEndpointErrorCode::kOther; |
| } |
| } |
| } |
| |
| // TODO(crbug.com/1344150): Use normal distribution after sufficient data is |
| // collected. |
| base::TimeDelta GetRandomRejectionTime() { |
| return kMaxRejectionTime * base::RandDouble(); |
| } |
| |
| std::string FormatUrlForDisplay(const GURL& url) { |
| // We do not use url_formatter::FormatUrlForSecurityDisplay() directly because |
| // our UI intentionally shows only the eTLD+1, as it makes for a shorter text |
| // that is also clearer to users. The identity provider's well-known file is |
| // in the root of the eTLD+1, and sign-in status within identity provider and |
| // relying party can be domain-wide because it relies on cookies. |
| std::string formatted_url_str = |
| net::IsLocalhost(url) |
| ? url.host() |
| : net::registry_controlled_domains::GetDomainAndRegistry( |
| url, |
| net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES); |
| return base::UTF16ToUTF8(url_formatter::FormatUrlForSecurityDisplay( |
| GURL(url.scheme() + "://" + formatted_url_str), |
| url_formatter::SchemeDisplay::OMIT_HTTP_AND_HTTPS)); |
| } |
| |
| std::string FormatOriginForDisplay(const url::Origin& origin) { |
| return FormatUrlForDisplay(origin.GetURL()); |
| } |
| |
| bool ShouldFailBecauseNotSignedInWithIdp( |
| const GURL& idp_url, |
| FederatedIdentityPermissionContextDelegate* permission_delegate) { |
| if (GetFedCmIdpSigninStatusMode() == FedCmIdpSigninStatusMode::DISABLED) |
| return false; |
| |
| const url::Origin idp_origin = url::Origin::Create(idp_url); |
| const absl::optional<bool> idp_signin_status = |
| permission_delegate->GetIdpSigninStatus(idp_origin); |
| return !idp_signin_status.value_or(true); |
| } |
| |
| FederatedAuthRequestPageData* GetPageData(RenderFrameHost* render_frame_host) { |
| return FederatedAuthRequestPageData::GetOrCreateForPage( |
| render_frame_host->GetPage()); |
| } |
| |
| } // namespace |
| |
| FederatedAuthRequestImpl::IdentityProviderGetInfo::IdentityProviderGetInfo( |
| blink::mojom::IdentityProviderConfig provider, |
| bool prefer_auto_signin) |
| : provider(std::move(provider)), prefer_auto_signin(prefer_auto_signin) {} |
| |
| FederatedAuthRequestImpl::IdentityProviderGetInfo::~IdentityProviderGetInfo() = |
| default; |
| FederatedAuthRequestImpl::IdentityProviderGetInfo::IdentityProviderGetInfo( |
| const IdentityProviderGetInfo&) = default; |
| |
| FederatedAuthRequestImpl::IdentityProviderInfo::IdentityProviderInfo( |
| blink::mojom::IdentityProviderConfig provider, |
| IdpNetworkRequestManager::Endpoints endpoints, |
| IdentityProviderMetadata metadata, |
| bool prefer_auto_signin) |
| : provider(std::move(provider)), |
| endpoints(std::move(endpoints)), |
| metadata(std::move(metadata)), |
| prefer_auto_signin(prefer_auto_signin) {} |
| |
| FederatedAuthRequestImpl::IdentityProviderInfo::~IdentityProviderInfo() = |
| default; |
| FederatedAuthRequestImpl::IdentityProviderInfo::IdentityProviderInfo( |
| const IdentityProviderInfo&) = default; |
| |
| FederatedAuthRequestImpl::FederatedAuthRequestImpl( |
| RenderFrameHost& host, |
| FederatedIdentityApiPermissionContextDelegate* api_permission_context, |
| FederatedIdentityPermissionContextDelegate* permission_context, |
| mojo::PendingReceiver<blink::mojom::FederatedAuthRequest> receiver) |
| : DocumentService(host, std::move(receiver)), |
| api_permission_delegate_(api_permission_context), |
| permission_delegate_(permission_context), |
| token_request_delay_(kDefaultTokenRequestDelay) {} |
| |
| FederatedAuthRequestImpl::~FederatedAuthRequestImpl() { |
| // Ensures key data members are destructed in proper order and resolves any |
| // pending promise. |
| if (auth_request_callback_) { |
| DCHECK(!logout_callback_); |
| CompleteRequestWithError(FederatedAuthRequestResult::kError, |
| TokenStatus::kUnhandledRequest, |
| /*should_delay_callback=*/false); |
| } |
| if (logout_callback_) { |
| // We do not complete the logout request, so unset the |
| // PendingWebIdentityRequest on the Page so that other frames in the |
| // same Page may still trigger new requests after the current |
| // RenderFrameHost is destroyed. |
| GetPageData(&render_frame_host())->SetHasPendingWebIdentityRequest(false); |
| } |
| } |
| |
| // 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<FederatedIdentityPermissionContextDelegate> permission_context = |
| browser_context->GetFederatedIdentityPermissionContext(); |
| if (!api_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, |
| permission_context, std::move(receiver)); |
| } |
| |
| FederatedAuthRequestImpl& FederatedAuthRequestImpl::CreateForTesting( |
| RenderFrameHost& host, |
| FederatedIdentityApiPermissionContextDelegate* api_permission_context, |
| FederatedIdentityPermissionContextDelegate* permission_context, |
| mojo::PendingReceiver<blink::mojom::FederatedAuthRequest> receiver) { |
| return *new FederatedAuthRequestImpl(host, api_permission_context, |
| permission_context, std::move(receiver)); |
| } |
| |
| void FederatedAuthRequestImpl::RequestToken( |
| std::vector<IdentityProviderGetParametersPtr> idp_get_params_ptrs, |
| 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()) { |
| mojo::ReportBadMessage("idp_get_params_ptrs is empty."); |
| 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, absl::nullopt, ""); |
| return; |
| } |
| |
| // Check that providers are non-empty. |
| for (auto& idp_get_params_ptr : idp_get_params_ptrs) { |
| if (idp_get_params_ptr->providers.size() == 0) { |
| std::move(callback).Run(RequestTokenStatus::kError, absl::nullopt, ""); |
| return; |
| } |
| } |
| |
| if (!fedcm_metrics_) { |
| // TODO(crbug.com/1307709): Handle FedCmMetrics for multiple IDPs. |
| fedcm_metrics_ = std::make_unique<FedCmMetrics>( |
| idp_get_params_ptrs[0]->providers[0]->config_url, |
| render_frame_host().GetPageUkmSourceId(), base::RandInt(1, 1 << 30), |
| /*is_disabled=*/idp_get_params_ptrs.size() > 1); |
| } |
| |
| if (HasPendingRequest()) { |
| fedcm_metrics_->RecordRequestTokenStatus(TokenStatus::kTooManyRequests); |
| std::move(callback).Run(RequestTokenStatus::kErrorTooManyRequests, |
| absl::nullopt, ""); |
| return; |
| } |
| |
| auth_request_callback_ = std::move(callback); |
| GetPageData(&render_frame_host())->SetHasPendingWebIdentityRequest(true); |
| network_manager_ = CreateNetworkManager(); |
| request_dialog_controller_ = CreateDialogController(); |
| start_time_ = base::TimeTicks::Now(); |
| |
| FederatedApiPermissionStatus permission_status = |
| api_permission_delegate_->GetApiPermissionStatus(GetEmbeddingOrigin()); |
| |
| absl::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_THIRD_PARTY_COOKIES_BLOCKED: |
| error_token_status = TokenStatus::kThirdPartyCookiesBlocked; |
| 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) { |
| CompleteRequestWithError(request_result, *error_token_status, |
| /*should_delay_callback=*/true); |
| return; |
| } |
| |
| std::set<GURL> pending_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 = |
| pending_idps.insert(idp_ptr->config_url).second; |
| if (!is_unique_idp) { |
| CompleteRequestWithError(FederatedAuthRequestResult::kError, |
| /*token_status=*/absl::nullopt, |
| /*should_delay_callback=*/false); |
| return; |
| } |
| |
| if (!network::IsOriginPotentiallyTrustworthy( |
| url::Origin::Create(idp_ptr->config_url))) { |
| CompleteRequestWithError(FederatedAuthRequestResult::kError, |
| TokenStatus::kIdpNotPotentiallyTrustworthy, |
| /*should_delay_callback=*/false); |
| return; |
| } |
| |
| // TODO(crbug.com/1382545): Handle ShouldFailIfNotSignedInWithIdp in the |
| // multi IDP use case. |
| bool has_failing_idp_signin_status = ShouldFailBecauseNotSignedInWithIdp( |
| idp_ptr->config_url, permission_delegate_); |
| |
| if (has_failing_idp_signin_status && |
| GetFedCmIdpSigninStatusMode() == FedCmIdpSigninStatusMode::ENABLED) { |
| CompleteRequestWithError(FederatedAuthRequestResult::kError, |
| TokenStatus::kNotSignedInWithIdp, |
| /*should_delay_callback=*/true); |
| return; |
| } |
| } |
| } |
| CHECK(pending_idps_.empty()); |
| pending_idps_ = std::move(pending_idps); |
| |
| base::flat_map<GURL, IdentityProviderGetInfo> get_infos; |
| |
| 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_url); |
| get_infos.emplace(idp_ptr->config_url, |
| IdentityProviderGetInfo( |
| *idp_ptr, idp_get_params_ptr->prefer_auto_sign_in && |
| IsFedCmAutoSigninEnabled())); |
| } |
| } |
| |
| int icon_ideal_size = request_dialog_controller_->GetBrandIconIdealSize(); |
| int icon_minimum_size = request_dialog_controller_->GetBrandIconMinimumSize(); |
| std::unique_ptr<FederatedProviderFetcher> provider_fetcher = |
| std::make_unique<FederatedProviderFetcher>(network_manager_.get()); |
| |
| // FederatedProviderFetcher is passed as a parameter of |
| // OnAllConfigAndWellKnownFetched() so that FederatedProviderFetcher is |
| // destroyed when FederatedAuthRequestImpl is destroyed. |
| FederatedProviderFetcher* provider_fetcher_ptr = provider_fetcher.get(); |
| provider_fetcher_ptr->Start( |
| idp_order_, icon_ideal_size, icon_minimum_size, |
| base::BindOnce(&FederatedAuthRequestImpl::OnAllConfigAndWellKnownFetched, |
| weak_ptr_factory_.GetWeakPtr(), |
| std::move(provider_fetcher), std::move(get_infos))); |
| } |
| |
| void FederatedAuthRequestImpl::CancelTokenRequest() { |
| if (!auth_request_callback_) |
| return; |
| |
| // Dialog will be hidden by the destructor for request_dialog_controller_, |
| // triggered by CompleteRequest. |
| |
| CompleteRequestWithError(FederatedAuthRequestResult::kErrorCanceled, |
| TokenStatus::kAborted, |
| /*should_delay_callback=*/false); |
| } |
| |
| // TODO(kenrb): Depending on how this code evolves, it might make sense to |
| // spin session management code into its own service. The prohibition on |
| // making authentication requests and logout requests at the same time, while |
| // not problematic for any plausible use case, need not be strictly necessary |
| // if there is a good way to not have to resource contention between requests. |
| // https://crbug.com/1200581 |
| void FederatedAuthRequestImpl::LogoutRps( |
| std::vector<blink::mojom::LogoutRpsRequestPtr> logout_requests, |
| LogoutRpsCallback callback) { |
| if (HasPendingRequest()) { |
| std::move(callback).Run(LogoutRpsStatus::kErrorTooManyRequests); |
| return; |
| } |
| |
| DCHECK(logout_requests_.empty()); |
| |
| logout_callback_ = std::move(callback); |
| GetPageData(&render_frame_host())->SetHasPendingWebIdentityRequest(true); |
| |
| if (logout_requests.empty()) { |
| CompleteLogoutRequest(LogoutRpsStatus::kError); |
| return; |
| } |
| |
| if (base::ranges::any_of(logout_requests, [](auto& request) { |
| return !request->url.is_valid(); |
| })) { |
| bad_message::ReceivedBadMessage(render_frame_host().GetProcess(), |
| bad_message::FARI_LOGOUT_BAD_ENDPOINT); |
| CompleteLogoutRequest(LogoutRpsStatus::kError); |
| return; |
| } |
| |
| for (auto& request : logout_requests) { |
| logout_requests_.push(std::move(request)); |
| } |
| |
| if (!network::IsOriginPotentiallyTrustworthy(origin())) { |
| CompleteLogoutRequest(LogoutRpsStatus::kError); |
| return; |
| } |
| |
| network_manager_ = CreateNetworkManager(); |
| |
| if (!IsFedCmIdpSignoutEnabled()) { |
| CompleteLogoutRequest(LogoutRpsStatus::kError); |
| return; |
| } |
| |
| if (api_permission_delegate_->GetApiPermissionStatus(GetEmbeddingOrigin()) != |
| FederatedApiPermissionStatus::GRANTED) { |
| CompleteLogoutRequest(LogoutRpsStatus::kError); |
| return; |
| } |
| |
| // TODO(kenrb): These should be parallelized rather than being dispatched |
| // serially. https://crbug.com/1200581. |
| DispatchOneLogout(); |
| } |
| |
| void FederatedAuthRequestImpl::SetIdpSigninStatus( |
| const url::Origin& idp_origin, |
| blink::mojom::IdpSigninStatus status) { |
| // We only allow setting the IDP signin status when the subresource is |
| // loaded from the same origin as the document. This is to protect from |
| // an RP embedding a tracker resource that would set this signin status |
| // for the tracker, enabling the FedCM request. |
| // This behavior may change in https://crbug.com/1382193 |
| if (!origin().IsSameOriginWith(idp_origin)) |
| return; |
| permission_delegate_->SetIdpSigninStatus( |
| idp_origin, status == blink::mojom::IdpSigninStatus::kSignedIn); |
| } |
| |
| bool FederatedAuthRequestImpl::HasPendingRequest() const { |
| bool has_pending_request = |
| GetPageData(&render_frame_host())->HasPendingWebIdentityRequest(); |
| DCHECK(has_pending_request || (!auth_request_callback_ && !logout_callback_)); |
| return has_pending_request; |
| } |
| |
| void FederatedAuthRequestImpl::OnAllConfigAndWellKnownFetched( |
| std::unique_ptr<FederatedProviderFetcher> provider_fetcher, |
| base::flat_map<GURL, IdentityProviderGetInfo> get_infos, |
| std::vector<FederatedProviderFetcher::FetchResult> fetch_results) { |
| for (const FederatedProviderFetcher::FetchResult& fetch_result : |
| fetch_results) { |
| const GURL& identity_provider_config_url = |
| fetch_result.identity_provider_config_url; |
| auto get_info_it = get_infos.find(identity_provider_config_url); |
| CHECK(get_info_it != get_infos.end()); |
| |
| metrics_endpoints_[identity_provider_config_url] = |
| fetch_result.endpoints.metrics; |
| |
| std::unique_ptr<IdentityProviderInfo> idp_info = |
| std::make_unique<IdentityProviderInfo>( |
| std::move(get_info_it->second.provider), |
| std::move(fetch_result.endpoints), |
| fetch_result.metadata ? std::move(*fetch_result.metadata) |
| : IdentityProviderMetadata(), |
| get_info_it->second.prefer_auto_signin); |
| |
| 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=*/true); |
| continue; |
| } |
| |
| // 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. |
| idp_info->has_failing_idp_signin_status = |
| ShouldFailBecauseNotSignedInWithIdp(identity_provider_config_url, |
| permission_delegate_); |
| if (idp_info->has_failing_idp_signin_status && |
| GetFedCmIdpSigninStatusMode() == FedCmIdpSigninStatusMode::ENABLED) { |
| // 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::kError, |
| TokenStatus::kNotSignedInWithIdp, |
| /*should_delay_callback=*/true); |
| continue; |
| } |
| |
| GURL accounts_endpoint = idp_info->endpoints.accounts; |
| std::string client_id = idp_info->provider.client_id; |
| network_manager_->SendAccountsRequest( |
| accounts_endpoint, client_id, |
| base::BindOnce(&FederatedAuthRequestImpl::OnAccountsResponseReceived, |
| weak_ptr_factory_.GetWeakPtr(), std::move(idp_info))); |
| } |
| } |
| |
| void FederatedAuthRequestImpl::OnClientMetadataResponseReceived( |
| std::unique_ptr<IdentityProviderInfo> idp_info, |
| const IdpNetworkRequestManager::AccountList& accounts, |
| IdpNetworkRequestManager::FetchStatus status, |
| IdpNetworkRequestManager::ClientMetadata client_metadata) { |
| // TODO(yigu): Clean up the client metadata related errors for metrics and |
| // console logs. |
| OnFetchDataForIdpSucceeded(std::move(idp_info), accounts, client_metadata); |
| } |
| |
| void FederatedAuthRequestImpl::OnFetchDataForIdpSucceeded( |
| std::unique_ptr<IdentityProviderInfo> idp_info, |
| const IdpNetworkRequestManager::AccountList& accounts, |
| const IdpNetworkRequestManager::ClientMetadata& client_metadata) { |
| const GURL& idp_config_url = idp_info->provider.config_url; |
| const std::string idp_for_display = FormatUrlForDisplay(idp_config_url); |
| idp_info->data = IdentityProviderData( |
| idp_for_display, accounts, idp_info->metadata, |
| ClientMetadata{GURL(client_metadata.terms_of_service_url), |
| GURL(client_metadata.privacy_policy_url)}); |
| idp_infos_[idp_config_url] = std::move(idp_info); |
| |
| pending_idps_.erase(idp_config_url); |
| MaybeShowAccountsDialog(); |
| } |
| |
| void FederatedAuthRequestImpl::OnFetchDataForIdpFailed( |
| const std::unique_ptr<IdentityProviderInfo> idp_info, |
| blink::mojom::FederatedAuthRequestResult result, |
| absl::optional<TokenStatus> token_status, |
| bool should_delay_callback) { |
| const GURL& idp_config_url = idp_info->provider.config_url; |
| if (idp_order_.size() == 1u) { |
| CompleteRequestWithError(result, token_status, should_delay_callback); |
| return; |
| } |
| |
| AddInspectorIssue(result); |
| AddConsoleErrorMessage(result); |
| |
| if (IsFedCmMetricsEndpointEnabled()) |
| SendFailedTokenRequestMetrics(idp_info->endpoints.metrics, result); |
| |
| pending_idps_.erase(idp_config_url); |
| metrics_endpoints_.erase(idp_config_url); |
| std::vector<GURL>::iterator idp_order_new_end_it = |
| std::remove(idp_order_.begin(), idp_order_.end(), idp_config_url); |
| idp_order_.erase(idp_order_new_end_it, idp_order_.end()); |
| |
| idp_infos_.erase(idp_config_url); |
| // Do not use `idp_config_url` after this line because the reference is no |
| // longer valid. |
| |
| MaybeShowAccountsDialog(); |
| } |
| |
| void FederatedAuthRequestImpl::MaybeShowAccountsDialog() { |
| if (!pending_idps_.empty()) |
| return; |
| |
| bool is_visible = (render_frame_host().IsActive() && |
| render_frame_host().GetVisibilityState() == |
| content::PageVisibilityState::kVisible); |
| fedcm_metrics_->RecordWebContentsVisibilityUponReadyToShowDialog(is_visible); |
| // Does not show the dialog if the user has left the page. e.g. they may |
| // open a new tab before browser is ready to show the dialog. |
| if (!is_visible) { |
| CompleteRequestWithError(FederatedAuthRequestResult::kErrorRpPageNotVisible, |
| TokenStatus::kRpPageNotVisible, |
| /*should_delay_callback=*/true); |
| return; |
| } |
| |
| show_accounts_dialog_time_ = base::TimeTicks::Now(); |
| fedcm_metrics_->RecordShowAccountsDialogTime(show_accounts_dialog_time_ - |
| start_time_); |
| std::string rp_url_for_display = FormatOriginForDisplay(GetEmbeddingOrigin()); |
| |
| bool prefer_auto_signin = true; |
| std::vector<IdentityProviderData> idp_data_for_display; |
| 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); |
| prefer_auto_signin &= idp_info_it->second->prefer_auto_signin; |
| } |
| } |
| |
| WebContents* rp_web_contents = |
| WebContents::FromRenderFrameHost(&render_frame_host()); |
| DCHECK(render_frame_host().GetMainFrame()->IsInPrimaryMainFrame()); |
| |
| bool screen_reader_is_on = rp_web_contents->GetAccessibilityMode().has_mode( |
| ui::AXMode::kScreenReader); |
| // Auto signs in returning users if they have a single account and are |
| // signing in. |
| // TODO(yigu): Add additional controls for RP/IDP/User for this flow. |
| // https://crbug.com/1236678. |
| bool is_auto_sign_in = |
| prefer_auto_signin && !screen_reader_is_on && |
| idp_data_for_display.size() == 1 && |
| idp_data_for_display[0].accounts.size() == 1 && |
| idp_data_for_display[0].accounts[0].login_state == LoginState::kSignIn; |
| |
| // TODO(crbug.com/1382863): Handle UI where some IDPs are successful and some |
| // IDPs are failing in the multi IDP case. |
| request_dialog_controller_->ShowAccountsDialog( |
| rp_web_contents, rp_url_for_display, idp_data_for_display, |
| is_auto_sign_in ? SignInMode::kAuto : SignInMode::kExplicit, |
| base::BindOnce(&FederatedAuthRequestImpl::OnAccountSelected, |
| weak_ptr_factory_.GetWeakPtr()), |
| base::BindOnce(&FederatedAuthRequestImpl::OnDialogDismissed, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void FederatedAuthRequestImpl::HandleAccountsFetchFailure( |
| std::unique_ptr<IdentityProviderInfo> idp_info, |
| blink::mojom::FederatedAuthRequestResult result, |
| absl::optional<TokenStatus> token_status) { |
| if (GetFedCmIdpSigninStatusMode() == FedCmIdpSigninStatusMode::DISABLED) { |
| OnFetchDataForIdpFailed(std::move(idp_info), result, token_status, |
| /*should_delay_callback=*/true); |
| return; |
| } |
| |
| url::Origin idp_origin = url::Origin::Create(idp_info->provider.config_url); |
| const absl::optional<bool> idp_signin_status = |
| permission_delegate_->GetIdpSigninStatus(idp_origin); |
| |
| // Ensures that we only fetch accounts unconditionally once. |
| permission_delegate_->SetIdpSigninStatus(idp_origin, false); |
| |
| if (!idp_signin_status.has_value() || |
| GetFedCmIdpSigninStatusMode() == FedCmIdpSigninStatusMode::METRICS_ONLY) { |
| OnFetchDataForIdpFailed(std::move(idp_info), result, token_status, |
| /*should_delay_callback=*/true); |
| return; |
| } |
| |
| // TODO(crbug.com/1357790): we should figure out how to handle multiple IDP |
| // w.r.t. showing a static failure UI. e.g. one IDP is always successful and |
| // one always returns 404. |
| WebContents* rp_web_contents = |
| WebContents::FromRenderFrameHost(&render_frame_host()); |
| DCHECK(render_frame_host().GetMainFrame()->IsInPrimaryMainFrame()); |
| |
| // TODO(crbug.com/1382495): Handle failure UI in the multi IDP case. |
| request_dialog_controller_->ShowFailureDialog( |
| rp_web_contents, FormatOriginForDisplay(GetEmbeddingOrigin()), |
| FormatOriginForDisplay(idp_origin), |
| base::BindOnce( |
| &FederatedAuthRequestImpl::OnDismissFailureDialog, |
| weak_ptr_factory_.GetWeakPtr(), FederatedAuthRequestResult::kError, |
| TokenStatus::kNotSignedInWithIdp, /*should_delay_callback=*/true)); |
| } |
| |
| void FederatedAuthRequestImpl::OnAccountsResponseReceived( |
| std::unique_ptr<IdentityProviderInfo> idp_info, |
| IdpNetworkRequestManager::FetchStatus status, |
| IdpNetworkRequestManager::AccountList accounts) { |
| url::Origin idp_origin = url::Origin::Create(idp_info->provider.config_url); |
| |
| if (GetFedCmIdpSigninStatusMode() != FedCmIdpSigninStatusMode::DISABLED) { |
| // Record metrics on effect of IDP sign-in status API. |
| const absl::optional<bool> idp_signin_status = |
| permission_delegate_->GetIdpSigninStatus(idp_origin); |
| fedcm_metrics_->RecordIdpSigninMatchStatus(idp_signin_status, |
| status.parse_status); |
| } |
| |
| constexpr char kAccountsUrl[] = "accounts endpoint"; |
| switch (status.parse_status) { |
| case IdpNetworkRequestManager::ParseStatus::kHttpNotFoundError: { |
| MaybeAddResponseCodeToConsole(kAccountsUrl, status.response_code); |
| HandleAccountsFetchFailure( |
| std::move(idp_info), |
| FederatedAuthRequestResult::kErrorFetchingAccountsHttpNotFound, |
| TokenStatus::kAccountsHttpNotFound); |
| return; |
| } |
| case IdpNetworkRequestManager::ParseStatus::kNoResponseError: { |
| MaybeAddResponseCodeToConsole(kAccountsUrl, status.response_code); |
| HandleAccountsFetchFailure( |
| std::move(idp_info), |
| FederatedAuthRequestResult::kErrorFetchingAccountsNoResponse, |
| TokenStatus::kAccountsNoResponse); |
| return; |
| } |
| case IdpNetworkRequestManager::ParseStatus::kInvalidResponseError: { |
| MaybeAddResponseCodeToConsole(kAccountsUrl, status.response_code); |
| HandleAccountsFetchFailure( |
| std::move(idp_info), |
| FederatedAuthRequestResult::kErrorFetchingAccountsInvalidResponse, |
| TokenStatus::kAccountsInvalidResponse); |
| return; |
| } |
| case IdpNetworkRequestManager::ParseStatus::kSuccess: { |
| ComputeLoginStateAndReorderAccounts(idp_info->provider, accounts); |
| |
| if (!idp_info->has_failing_idp_signin_status) { |
| // This scenario occurs in FedCmIdpSigninStatusMode::METRICS_ONLY mode. |
| // Don't set the IDP sign-in status because we would not get here in |
| // FedCmIdpSigninStatusMode::ENABLED mode. |
| permission_delegate_->SetIdpSigninStatus(idp_origin, true); |
| } |
| |
| bool need_client_metadata = false; |
| 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::IsEndpointUrlValid(idp_info->provider.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.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 IdentityProviderConfig& idp, |
| IdpNetworkRequestManager::AccountList& accounts) { |
| // 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(), url::Origin::Create(idp.config_url), |
| account.id); |
| |
| if (idp_claimed_sign_in == browser_observed_sign_in) { |
| fedcm_metrics_->RecordSignInStateMatchStatus( |
| SignInStateMatchStatus::kMatch); |
| } else if (idp_claimed_sign_in) { |
| fedcm_metrics_->RecordSignInStateMatchStatus( |
| SignInStateMatchStatus::kIdpClaimedSignIn); |
| } else { |
| fedcm_metrics_->RecordSignInStateMatchStatus( |
| 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; |
| } |
| |
| // 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. |
| if (api_permission_delegate_->GetApiPermissionStatus(GetEmbeddingOrigin()) != |
| FederatedApiPermissionStatus::GRANTED) { |
| CompleteRequestWithError( |
| FederatedAuthRequestResult::kErrorDisabledInSettings, |
| TokenStatus::kDisabledInSettings, |
| /*should_delay_callback=*/true); |
| return; |
| } |
| |
| fedcm_metrics_->RecordIsSignInUser(is_sign_in); |
| |
| api_permission_delegate_->RemoveEmbargoAndResetCounts(GetEmbeddingOrigin()); |
| |
| account_id_ = account_id; |
| select_account_time_ = base::TimeTicks::Now(); |
| fedcm_metrics_->RecordContinueOnDialogTime(select_account_time_ - |
| show_accounts_dialog_time_); |
| |
| network_manager_->SendTokenRequest( |
| idp_info.endpoints.token, account_id_, |
| ComputeUrlEncodedTokenPostData(idp_info.provider.client_id, |
| idp_info.provider.nonce, account_id, |
| is_sign_in), |
| base::BindOnce(&FederatedAuthRequestImpl::OnTokenResponseReceived, |
| weak_ptr_factory_.GetWeakPtr(), idp_info.provider)); |
| } |
| |
| void FederatedAuthRequestImpl::OnDismissFailureDialog( |
| blink::mojom::FederatedAuthRequestResult result, |
| absl::optional<TokenStatus> token_status, |
| bool should_delay_callback, |
| IdentityRequestDialogController::DismissReason dismiss_reason) { |
| CompleteRequestWithError(result, token_status, should_delay_callback); |
| } |
| |
| void FederatedAuthRequestImpl::OnDialogDismissed( |
| 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. |
| bool should_embargo = false; |
| switch (dismiss_reason) { |
| case IdentityRequestDialogController::DismissReason::CLOSE_BUTTON: |
| case IdentityRequestDialogController::DismissReason::SWIPE: |
| should_embargo = true; |
| break; |
| default: |
| break; |
| } |
| |
| if (should_embargo) { |
| base::TimeTicks dismiss_dialog_time = base::TimeTicks::Now(); |
| fedcm_metrics_->RecordCancelOnDialogTime(dismiss_dialog_time - |
| show_accounts_dialog_time_); |
| } |
| fedcm_metrics_->RecordCancelReason(dismiss_reason); |
| |
| 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, |
| /*should_delay_callback=*/false); |
| } |
| |
| void FederatedAuthRequestImpl::OnTokenResponseReceived( |
| const IdentityProviderConfig& idp, |
| IdpNetworkRequestManager::FetchStatus status, |
| const std::string& id_token) { |
| // 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 |
| // |token_request_delay_| seconds for better UX. |
| token_response_time_ = base::TimeTicks::Now(); |
| base::TimeDelta fetch_time = token_response_time_ - select_account_time_; |
| if (ShouldCompleteRequestImmediately() || |
| fetch_time >= token_request_delay_) { |
| CompleteTokenRequest(idp, status, id_token); |
| return; |
| } |
| |
| base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask( |
| FROM_HERE, |
| base::BindOnce(&FederatedAuthRequestImpl::CompleteTokenRequest, |
| weak_ptr_factory_.GetWeakPtr(), idp, status, id_token), |
| token_request_delay_ - fetch_time); |
| } |
| |
| void FederatedAuthRequestImpl::CompleteTokenRequest( |
| const IdentityProviderConfig& idp, |
| IdpNetworkRequestManager::FetchStatus status, |
| const std::string& token) { |
| 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, |
| /*should_delay_callback=*/true); |
| return; |
| } |
| case IdpNetworkRequestManager::ParseStatus::kNoResponseError: { |
| MaybeAddResponseCodeToConsole(kIdAssertionUrl, status.response_code); |
| CompleteRequestWithError( |
| FederatedAuthRequestResult::kErrorFetchingIdTokenNoResponse, |
| TokenStatus::kIdTokenNoResponse, |
| /*should_delay_callback=*/true); |
| return; |
| } |
| case IdpNetworkRequestManager::ParseStatus::kInvalidResponseError: { |
| MaybeAddResponseCodeToConsole(kIdAssertionUrl, status.response_code); |
| CompleteRequestWithError( |
| FederatedAuthRequestResult::kErrorFetchingIdTokenInvalidResponse, |
| TokenStatus::kIdTokenInvalidResponse, |
| /*should_delay_callback=*/true); |
| return; |
| } |
| case IdpNetworkRequestManager::ParseStatus::kSuccess: { |
| // Grant sharing permission specific to *this account*. |
| // |
| // TODO(majidvp): But wait which account? |
| // 1) The account that user selected in our UI (i.e., account_id_) or |
| // 2) The one for which the IDP generated a token. |
| // |
| // Ideally these are one and the same but currently there is no |
| // enforcement for that equality so they could be different. In the |
| // future we may want to enforce that the token account (aka subject) |
| // matches the user selected account. But for now these questions are |
| // moot since we don't actually inspect the returned idtoken. |
| // https://crbug.com/1199088 |
| CHECK(!account_id_.empty()); |
| permission_delegate_->GrantSharingPermission( |
| origin(), GetEmbeddingOrigin(), url::Origin::Create(idp.config_url), |
| account_id_); |
| |
| permission_delegate_->GrantActiveSession( |
| origin(), url::Origin::Create(idp.config_url), account_id_); |
| |
| fedcm_metrics_->RecordTokenResponseAndTurnaroundTime( |
| token_response_time_ - select_account_time_, |
| token_response_time_ - start_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, show_accounts_dialog_time_ - start_time_, |
| select_account_time_ - show_accounts_dialog_time_, |
| token_response_time_ - select_account_time_, |
| token_response_time_ - start_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::kSuccess, idp.config_url, token, |
| /*should_delay_callback=*/false); |
| return; |
| } |
| } |
| } |
| |
| void FederatedAuthRequestImpl::DispatchOneLogout() { |
| auto logout_request = std::move(logout_requests_.front()); |
| DCHECK(logout_request->url.is_valid()); |
| std::string account_id = logout_request->account_id; |
| auto logout_origin = url::Origin::Create(logout_request->url); |
| logout_requests_.pop(); |
| |
| if (permission_delegate_->HasActiveSession(logout_origin, origin(), |
| account_id)) { |
| network_manager_->SendLogout( |
| logout_request->url, |
| base::BindOnce(&FederatedAuthRequestImpl::OnLogoutCompleted, |
| weak_ptr_factory_.GetWeakPtr())); |
| permission_delegate_->RevokeActiveSession(logout_origin, origin(), |
| account_id); |
| } else { |
| if (logout_requests_.empty()) { |
| CompleteLogoutRequest(LogoutRpsStatus::kSuccess); |
| return; |
| } |
| |
| DispatchOneLogout(); |
| } |
| } |
| |
| void FederatedAuthRequestImpl::OnLogoutCompleted() { |
| if (logout_requests_.empty()) { |
| CompleteLogoutRequest(LogoutRpsStatus::kSuccess); |
| return; |
| } |
| |
| DispatchOneLogout(); |
| } |
| |
| void FederatedAuthRequestImpl::CompleteRequestWithError( |
| blink::mojom::FederatedAuthRequestResult result, |
| absl::optional<TokenStatus> token_status, |
| bool should_delay_callback) { |
| CompleteRequest(result, token_status, |
| /*selected_idp_config_url=*/absl::nullopt, |
| /*token=*/"", should_delay_callback); |
| } |
| |
| void FederatedAuthRequestImpl::CompleteRequest( |
| blink::mojom::FederatedAuthRequestResult result, |
| absl::optional<TokenStatus> token_status, |
| const absl::optional<GURL>& selected_idp_config_url, |
| const std::string& id_token, |
| bool should_delay_callback) { |
| DCHECK(result == FederatedAuthRequestResult::kSuccess || id_token.empty()); |
| |
| if (!auth_request_callback_) |
| return; |
| |
| if (token_status) |
| fedcm_metrics_->RecordRequestTokenStatus(*token_status); |
| |
| if (!errors_logged_to_console_ && |
| result != FederatedAuthRequestResult::kSuccess) { |
| errors_logged_to_console_ = true; |
| |
| AddInspectorIssue(result); |
| AddConsoleErrorMessage(result); |
| |
| if (IsFedCmMetricsEndpointEnabled()) { |
| for (const auto& metrics_endpoint_kv : metrics_endpoints_) |
| SendFailedTokenRequestMetrics(metrics_endpoint_kv.second, result); |
| } |
| } |
| |
| CleanUp(); |
| |
| if (!should_delay_callback || ShouldCompleteRequestImmediately()) { |
| GetPageData(&render_frame_host())->SetHasPendingWebIdentityRequest(false); |
| errors_logged_to_console_ = false; |
| |
| RequestTokenStatus status = |
| FederatedAuthRequestResultToRequestTokenStatus(result); |
| std::move(auth_request_callback_) |
| .Run(status, selected_idp_config_url, id_token); |
| auth_request_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(); |
| request_dialog_controller_.reset(); |
| network_manager_.reset(); |
| // Given that |request_dialog_controller_| has reference to this web content |
| // instance we destroy that first. |
| account_id_ = std::string(); |
| start_time_ = base::TimeTicks(); |
| show_accounts_dialog_time_ = base::TimeTicks(); |
| select_account_time_ = base::TimeTicks(); |
| token_response_time_ = base::TimeTicks(); |
| idp_infos_.clear(); |
| pending_idps_.clear(); |
| idp_order_.clear(); |
| metrics_endpoints_.clear(); |
| } |
| |
| void FederatedAuthRequestImpl::AddInspectorIssue( |
| 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) { |
| std::string message = GetConsoleErrorMessage(result); |
| render_frame_host().AddMessageToConsole( |
| blink::mojom::ConsoleMessageLevel::kError, message); |
| } |
| |
| void FederatedAuthRequestImpl::MaybeAddResponseCodeToConsole( |
| const char* fetch_description, |
| int response_code) { |
| absl::optional<std::string> console_message = |
| webid::ComputeConsoleMessageForHttpResponseCode(fetch_description, |
| response_code); |
| if (console_message) { |
| render_frame_host().AddMessageToConsole( |
| blink::mojom::ConsoleMessageLevel::kError, *console_message); |
| } |
| } |
| |
| bool FederatedAuthRequestImpl::ShouldCompleteRequestImmediately() { |
| return api_permission_delegate_->ShouldCompleteRequestImmediately(); |
| } |
| |
| url::Origin FederatedAuthRequestImpl::GetEmbeddingOrigin() const { |
| RenderFrameHost* main_frame = render_frame_host().GetMainFrame(); |
| DCHECK(main_frame->IsInPrimaryMainFrame()); |
| return main_frame->GetLastCommittedOrigin(); |
| } |
| |
| void FederatedAuthRequestImpl::CompleteLogoutRequest( |
| blink::mojom::LogoutRpsStatus status) { |
| network_manager_.reset(); |
| base::queue<blink::mojom::LogoutRpsRequestPtr>().swap(logout_requests_); |
| if (logout_callback_) { |
| std::move(logout_callback_).Run(status); |
| logout_callback_.Reset(); |
| GetPageData(&render_frame_host())->SetHasPendingWebIdentityRequest(false); |
| } |
| } |
| |
| 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_); |
| |
| 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() |
| ? absl::nullopt |
| : absl::optional<std::string>(selected_account)); |
| } |
| |
| return GetContentClient()->browser()->CreateIdentityRequestDialogController(); |
| } |
| |
| void FederatedAuthRequestImpl::SetTokenRequestDelayForTests( |
| base::TimeDelta delay) { |
| token_request_delay_ = delay; |
| } |
| |
| 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::OnRejectRequest() { |
| if (auth_request_callback_) { |
| DCHECK(!logout_callback_); |
| DCHECK(errors_logged_to_console_); |
| CompleteRequestWithError(FederatedAuthRequestResult::kError, absl::nullopt, |
| /*should_delay_callback=*/false); |
| } |
| } |
| |
| } // namespace content |