| // 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/command_line.h" |
| #include "base/functional/callback.h" |
| #include "base/metrics/histogram_macros.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/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_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/mdocs/mdoc_provider.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 "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" |
| |
| 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 blink::mojom::RequestUserInfoStatus; |
| using FederatedApiPermissionStatus = |
| content::FederatedIdentityApiPermissionContextDelegate::PermissionStatus; |
| using TokenStatus = content::FedCmRequestIdTokenStatus; |
| using SignInStateMatchStatus = content::FedCmSignInStateMatchStatus; |
| using LoginState = content::IdentityRequestAccount::LoginState; |
| using SignInMode = content::IdentityRequestAccount::SignInMode; |
| using CompleteRequestWithErrorCallback = |
| base::OnceCallback<void(blink::mojom::FederatedAuthRequestResult, |
| absl::optional<content::FedCmRequestIdTokenStatus>, |
| bool)>; |
| |
| 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, |
| 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 += "&disclosure_text_shown=" + disclosure_text_shown; |
| |
| if (IsFedCmAuthzEnabled()) { |
| // 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/1429083): 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::kErrorFetchingIdTokenInvalidContentType: |
| 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: |
| case FederatedAuthRequestResult::kErrorFetchingAccountsListEmpty: |
| case FederatedAuthRequestResult::kErrorFetchingAccountsInvalidContentType: { |
| return IdpNetworkRequestManager::MetricsEndpointErrorCode:: |
| kAccountsEndpointInvalidResponse; |
| } |
| case FederatedAuthRequestResult::kErrorFetchingIdTokenInvalidResponse: |
| case FederatedAuthRequestResult::kErrorFetchingIdTokenInvalidContentType: { |
| 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::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: { |
| 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 FormatUrlWithDomain(const GURL& url, bool for_display) { |
| // We do not use url_formatter::FormatUrlForSecurityDisplay() directly because |
| // our UI intentionally shows only the eTLD+1, as it makes for a shorter text |
| // that is also clearer to users. The identity provider's well-known file is |
| // in the root of the eTLD+1, and sign-in status within identity provider and |
| // relying party can be domain-wide because it relies on cookies. |
| std::string formatted_url_str = |
| net::IsLocalhost(url) |
| ? url.host() |
| : net::registry_controlled_domains::GetDomainAndRegistry( |
| url, |
| net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES); |
| if (for_display) { |
| return base::UTF16ToUTF8(url_formatter::FormatUrlForSecurityDisplay( |
| GURL(url.scheme() + "://" + formatted_url_str), |
| url_formatter::SchemeDisplay::OMIT_HTTP_AND_HTTPS)); |
| } |
| return GURL(url.scheme() + "://" + formatted_url_str).spec(); |
| } |
| |
| std::string FormatOriginForDisplay(const url::Origin& origin) { |
| return FormatUrlWithDomain(origin.GetURL(), |
| /*for_display=*/true); |
| } |
| |
| bool ShouldSuppressIdpSigninFailureDialog( |
| absl::optional<TokenStatus> token_status) { |
| if (!token_status) { |
| return false; |
| } |
| |
| return token_status == TokenStatus::kAborted || |
| token_status == TokenStatus::kUnhandledRequest; |
| } |
| |
| FederatedAuthRequestPageData* GetPageData(RenderFrameHost* render_frame_host) { |
| return FederatedAuthRequestPageData::GetOrCreateForPage( |
| render_frame_host->GetPage()); |
| } |
| |
| 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 std::find(account.hints.begin(), account.hints.end(), login_hint) == |
| account.hints.end(); |
| }; |
| accounts.erase(std::remove_if(accounts.begin(), accounts.end(), Filter), |
| accounts.end()); |
| } |
| |
| std::unique_ptr<FedCmMetrics> CreateFedCmMetrics( |
| const GURL& provider_config_url, |
| const ukm::SourceId& source_id, |
| bool is_disabled) { |
| return std::make_unique<FedCmMetrics>(provider_config_url, source_id, |
| base::RandInt(1, 1 << 30), is_disabled); |
| } |
| |
| std::string GetTopFrameOriginForDisplay(const url::Origin& top_frame_origin) { |
| return FormatOriginForDisplay(top_frame_origin); |
| } |
| |
| absl::optional<std::string> GetIframeOriginForDisplay( |
| const url::Origin& top_frame_origin, |
| const url::Origin& iframe_origin, |
| CompleteRequestWithErrorCallback callback) { |
| // TODO(crbug.com/1418719): Replace exclude_iframe based on client metadata |
| // response. |
| bool exclude_iframe = net::registry_controlled_domains::SameDomainOrHost( |
| top_frame_origin, iframe_origin, |
| net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES); |
| absl::optional<std::string> iframe_for_display = absl::nullopt; |
| |
| if (!exclude_iframe) { |
| iframe_for_display = FormatOriginForDisplay(iframe_origin); |
| |
| // TODO(crbug.com/1422040): 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=*/absl::nullopt, |
| /*should_delay_callback=*/false); |
| } |
| } |
| |
| return iframe_for_display; |
| } |
| |
| } // namespace |
| |
| FederatedAuthRequestImpl::FetchData::FetchData() = default; |
| FederatedAuthRequestImpl::FetchData::~FetchData() = default; |
| |
| FederatedAuthRequestImpl::IdentityProviderGetInfo::IdentityProviderGetInfo( |
| blink::mojom::IdentityProviderConfigPtr provider, |
| blink::mojom::RpContext rp_context) |
| : provider(std::move(provider)), rp_context(rp_context) {} |
| |
| 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; |
| return *this; |
| } |
| |
| FederatedAuthRequestImpl::IdentityProviderInfo::IdentityProviderInfo( |
| const blink::mojom::IdentityProviderConfigPtr& provider, |
| IdpNetworkRequestManager::Endpoints endpoints, |
| IdentityProviderMetadata metadata, |
| blink::mojom::RpContext rp_context) |
| : provider(provider->Clone()), |
| endpoints(std::move(endpoints)), |
| metadata(std::move(metadata)), |
| rp_context(rp_context) {} |
| |
| 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; |
| 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), |
| token_request_delay_(kDefaultTokenRequestDelay) {} |
| |
| FederatedAuthRequestImpl::~FederatedAuthRequestImpl() { |
| // Ensures key data members are destructed in proper order and resolves any |
| // pending promise. |
| if (auth_request_token_callback_) { |
| DCHECK(!logout_callback_); |
| CompleteRequestWithError(FederatedAuthRequestResult::kError, |
| TokenStatus::kUnhandledRequest, |
| /*should_delay_callback=*/false); |
| } |
| if (user_info_request_) { |
| // 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_request_.reset(); |
| } |
| 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())->SetPendingWebIdentityRequest(nullptr); |
| } |
| } |
| |
| // 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)); |
| } |
| |
| void FederatedAuthRequestImpl::CompleteMDocRequest(std::string mdoc) { |
| if (!mdoc_provider_) { |
| std::move(mdoc_request_callback_) |
| .Run(RequestTokenStatus::kError, absl::nullopt, ""); |
| return; |
| } |
| |
| if (!mdoc.empty()) { |
| std::move(mdoc_request_callback_) |
| .Run(RequestTokenStatus::kSuccess, absl::nullopt, mdoc); |
| } else { |
| std::move(mdoc_request_callback_) |
| .Run(RequestTokenStatus::kError, absl::nullopt, ""); |
| } |
| } |
| |
| 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()) { |
| 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; |
| } |
| |
| if (idp_get_params_ptrs[0]->providers[0]->is_mdoc()) { |
| if (!IsWebIdentityMDocsEnabled() || |
| IsFedCmMultipleIdentityProvidersEnabled()) { |
| // TODO(https://crbug.com/1416939): Support calling the MDocs API with the |
| // Multi IdP API support. |
| std::move(callback).Run(RequestTokenStatus::kError, absl::nullopt, ""); |
| return; |
| } |
| |
| if (mdoc_request_callback_) { |
| // Similar to the token request, only allow one in-flight mdoc request. |
| // TODO(https://crbug.com/1416939): Reconcile with federated identity |
| // requests. |
| std::move(callback).Run(RequestTokenStatus::kErrorTooManyRequests, |
| absl::nullopt, ""); |
| return; |
| } |
| |
| mdoc_request_callback_ = std::move(callback); |
| // mdoc_provider_ is not destroyed after a successful mdoc request so we |
| // need to have the nullcheck to avoid duplicated creation. |
| if (!mdoc_provider_) { |
| mdoc_provider_ = CreateMDocProvider(); |
| } |
| if (!mdoc_provider_) { |
| std::move(mdoc_request_callback_) |
| .Run(RequestTokenStatus::kError, absl::nullopt, ""); |
| return; |
| } |
| |
| auto mdoc = std::move(idp_get_params_ptrs[0]->providers[0]->get_mdoc()); |
| std::string reader_public_key = mdoc->reader_public_key; |
| std::string document_type = mdoc->document_type; |
| |
| mdoc_provider_->RequestMDoc( |
| WebContents::FromRenderFrameHost(&render_frame_host()), |
| reader_public_key, document_type, mdoc->requested_elements, |
| base::BindOnce(&FederatedAuthRequestImpl::CompleteMDocRequest, |
| weak_ptr_factory_.GetWeakPtr())); |
| |
| // TODO(https://crbug.com/1416939): rather than returning early, |
| // we would ultimately like to make the mdocs response reconcile with the |
| // federated identities, so that they can be presented to the user in an |
| // unified manner. |
| 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_ = CreateFedCmMetrics( |
| idp_get_params_ptrs[0]->providers[0]->get_federated()->config_url, |
| render_frame_host().GetPageUkmSourceId(), |
| /*is_disabled=*/idp_get_params_ptrs.size() > 1); |
| } |
| |
| if (HasPendingRequest()) { |
| fedcm_metrics_->RecordRequestTokenStatus(TokenStatus::kTooManyRequests); |
| std::move(callback).Run(RequestTokenStatus::kErrorTooManyRequests, |
| absl::nullopt, ""); |
| 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(); |
| |
| mediation_requirement_ = requirement; |
| auth_request_token_callback_ = std::move(callback); |
| GetPageData(&render_frame_host())->SetPendingWebIdentityRequest(this); |
| 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> 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->get_federated()->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->get_federated()->config_url))) { |
| CompleteRequestWithError(FederatedAuthRequestResult::kError, |
| TokenStatus::kIdpNotPotentiallyTrustworthy, |
| /*should_delay_callback=*/false); |
| return; |
| } |
| |
| // TODO(crbug.com/1382545): Handle |
| // ShouldFailAccountsEndpointRequestBecauseNotSignedInWithIdp in the multi |
| // IDP use case. |
| bool has_failing_idp_signin_status = |
| webid::ShouldFailAccountsEndpointRequestBecauseNotSignedInWithIdp( |
| idp_ptr->get_federated()->config_url, permission_delegate_); |
| |
| if (has_failing_idp_signin_status && |
| GetFedCmIdpSigninStatusMode() == FedCmIdpSigninStatusMode::ENABLED) { |
| CompleteRequestWithError(FederatedAuthRequestResult::kError, |
| TokenStatus::kNotSignedInWithIdp, |
| /*should_delay_callback=*/true); |
| return; |
| } |
| // TODO(crbug.com/1383384): Handle auto_reauthn for multi IDP. |
| if (ShouldFailBeforeFetchingAccounts( |
| idp_ptr->get_federated()->config_url)) { |
| CompleteRequestWithError(FederatedAuthRequestResult::kError, |
| /*token_status=*/absl::nullopt, |
| /*should_delay_callback=*/false); |
| return; |
| } |
| } |
| } |
| |
| 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->get_federated()->config_url); |
| blink::mojom::RpContext rp_context = |
| IsFedCmRpContextEnabled() ? idp_get_params_ptr->context |
| : blink::mojom::RpContext::kSignIn; |
| const GURL& idp_config_url = idp_ptr->get_federated()->config_url; |
| token_request_get_infos_.emplace( |
| idp_config_url, IdentityProviderGetInfo( |
| std::move(idp_ptr->get_federated()), rp_context)); |
| } |
| } |
| |
| FetchEndpointsForIdps(std::move(unique_idps), /*for_idp_signin=*/false); |
| } |
| |
| void FederatedAuthRequestImpl::RequestUserInfo( |
| blink::mojom::IdentityProviderConfigPtr provider, |
| RequestUserInfoCallback callback) { |
| if (!IsFedCmUserInfoEnabled()) { |
| // This could happen with a compromised renderer. Exit early such that we |
| // don't proceed when the flag is off or crash the browser. |
| std::move(callback).Run(RequestUserInfoStatus::kError, absl::nullopt); |
| return; |
| } |
| |
| if (user_info_request_) { |
| std::move(callback).Run(RequestUserInfoStatus::kErrorTooManyRequests, |
| absl::nullopt); |
| return; |
| } |
| |
| if (!fedcm_metrics_) { |
| fedcm_metrics_ = CreateFedCmMetrics( |
| provider->config_url, render_frame_host().GetPageUkmSourceId(), |
| /*is_disabled=*/false); |
| } |
| |
| auto network_manager = IdpNetworkRequestManager::Create( |
| static_cast<RenderFrameHostImpl*>(&render_frame_host())); |
| user_info_request_ = FederatedAuthUserInfoRequest::CreateAndStart( |
| std::move(network_manager), api_permission_delegate_.get(), |
| permission_delegate_.get(), &render_frame_host(), fedcm_metrics_.get(), |
| std::move(provider), |
| base::BindOnce(&FederatedAuthRequestImpl::CompleteUserInfoRequest, |
| weak_ptr_factory_.GetWeakPtr(), std::move(callback))); |
| } |
| |
| void FederatedAuthRequestImpl::CancelTokenRequest() { |
| if (!auth_request_token_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())->SetPendingWebIdentityRequest(this); |
| |
| 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::ResolveTokenRequest( |
| const std::string& token, |
| ResolveTokenRequestCallback callback) { |
| if (!IsFedCmAuthzEnabled()) { |
| std::move(callback).Run(false); |
| return; |
| } |
| |
| if (!identity_registry_) { |
| std::move(callback).Run(false); |
| return; |
| } |
| |
| bool accepted = identity_registry_->NotifyResolve(origin(), token); |
| std::move(callback).Run(accepted); |
| } |
| |
| 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); |
| } |
| |
| 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; |
| } |
| // TODO(crbug.com/1406698): prompt the user for permission. |
| permission_delegate_->RegisterIdP(idp); |
| std::move(callback).Run(true); |
| } |
| |
| 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::OnIdpSigninStatusChanged( |
| const url::Origin& idp_config_origin, |
| bool idp_signin_status) { |
| if (!idp_signin_status) { |
| return; |
| } |
| |
| 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); |
| FetchEndpointsForIdps({get_idp_config_url}, /*for_idp_signin=*/true); |
| break; |
| } |
| } |
| } |
| |
| bool FederatedAuthRequestImpl::HasPendingRequest() const { |
| bool has_pending_request = |
| GetPageData(&render_frame_host())->PendingWebIdentityRequest() != nullptr; |
| DCHECK(has_pending_request || |
| (!auth_request_token_callback_ && !logout_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>(network_manager_.get()); |
| provider_fetcher_->Start( |
| fetch_data_.pending_idps, 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(); |
| |
| 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); |
| |
| 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 = |
| webid::ShouldFailAccountsEndpointRequestBecauseNotSignedInWithIdp( |
| 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); |
| } |
| |
| 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 ShouldRequestPermission(const std::vector<std::string>& scope) { |
| if (!IsFedCmAuthzEnabled()) { |
| return true; |
| } |
| |
| if (scope.size() == 0) { |
| // If "scope" is not passed, defaults the parameter to |
| // ["sub", "name", "email" and "picture"]. |
| return true; |
| } |
| return HasScope(scope, "sub") && HasScope(scope, "name") && |
| HasScope(scope, "email") && HasScope(scope, "picture"); |
| } |
| |
| 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_url; |
| |
| bool request_permission = ShouldRequestPermission(idp_info->provider->scope); |
| |
| const std::string idp_for_display = |
| FormatUrlWithDomain(idp_config_url, /*for_display=*/true); |
| idp_info->data = IdentityProviderData( |
| idp_for_display, accounts, idp_info->metadata, |
| ClientMetadata{client_metadata.terms_of_service_url, |
| client_metadata.privacy_policy_url}, |
| idp_info->rp_context, /* request_permission */ request_permission); |
| 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, |
| absl::optional<TokenStatus> token_status, |
| bool should_delay_callback) { |
| const GURL& idp_config_url = idp_info->provider->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, should_delay_callback); |
| return; |
| } |
| |
| AddInspectorIssue(result); |
| AddConsoleErrorMessage(result); |
| |
| if (IsFedCmMetricsEndpointEnabled()) |
| SendFailedTokenRequestMetrics(idp_info->endpoints.metrics, result); |
| |
| metrics_endpoints_.erase(idp_config_url); |
| |
| 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 (!fetch_data_.pending_idps.empty()) { |
| return; |
| } |
| |
| // The RenderFrameHost may be alive but not visible in the following |
| // situations: |
| // Situation #1: User switched tabs |
| // Situation #2: User navigated the page. The RenderFrameHost is still |
| // alive thanks to the 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_visible = (render_frame_host().IsActive() && |
| render_frame_host().GetVisibilityState() == |
| content::PageVisibilityState::kVisible); |
| fedcm_metrics_->RecordWebContentsVisibilityUponReadyToShowDialog( |
| is_visible); |
| |
| 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_); |
| } |
| |
| fetch_data_ = FetchData(); |
| |
| DCHECK(idp_data_for_display_.empty()); |
| |
| 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); |
| } |
| } |
| |
| // RenderFrameHost should be in the primary page (ex not in the BFCache). |
| DCHECK(render_frame_host().GetPage().IsPrimary()); |
| |
| // TODO(crbug.com/1383384): Handle auto_reauthn for multi IDP. |
| bool auto_reauthn_enabled = |
| mediation_requirement_ != MediationRequirement::kRequired && |
| IsFedCmAutoReauthnEnabled(); |
| |
| bool auto_reauthn = auto_reauthn_enabled; |
| bool is_auto_reauthn_setting_enabled = false; |
| bool is_auto_reauthn_embargoed = false; |
| absl::optional<base::TimeDelta> time_from_embargo; |
| if (auto_reauthn_enabled) { |
| is_auto_reauthn_setting_enabled = |
| auto_reauthn_permission_delegate_->IsAutoReauthnSettingEnabled(); |
| auto_reauthn &= is_auto_reauthn_setting_enabled; |
| 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."); |
| } |
| auto_reauthn &= !is_auto_reauthn_embargoed; |
| auto_reauthn &= !RequiresUserMediation(); |
| } |
| |
| const IdentityProviderData* auto_reauthn_idp = nullptr; |
| const IdentityRequestAccount* auto_reauthn_account = nullptr; |
| bool has_single_returning_account = false; |
| if (auto_reauthn_enabled) { |
| // Auto signs in returning users if they have a single returning account and |
| // are signing in. |
| has_single_returning_account = |
| GetSingleReturningAccount(&auto_reauthn_idp, &auto_reauthn_account); |
| if (!has_single_returning_account && |
| mediation_requirement_ == MediationRequirement::kSilent) { |
| fedcm_metrics_->RecordAutoReauthnMetrics( |
| has_single_returning_account, auto_reauthn_account, auto_reauthn, |
| !is_auto_reauthn_setting_enabled, is_auto_reauthn_embargoed, |
| time_from_embargo); |
| |
| // 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/1441436): validate the statement above with stakeholders |
| CompleteRequestWithError(FederatedAuthRequestResult::kError, |
| /*token_status=*/absl::nullopt, |
| /*should_delay_callback=*/false); |
| return; |
| } |
| auto_reauthn &= has_single_returning_account; |
| } |
| |
| if (auto_reauthn) { |
| IdentityRequestAccount account{*auto_reauthn_account}; |
| IdentityProviderData idp{*auto_reauthn_idp}; |
| idp.accounts = {account}; |
| idp_data_for_display_ = {idp}; |
| } |
| |
| // TODO(crbug.com/1408520): opt-out affordance is not included in the origin |
| // trial. Should revisit based on the OT feedback. |
| bool show_auto_reauthn_checkbox = false; |
| |
| 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); |
| } |
| |
| absl::optional<std::string> iframe_for_display = GetIframeOriginForDisplay( |
| GetEmbeddingOrigin(), origin(), |
| base::BindOnce(&FederatedAuthRequestImpl::CompleteRequestWithError, |
| weak_ptr_factory_.GetWeakPtr())); |
| |
| // 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( |
| WebContents::FromRenderFrameHost(&render_frame_host()), |
| GetTopFrameOriginForDisplay(GetEmbeddingOrigin()), iframe_for_display, |
| idp_data_for_display_, |
| auto_reauthn ? SignInMode::kAuto : SignInMode::kExplicit, |
| show_auto_reauthn_checkbox, |
| base::BindOnce(&FederatedAuthRequestImpl::OnAccountSelected, |
| weak_ptr_factory_.GetWeakPtr(), auto_reauthn), |
| base::BindOnce(&FederatedAuthRequestImpl::OnDialogDismissed, |
| weak_ptr_factory_.GetWeakPtr())); |
| devtools_instrumentation::OnFedCmAccountsDialogShown(&render_frame_host(), |
| auto_reauthn); |
| |
| if (auto_reauthn_enabled) { |
| fedcm_metrics_->RecordAutoReauthnMetrics( |
| has_single_returning_account, auto_reauthn_account, auto_reauthn, |
| !is_auto_reauthn_setting_enabled, is_auto_reauthn_embargoed, |
| time_from_embargo); |
| } |
| } |
| |
| void FederatedAuthRequestImpl::HandleAccountsFetchFailure( |
| std::unique_ptr<IdentityProviderInfo> idp_info, |
| absl::optional<bool> old_idp_signin_status, |
| 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); |
| |
| if (!old_idp_signin_status.has_value() || |
| GetFedCmIdpSigninStatusMode() == FedCmIdpSigninStatusMode::METRICS_ONLY) { |
| OnFetchDataForIdpFailed(std::move(idp_info), result, token_status, |
| /*should_delay_callback=*/true); |
| return; |
| } |
| |
| bool is_visible = (render_frame_host().IsActive() && |
| render_frame_host().GetVisibilityState() == |
| content::PageVisibilityState::kVisible); |
| if (!is_visible) { |
| CompleteRequestWithError(FederatedAuthRequestResult::kErrorRpPageNotVisible, |
| TokenStatus::kRpPageNotVisible, |
| /*should_delay_callback=*/true); |
| return; |
| } |
| |
| // 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/1441436): validate the statement above with stakeholders |
| if (mediation_requirement_ == MediationRequirement::kSilent) { |
| CompleteRequestWithError(FederatedAuthRequestResult::kError, |
| /*token_status=*/absl::nullopt, |
| /*should_delay_callback=*/false); |
| 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()); |
| // RenderFrameHost should be in the primary page (ex not in the BFCache). |
| DCHECK(render_frame_host().GetPage().IsPrimary()); |
| // TODO(crbug.com/1382495): Handle failure UI in the multi IDP case. |
| |
| fetch_data_ = FetchData(); |
| permission_delegate_->AddIdpSigninStatusObserver(this); |
| |
| absl::optional<std::string> iframe_for_display = GetIframeOriginForDisplay( |
| GetEmbeddingOrigin(), origin(), |
| base::BindOnce(&FederatedAuthRequestImpl::CompleteRequestWithError, |
| weak_ptr_factory_.GetWeakPtr())); |
| |
| // If IdP sign-in status mismatch dialog is already visible, calling |
| // ShowFailureDialog() a 2nd time should notify the user that sign-in |
| // failed. |
| request_dialog_controller_->ShowFailureDialog( |
| rp_web_contents, GetTopFrameOriginForDisplay(GetEmbeddingOrigin()), |
| iframe_for_display, FormatOriginForDisplay(idp_origin), |
| idp_info->metadata, |
| base::BindOnce(&FederatedAuthRequestImpl::OnDismissFailureDialog, |
| weak_ptr_factory_.GetWeakPtr(), |
| FederatedAuthRequestResult::kError, |
| TokenStatus::kNotSignedInWithIdp, |
| /*should_delay_callback=*/true), |
| base::BindOnce(&FederatedAuthRequestImpl::ShowModalDialog, |
| weak_ptr_factory_.GetWeakPtr(), |
| idp_info->metadata.idp_signin_url)); |
| } |
| |
| void FederatedAuthRequestImpl::CloseModalDialogView() { |
| if (identity_registry_) { |
| identity_registry_->NotifyClose(origin()); |
| } |
| } |
| |
| void FederatedAuthRequestImpl::OnAccountsResponseReceived( |
| std::unique_ptr<IdentityProviderInfo> idp_info, |
| IdpNetworkRequestManager::FetchStatus status, |
| IdpNetworkRequestManager::AccountList accounts) { |
| GURL idp_config_url = idp_info->provider->config_url; |
| const absl::optional<bool> old_idp_signin_status = |
| permission_delegate_->GetIdpSigninStatus( |
| url::Origin::Create(idp_config_url)); |
| webid::UpdateIdpSigninStatusForAccountsEndpointResponse( |
| idp_config_url, status, idp_info->has_failing_idp_signin_status, |
| permission_delegate_, fedcm_metrics_.get()); |
| |
| 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: { |
| if (IsFedCmLoginHintEnabled()) { |
| FilterAccountsWithLoginHint(idp_info->provider->login_hint, accounts); |
| if (accounts.empty()) { |
| // 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; |
| } |
| } |
| ComputeLoginStateAndReorderAccounts(idp_info->provider, accounts); |
| |
| bool need_client_metadata = false; |
| |
| if (ShouldRequestPermission(idp_info->provider->scope)) { |
| 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 IdentityProviderConfigPtr& 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(bool auto_reauthn, |
| 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; |
| } |
| |
| if (auto_reauthn) { |
| // 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_->RecordDisplayAndEmbargo( |
| GetEmbeddingOrigin()); |
| } |
| |
| 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, idp_info.provider->scope, idp_info.provider->responseType, |
| idp_info.provider->params), |
| base::BindOnce(&FederatedAuthRequestImpl::OnTokenResponseReceived, |
| weak_ptr_factory_.GetWeakPtr(), |
| idp_info.provider->Clone()), |
| base::BindOnce(&FederatedAuthRequestImpl::OnContinueOnResponseReceived, |
| weak_ptr_factory_.GetWeakPtr(), |
| idp_info.provider->Clone())); |
| } |
| |
| 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::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(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::ShowModalDialog(const GURL& url) { |
| WebContents* web_contents = request_dialog_controller_->ShowModalDialog( |
| url, base::BindOnce(&FederatedAuthRequestImpl::OnDialogDismissed, |
| weak_ptr_factory_.GetWeakPtr())); |
| IdentityRegistry::CreateForWebContents(web_contents, this, |
| url::Origin::Create(url)); |
| } |
| |
| void FederatedAuthRequestImpl::OnContinueOnResponseReceived( |
| IdentityProviderConfigPtr idp, |
| IdpNetworkRequestManager::FetchStatus status, |
| const GURL& continue_on) { |
| if (!IsFedCmAuthzEnabled()) { |
| CompleteRequestWithError( |
| FederatedAuthRequestResult::kErrorFetchingIdTokenInvalidResponse, |
| TokenStatus::kIdTokenInvalidResponse, |
| /*should_delay_callback=*/true); |
| return; |
| } |
| |
| // TODO(crbug.com/1429083): record the appropriate metrics. |
| ShowModalDialog(idp->config_url); |
| } |
| |
| void FederatedAuthRequestImpl::OnTokenResponseReceived( |
| IdentityProviderConfigPtr 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 (should_complete_request_immediately_ || |
| fetch_time >= token_request_delay_) { |
| CompleteTokenRequest(std::move(idp), status, id_token); |
| return; |
| } |
| |
| base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask( |
| FROM_HERE, |
| base::BindOnce(&FederatedAuthRequestImpl::CompleteTokenRequest, |
| weak_ptr_factory_.GetWeakPtr(), std::move(idp), status, |
| id_token), |
| token_request_delay_ - fetch_time); |
| } |
| |
| void FederatedAuthRequestImpl::CompleteTokenRequest( |
| IdentityProviderConfigPtr 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::kInvalidContentTypeError: { |
| MaybeAddResponseCodeToConsole(kIdAssertionUrl, status.response_code); |
| CompleteRequestWithError( |
| FederatedAuthRequestResult::kErrorFetchingIdTokenInvalidContentType, |
| TokenStatus::kIdTokenInvalidContentType, |
| /*should_delay_callback=*/true); |
| return; |
| } |
| case IdpNetworkRequestManager::ParseStatus::kEmptyListError: { |
| NOTREACHED() << "kEmptyListError is undefined for CompleteTokenRequest"; |
| 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_); |
| |
| SetRequiresUserMediation(false); |
| |
| 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_token_callback_) { |
| return; |
| } |
| |
| if (result != FederatedAuthRequestResult::kSuccess && |
| fetch_data_.for_idp_signin && |
| !ShouldSuppressIdpSigninFailureDialog(token_status)) { |
| fetch_data_ = FetchData(); |
| |
| request_dialog_controller_->ShowIdpSigninFailureDialog(base::BindOnce( |
| &FederatedAuthRequestImpl::CompleteRequest, |
| weak_ptr_factory_.GetWeakPtr(), result, std::move(token_status), |
| selected_idp_config_url, id_token, should_delay_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 || should_complete_request_immediately_) { |
| GetPageData(&render_frame_host())->SetPendingWebIdentityRequest(nullptr); |
| errors_logged_to_console_ = false; |
| |
| RequestTokenStatus status = |
| FederatedAuthRequestResultToRequestTokenStatus(result); |
| std::move(auth_request_token_callback_) |
| .Run(status, selected_idp_config_url, id_token); |
| 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(); |
| show_accounts_dialog_time_ = base::TimeTicks(); |
| select_account_time_ = base::TimeTicks(); |
| token_response_time_ = base::TimeTicks(); |
| idp_infos_.clear(); |
| idp_data_for_display_.clear(); |
| fetch_data_ = FetchData(); |
| idp_order_.clear(); |
| metrics_endpoints_.clear(); |
| token_request_get_infos_.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) { |
| render_frame_host().AddMessageToConsole( |
| blink::mojom::ConsoleMessageLevel::kError, |
| webid::GetConsoleErrorMessageFromResult(result)); |
| } |
| |
| 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); |
| } |
| } |
| |
| url::Origin FederatedAuthRequestImpl::GetEmbeddingOrigin() const { |
| return render_frame_host().GetMainFrame()->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())->SetPendingWebIdentityRequest(nullptr); |
| } |
| } |
| |
| void FederatedAuthRequestImpl::CompleteUserInfoRequest( |
| RequestUserInfoCallback callback, |
| blink::mojom::RequestUserInfoStatus status, |
| absl::optional<std::vector<blink::mojom::IdentityUserInfoPtr>> user_info) { |
| if (!user_info_request_) { |
| return; |
| } |
| |
| std::move(callback).Run(status, std::move(user_info)); |
| user_info_request_.reset(); |
| } |
| |
| 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(); |
| } |
| |
| std::unique_ptr<MDocProvider> FederatedAuthRequestImpl::CreateMDocProvider() { |
| // A provider may only be created in browser tests by this moment. |
| std::unique_ptr<MDocProvider> provider = |
| GetContentClient()->browser()->CreateMDocProvider(); |
| |
| if (!provider) { |
| return MDocProvider::Create(); |
| } |
| return provider; |
| } |
| |
| 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::NotifyClose() { |
| request_dialog_controller_->CloseModalDialog(); |
| } |
| |
| bool FederatedAuthRequestImpl::NotifyResolve(const std::string& token) { |
| // TODO(crbug.com/1429083): handle the multi-idp case when there are |
| // more than one config_urls hanging. |
| CompleteRequest(FederatedAuthRequestResult::kSuccess, TokenStatus::kSuccess, |
| absl::nullopt, token, |
| /*should_delay_callback=*/false); |
| // TODO(crbug.com/1429083): handle the corner cases where CompleteRequest |
| // can't actually fulfill the request. |
| return true; |
| } |
| |
| void FederatedAuthRequestImpl::OnRejectRequest() { |
| if (auth_request_token_callback_) { |
| DCHECK(!logout_callback_); |
| DCHECK(errors_logged_to_console_); |
| CompleteRequestWithError(FederatedAuthRequestResult::kError, absl::nullopt, |
| /*should_delay_callback=*/false); |
| } |
| } |
| |
| void FederatedAuthRequestImpl::AcceptAccountsDialogForDevtools( |
| const GURL& config_url, |
| const IdentityRequestAccount& account) { |
| bool is_sign_in = |
| account.login_state == IdentityRequestAccount::LoginState::kSignIn; |
| OnAccountSelected(/*auto_reauthn=*/false, 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); |
| } |
| |
| bool FederatedAuthRequestImpl::GetSingleReturningAccount( |
| const IdentityProviderData** out_idp_data, |
| const IdentityRequestAccount** out_account) { |
| for (const auto& idp_info : idp_infos_) { |
| 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. |
| if (!permission_delegate_->HasSharingPermission( |
| origin(), GetEmbeddingOrigin(), |
| url::Origin::Create(idp_info.first), account.id)) { |
| 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 || |
| !IsFedCmAutoReauthnEnabled()) { |
| return false; |
| } |
| |
| bool is_auto_reauthn_setting_enabled = |
| auto_reauthn_permission_delegate_->IsAutoReauthnSettingEnabled(); |
| |
| bool is_auto_reauthn_embargoed = |
| auto_reauthn_permission_delegate_->IsAutoReauthnEmbargoed( |
| GetEmbeddingOrigin()); |
| |
| bool has_sharing_permission_for_any_account = |
| permission_delegate_->HasSharingPermission( |
| origin(), GetEmbeddingOrigin(), url::Origin::Create(config_url), |
| absl::nullopt); |
| |
| return RequiresUserMediation() || !is_auto_reauthn_setting_enabled || |
| is_auto_reauthn_embargoed || !has_sharing_permission_for_any_account; |
| } |
| |
| bool FederatedAuthRequestImpl::RequiresUserMediation() { |
| std::string site = FormatUrlWithDomain(origin().GetURL(), |
| /*for_display=*/false); |
| return auto_reauthn_permission_delegate_->RequiresUserMediation(GURL(site)); |
| } |
| |
| void FederatedAuthRequestImpl::SetRequiresUserMediation( |
| bool requires_user_mediation) { |
| // Get the domain and set RequiresUserMediation to the given value on it. |
| // Include the scheme to make sure that for example that http://domain and |
| // https://domain are not treated as the same. |
| std::string site = FormatUrlWithDomain(origin().GetURL(), |
| /*for_display=*/false); |
| auto_reauthn_permission_delegate_->SetRequiresUserMediation( |
| GURL(site), requires_user_mediation); |
| } |
| |
| void FederatedAuthRequestImpl::PreventSilentAccess( |
| PreventSilentAccessCallback callback) { |
| SetRequiresUserMediation(true); |
| |
| // Send acknowledge response back. |
| std::move(callback).Run(); |
| } |
| |
| } // namespace content |