blob: 7e8087da79f887b5dfa97045572b1de1c44e55de [file] [log] [blame]
// Copyright 2023 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/user_info_request.h"
#include "base/functional/callback.h"
#include "base/metrics/histogram_functions.h"
#include "base/trace_event/trace_event.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/browser/webid/flags.h"
#include "content/browser/webid/request_page_data.h"
#include "content/browser/webid/webid_utils.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/webid/federated_identity_api_permission_context_delegate.h"
#include "content/public/browser/webid/federated_identity_permission_context_delegate.h"
#include "services/network/public/cpp/is_potentially_trustworthy.h"
#include "third_party/blink/public/mojom/webid/federated_auth_request.mojom.h"
#include "url/origin.h"
#include "url/url_constants.h"
namespace content::webid {
namespace {
std::string GetConsoleErrorMessage(UserInfoRequestResult error) {
switch (error) {
case UserInfoRequestResult::kNotSameOrigin: {
return "getUserInfo() caller is not same origin as the config URL.";
}
case UserInfoRequestResult::kNotIframe: {
return "getUserInfo() caller is not an iframe.";
}
case UserInfoRequestResult::kNotPotentiallyTrustworthy: {
return "getUserInfo() failed because the config URL is not potentially "
"trustworthy.";
}
case UserInfoRequestResult::kNoApiPermission: {
return "getUserInfo() is disabled because FedCM is disabled.";
}
case UserInfoRequestResult::kNotSignedInWithIdp: {
return "getUserInfo() is disabled because the IDP Sign-In Status is "
"signed-out.";
}
case UserInfoRequestResult::kNoAccountSharingPermission: {
return "getUserInfo() failed because the user has not yet used FedCM on "
"this site with the provided IDP.";
}
case UserInfoRequestResult::kInvalidConfigOrWellKnown: {
return "getUserInfo() failed because the config and well-known files "
"were invalid.";
}
case UserInfoRequestResult::kInvalidAccountsResponse: {
return "getUserInfo() failed because of an invalid accounts response.";
}
case UserInfoRequestResult::kNoReturningUserFromFetchedAccounts: {
return "getUserInfo() failed because no account received was a returning "
"account.";
}
case UserInfoRequestResult::kUnhandledRequest:
case UserInfoRequestResult::kSuccess: {
NOTREACHED();
}
}
}
} // namespace
using FederatedApiPermissionStatus =
FederatedIdentityApiPermissionContextDelegate::PermissionStatus;
using LoginState = IdentityRequestAccount::LoginState;
// static
std::unique_ptr<UserInfoRequest> UserInfoRequest::Create(
std::unique_ptr<IdpNetworkRequestManager> network_manager,
FederatedIdentityPermissionContextDelegate* permission_delegate,
FederatedIdentityApiPermissionContextDelegate* api_permission_delegate,
RenderFrameHost* render_frame_host,
blink::mojom::IdentityProviderConfigPtr provider) {
std::unique_ptr<UserInfoRequest> request =
base::WrapUnique<UserInfoRequest>(new UserInfoRequest(
std::move(network_manager), permission_delegate,
api_permission_delegate, render_frame_host, std::move(provider)));
return request;
}
UserInfoRequest::~UserInfoRequest() {
CompleteWithError(UserInfoRequestResult::kUnhandledRequest);
}
UserInfoRequest::UserInfoRequest(
std::unique_ptr<IdpNetworkRequestManager> network_manager,
FederatedIdentityPermissionContextDelegate* permission_delegate,
FederatedIdentityApiPermissionContextDelegate* api_permission_delegate,
RenderFrameHost* render_frame_host,
blink::mojom::IdentityProviderConfigPtr provider)
: network_manager_(std::move(network_manager)),
permission_delegate_(permission_delegate),
api_permission_delegate_(api_permission_delegate),
render_frame_host_(render_frame_host),
client_id_(provider->client_id),
idp_config_url_(provider->config_url),
origin_(render_frame_host->GetLastCommittedOrigin()),
perfetto_track_(CreatePerfettoTrackForFedCM(this)) {
RenderFrameHost* main_frame = render_frame_host->GetMainFrame();
DCHECK(main_frame->IsInPrimaryMainFrame());
embedding_origin_ = main_frame->GetLastCommittedOrigin();
RenderFrameHost* parent_frame = render_frame_host->GetParentOrOuterDocument();
parent_frame_origin_ =
parent_frame ? parent_frame->GetLastCommittedOrigin() : url::Origin();
}
void UserInfoRequest::SetCallbackAndStart(
blink::mojom::FederatedAuthRequest::RequestUserInfoCallback callback) {
TRACE_EVENT_BEGIN("content.fedcm", "FedCM getUserInfo", perfetto_track_);
callback_ = std::move(callback);
request_start_time_ = base::TimeTicks::Now();
// Renderer also checks that the origin is same origin with `idp_config_url_`.
// The check is duplicated in case that the renderer is compromised.
if (!origin_.IsSameOriginWith(idp_config_url_)) {
CompleteWithError(UserInfoRequestResult::kNotSameOrigin);
return;
}
// Check that `render_frame_host` is for an iframe.
if (!parent_frame_origin_.GetURL().is_valid()) {
CompleteWithError(UserInfoRequestResult::kNotIframe);
return;
}
url::Origin idp_origin = url::Origin::Create(idp_config_url_);
if (!network::IsOriginPotentiallyTrustworthy(idp_origin)) {
CompleteWithError(UserInfoRequestResult::kNotPotentiallyTrustworthy);
return;
}
FederatedApiPermissionStatus permission_status =
api_permission_delegate_->GetApiPermissionStatus(embedding_origin_);
if (permission_status != FederatedApiPermissionStatus::GRANTED) {
CompleteWithError(UserInfoRequestResult::kNoApiPermission);
return;
}
if (ShouldFailAccountsEndpointRequestBecauseNotSignedInWithIdp(
*render_frame_host_, idp_config_url_, permission_delegate_)) {
CompleteWithError(UserInfoRequestResult::kNotSignedInWithIdp);
return;
}
if (!HasSharingPermissionOrIdpHasThirdPartyCookiesAccess(
*render_frame_host_, idp_config_url_, embedding_origin_,
parent_frame_origin_, /*account_id=*/std::nullopt,
permission_delegate_, api_permission_delegate_)) {
// If there is no sharing permission or the IdP does not have third party
// cookies access, we can abort before performing any fetch.
CompleteWithError(UserInfoRequestResult::kNoAccountSharingPermission);
return;
}
// ConfigFetcher is stored as a member so that it is destroyed when
// FederatedAuthRequestImpl is destroyed.
config_fetcher_ = std::make_unique<ConfigFetcher>(*render_frame_host_,
network_manager_.get());
// TODO(crbug.com/390626180): It seems ok to ignore the well-known checks in
// all cases here. However, keeping this unchanged for now when the IDP
// registration API is not enabled since we only really need this for that
// case.
config_fetcher_->Start(
{{idp_config_url_, webid::IsIdPRegistrationEnabled()}},
blink::mojom::RpMode::kPassive, /*icon_ideal_size=*/0,
/*icon_minimum_size=*/0,
base::BindOnce(&UserInfoRequest::OnAllConfigAndWellKnownFetched,
weak_ptr_factory_.GetWeakPtr()));
}
void UserInfoRequest::OnAllConfigAndWellKnownFetched(
std::vector<ConfigFetcher::FetchResult> fetch_results) {
config_fetcher_.reset();
if (fetch_results.size() != 1u) {
// This could happen when the user info request was sent from a compromised
// renderer (>1) or fetch_results is empty (<1).
CompleteWithError(UserInfoRequestResult::kInvalidConfigOrWellKnown);
return;
}
if (fetch_results[0].error) {
CompleteWithError(UserInfoRequestResult::kInvalidConfigOrWellKnown);
return;
}
// 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.
does_idp_have_failing_signin_status_ =
ShouldFailAccountsEndpointRequestBecauseNotSignedInWithIdp(
*render_frame_host_, idp_config_url_, permission_delegate_);
if (does_idp_have_failing_signin_status_) {
CompleteWithError(UserInfoRequestResult::kNotSignedInWithIdp);
return;
}
network_manager_->SendAccountsRequest(
url::Origin::Create(idp_config_url_), fetch_results[0].endpoints.accounts,
client_id_,
base::BindOnce(&UserInfoRequest::OnAccountsResponseReceived,
weak_ptr_factory_.GetWeakPtr()));
}
void UserInfoRequest::OnAccountsResponseReceived(
IdpNetworkRequestManager::FetchStatus fetch_status,
std::vector<IdentityRequestAccountPtr> accounts) {
UpdateIdpSigninStatusForAccountsEndpointResponse(
*render_frame_host_, idp_config_url_, fetch_status,
does_idp_have_failing_signin_status_, permission_delegate_);
if (fetch_status.parse_status !=
IdpNetworkRequestManager::ParseStatus::kSuccess) {
CompleteWithError(UserInfoRequestResult::kInvalidAccountsResponse);
return;
}
GetPageData(render_frame_host_->GetPage())
->SetUserInfoAccountsResponseTime(idp_config_url_,
base::TimeTicks::Now());
// Populate the accounts' login state based on browser stored permission
// grants.
for (auto& account : accounts) {
LoginState login_state = LoginState::kSignUp;
// Consider this a sign-in if we have seen a successful sign-up for
// this account before.
if (permission_delegate_->GetLastUsedTimestamp(
parent_frame_origin_, embedding_origin_,
url::Origin::Create(idp_config_url_), account->id)) {
login_state = LoginState::kSignIn;
}
account->browser_trusted_login_state = login_state;
}
MaybeReturnAccounts(std::move(accounts));
}
void UserInfoRequest::MaybeReturnAccounts(
const std::vector<IdentityRequestAccountPtr>& accounts) {
DCHECK(!accounts.empty());
bool has_returning_accounts = false;
for (const auto& account : accounts) {
if (IsReturningAccount(*account)) {
has_returning_accounts = true;
break;
}
}
FedCmMetrics::NumAccounts num_accounts = FedCmMetrics::NumAccounts::kZero;
if (has_returning_accounts) {
num_accounts = accounts.size() == 1u ? FedCmMetrics::NumAccounts::kOne
: FedCmMetrics::NumAccounts::kMultiple;
}
base::UmaHistogramEnumeration("Blink.FedCm.UserInfo.NumAccounts",
num_accounts);
base::UmaHistogramMediumTimes("Blink.FedCm.UserInfo.TimeToRequestCompleted",
base::TimeTicks::Now() - request_start_time_);
if (!has_returning_accounts) {
CompleteWithError(
UserInfoRequestResult::kNoReturningUserFromFetchedAccounts);
return;
}
// The user previously accepted the FedCM prompt for one of the returned IdP
// accounts. Return data for all the IdP accounts.
std::vector<blink::mojom::IdentityUserInfoPtr> user_info;
std::vector<blink::mojom::IdentityUserInfoPtr> not_returning_accounts;
for (const auto& account : accounts) {
if (IsReturningAccount(*account)) {
user_info.push_back(blink::mojom::IdentityUserInfo::New(
account->email, account->given_name, account->name,
account->picture.spec()));
} else {
not_returning_accounts.push_back(blink::mojom::IdentityUserInfo::New(
account->email, account->given_name, account->name,
account->picture.spec()));
}
}
user_info.insert(user_info.end(),
std::make_move_iterator(not_returning_accounts.begin()),
std::make_move_iterator(not_returning_accounts.end()));
Complete(blink::mojom::RequestUserInfoStatus::kSuccess, std::move(user_info),
UserInfoRequestResult::kSuccess);
}
bool UserInfoRequest::IsReturningAccount(
const IdentityRequestAccount& account) {
// The |idp_claimed_login_state| will be std::nullopt if the IDP doesn't
// provide an |approved_clients| list and the |browser_trusted_login_state|
// will be |kSignUp| if there are no browser stored permission grants. The
// |idp_claimed_login_state| will be |kSignUp| if IDP provides an
// |approved_clients| AND the client id is NOT on the |approved_clients|
// list, in which case we trust the IDP that we should treat the user as a
// new user and shouldn't return the user info. This should override browser
// local stored permission since a user can revoke their account out of
// band.
if (account.idp_claimed_login_state.value_or(
account.browser_trusted_login_state) == LoginState::kSignUp) {
return false;
}
return HasSharingPermissionOrIdpHasThirdPartyCookiesAccess(
*render_frame_host_, idp_config_url_, embedding_origin_,
parent_frame_origin_, account.id, permission_delegate_,
api_permission_delegate_);
}
void UserInfoRequest::Complete(
blink::mojom::RequestUserInfoStatus status,
std::optional<std::vector<blink::mojom::IdentityUserInfoPtr>> user_info,
UserInfoRequestResult request_status) {
if (!callback_) {
return;
}
TRACE_EVENT_END("content.fedcm", perfetto_track_);
base::UmaHistogramEnumeration("Blink.FedCm.UserInfo.Status", request_status);
std::move(callback_).Run(status, std::move(user_info));
}
void UserInfoRequest::CompleteWithError(UserInfoRequestResult error) {
// Do not add a console error for an unhandled request: the RenderFrameHost
// may have been destroyed.
if (error != UserInfoRequestResult::kUnhandledRequest) {
render_frame_host_->AddMessageToConsole(
blink::mojom::ConsoleMessageLevel::kError,
GetConsoleErrorMessage(error));
AddDevToolsIssue(error);
}
Complete(blink::mojom::RequestUserInfoStatus::kError, std::nullopt, error);
}
void UserInfoRequest::AddDevToolsIssue(UserInfoRequestResult error) {
DCHECK_NE(error, UserInfoRequestResult::kSuccess);
auto details = blink::mojom::InspectorIssueDetails::New();
auto federated_auth_user_info_request_details =
blink::mojom::FederatedAuthUserInfoRequestIssueDetails::New(error);
details->federated_auth_user_info_request_details =
std::move(federated_auth_user_info_request_details);
render_frame_host_->ReportInspectorIssue(
blink::mojom::InspectorIssueInfo::New(
blink::mojom::InspectorIssueCode::kFederatedAuthUserInfoRequestIssue,
std::move(details)));
}
} // namespace content::webid