blob: 4f11129ad846c87b707b07438ea0954f916c63a6 [file] [log] [blame]
// 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/idp_network_request_manager.h"
#include "base/barrier_closure.h"
#include "base/base64.h"
#include "base/containers/flat_set.h"
#include "base/functional/callback_helpers.h"
#include "base/json/json_writer.h"
#include "base/strings/escape.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/strings/to_string.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/sequenced_task_runner.h"
#include "base/values.h"
#include "content/browser/devtools/devtools_instrumentation.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/browser/webid/flags.h"
#include "content/browser/webid/identity_provider_info.h"
#include "content/browser/webid/mappers.h"
#include "content/browser/webid/metrics.h"
#include "content/browser/webid/webid_utils.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/storage_partition.h"
#include "content/public/browser/webid/constants.h"
#include "content/public/browser/webid/federated_identity_permission_context_delegate.h"
#include "content/public/browser/webid/identity_request_dialog_controller.h"
#include "content/public/common/color_parser.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "net/base/isolation_info.h"
#include "net/base/load_flags.h"
#include "net/base/network_isolation_partition.h"
#include "net/base/registry_controlled_domains/registry_controlled_domain.h"
#include "net/base/schemeful_site.h"
#include "net/base/url_util.h"
#include "net/cookies/site_for_cookies.h"
#include "net/http/http_request_headers.h"
#include "net/http/http_status_code.h"
#include "services/data_decoder/public/cpp/data_decoder.h"
#include "services/data_decoder/public/cpp/decode_image.h"
#include "services/network/public/cpp/is_potentially_trustworthy.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "services/network/public/cpp/simple_url_loader.h"
#include "services/network/public/cpp/url_loader_completion_status.h"
#include "services/network/public/mojom/client_security_state.mojom.h"
#include "services/network/public/mojom/url_response_head.mojom.h"
#include "third_party/blink/public/common/manifest/manifest_icon_selector.h"
#include "third_party/blink/public/mojom/webid/federated_auth_request.mojom.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "ui/gfx/color_utils.h"
#include "ui/gfx/image/image.h"
#include "url/origin.h"
namespace content {
namespace {
using AccountsResponseInvalidReason =
IdpNetworkRequestManager::AccountsResponseInvalidReason;
using ClientMetadata = IdpNetworkRequestManager::ClientMetadata;
using Endpoints = IdpNetworkRequestManager::Endpoints;
using ErrorDialogType = IdpNetworkRequestManager::FedCmErrorDialogType;
using ErrorUrlType = IdpNetworkRequestManager::FedCmErrorUrlType;
using FetchStatus = IdpNetworkRequestManager::FetchStatus;
using LoginState = IdentityRequestAccount::LoginState;
using ParseStatus = IdpNetworkRequestManager::ParseStatus;
using TokenError = IdentityCredentialTokenError;
using TokenResponseType = IdpNetworkRequestManager::FedCmTokenResponseType;
using TokenResult = IdpNetworkRequestManager::TokenResult;
// Path to find the well-known file on the eTLD+1 host.
constexpr char kWellKnownPath[] = "/.well-known/web-identity";
// Well-known file JSON keys
constexpr char kProviderUrlListKey[] = "provider_urls";
// fedcm.json configuration keys.
constexpr char kIdAssertionEndpoint[] = "id_assertion_endpoint";
constexpr char kVcIssuanceEndpoint[] = "vc_issuance_endpoint";
constexpr char kClientMetadataEndpointKey[] = "client_metadata_endpoint";
constexpr char kMetricsEndpoint[] = "metrics_endpoint";
constexpr char kDisconnectEndpoint[] = "disconnect_endpoint";
constexpr char kModesKey[] = "modes";
constexpr char kTypesKey[] = "types";
constexpr char kFormatsKey[] = "formats";
constexpr char kAccountLabelKey[] = "account_label";
// Keys in the 'accounts' dictionary
constexpr char kIncludeKey[] = "include";
// Keys in the 'modes' dictionary.
constexpr char kActiveModeKey[] = "active";
constexpr char kPassiveModeKey[] = "passive";
// Keys in the specific mode dictionary.
constexpr char kSupportsUseOtherAccountKey[] = "supports_use_other_account";
// Shared between the well-known files and config files
constexpr char kAccountsEndpointKey[] = "accounts_endpoint";
constexpr char kLoginUrlKey[] = "login_url";
// Keys in fedcm.json 'branding' dictionary.
constexpr char kIdpBrandingBackgroundColorKey[] = "background_color";
constexpr char kIdpBrandingForegroundColorKey[] = "color";
// Client metadata keys.
constexpr char kPrivacyPolicyKey[] = "privacy_policy_url";
constexpr char kTermsOfServiceKey[] = "terms_of_service_url";
constexpr char kClientMatchesTopFrameOriginKey[] =
"client_matches_top_frame_origin";
// Accounts endpoint response keys.
constexpr char kAccountsKey[] = "accounts";
constexpr char kIdpBrandingKey[] = "branding";
// Keys in 'branding' 'icons' dictionary in config for the IDP icon and client
// metadata endpoint for the RP icon.
constexpr char kBrandingIconsKey[] = "icons";
constexpr char kBrandingIconUrl[] = "url";
constexpr char kBrandingIconSize[] = "size";
// The id assertion endpoint contains a token result.
constexpr char kTokenKey[] = "token";
// The id assertion endpoint contains a URL, which indicates that
// the serve wants to direct the user to continue on a pop-up
// window before it provides a token result.
constexpr char kContinueOnKey[] = "continue_on";
// The id assertion endpoint may contain an error dict containing a code and url
// which describes the error.
constexpr char kErrorKey[] = "error";
constexpr char kErrorCodeKey[] = "code";
constexpr char kErrorUrlKey[] = "url";
// Body content types.
constexpr char kUrlEncodedContentType[] = "application/x-www-form-urlencoded";
constexpr char kPlusJson[] = "+json";
constexpr char kApplicationJson[] = "application/json";
constexpr char kTextJson[] = "text/json";
// Error API codes.
constexpr char kGenericEmpty[] = "";
constexpr char kInvalidRequest[] = "invalid_request";
constexpr char kUnauthorizedClient[] = "unauthorized_client";
constexpr char kAccessDenied[] = "access_denied";
constexpr char kTemporarilyUnavailable[] = "temporarily_unavailable";
constexpr char kServerError[] = "server_error";
// Disconnect response keys.
constexpr char kDisconnectAccountId[] = "account_id";
// 1 MiB is an arbitrary upper bound that should account for any reasonable
// response size that is a part of this protocol.
constexpr int maxResponseSizeInKiB = 1024;
net::NetworkTrafficAnnotationTag CreateTrafficAnnotation() {
return net::DefineNetworkTrafficAnnotation("fedcm", R"(
semantics {
sender: "FedCM Backend"
description:
"The FedCM API allows websites to initiate user account login "
"with identity providers which provide federated sign-in "
"capabilities using OpenID Connect. The API provides a "
"browser-mediated alternative to previously existing federated "
"sign-in implementations."
trigger:
"A website executes the navigator.credentials.get() JavaScript "
"method to initiate federated user sign-in to a designated "
"provider."
data:
"An identity request contains a scope of claims specifying what "
"user information is being requested from the identity provider, "
"a label identifying the calling website application, and some "
"OpenID Connect protocol functional fields."
destination: WEBSITE
}
policy {
cookies_allowed: YES
cookies_store: "user"
setting: "Not user controlled. But the verification is a trusted "
"API that doesn't use user data."
policy_exception_justification:
"Not implemented, considered not useful as no content is being "
"uploaded; this request merely downloads the resources on the web."
})");
}
// Returns true for nullptr for easy use with Dict::FindString.
bool IsEmptyOrWhitespace(const std::string* input) {
if (!input) {
return true;
}
auto trimmed_string =
base::TrimWhitespace(base::UTF8ToUTF16(*input), base::TRIM_ALL);
return trimmed_string.empty();
}
GURL ResolveConfigUrl(const GURL& config_url, const std::string& endpoint) {
if (endpoint.empty())
return GURL();
return config_url.Resolve(endpoint);
}
GURL ExtractUrl(const base::Value::Dict& response, const char* key) {
const std::string* response_url = response.FindString(key);
if (!response_url) {
return GURL();
}
GURL url = GURL(*response_url);
if (!url.is_valid() || !url.SchemeIsHTTPOrHTTPS()) {
return GURL();
}
return url;
}
std::string ExtractString(const base::Value::Dict& response, const char* key) {
const std::string* str = response.FindString(key);
if (!str) {
return "";
}
return *str;
}
IdentityRequestAccountPtr ParseAccount(const base::Value::Dict& account,
const std::string& client_id) {
auto* id = account.FindString(webid::kAccountIdKey);
auto* email = account.FindString(webid::kAccountEmailKey);
auto* name = account.FindString(webid::kAccountNameKey);
auto* phone = account.FindString(webid::kAccountPhoneNumberKey);
auto* username = account.FindString(webid::kAccountUsernameKey);
auto* given_name = account.FindString(webid::kAccountGivenNameKey);
auto* picture = account.FindString(webid::kAccountPictureKey);
auto* approved_clients = account.FindList(webid::kAccountApprovedClientsKey);
std::vector<std::string> account_hints;
auto* hints = account.FindList(webid::kHintsKey);
if (hints) {
for (const base::Value& entry : *hints) {
if (entry.is_string()) {
account_hints.emplace_back(entry.GetString());
}
}
}
std::vector<std::string> domain_hints;
auto* domain_hints_list = account.FindList(webid::kDomainHintsKey);
if (domain_hints_list) {
for (const base::Value& entry : *domain_hints_list) {
if (entry.is_string()) {
domain_hints.emplace_back(entry.GetString());
}
}
}
std::vector<std::string> labels;
const base::ListValue* labels_list = nullptr;
if (webid::IsUseOtherAccountAndLabelsNewSyntaxEnabled()) {
labels_list = account.FindList(webid::kLabelHintsKey);
} else {
labels_list = account.FindList(webid::kLabelsKey);
}
if (labels_list) {
for (const base::Value& entry : *labels_list) {
if (entry.is_string()) {
labels.emplace_back(entry.GetString());
}
}
}
if (!id) {
return nullptr;
}
std::string display_identifier;
std::string display_name;
std::string empty_string;
if (webid::IsAlternativeIdentifiersEnabled()) {
std::vector<std::string_view> identifiers;
if (!IsEmptyOrWhitespace(name)) {
identifiers.emplace_back(*name);
} else {
name = &empty_string;
}
if (!IsEmptyOrWhitespace(username)) {
identifiers.emplace_back(*username);
}
if (!IsEmptyOrWhitespace(email)) {
identifiers.emplace_back(*email);
} else {
email = &empty_string;
}
if (!IsEmptyOrWhitespace(phone)) {
identifiers.emplace_back(*phone);
}
if (identifiers.empty()) {
return nullptr;
}
display_name = identifiers[0];
if (identifiers.size() > 1) {
display_identifier = identifiers[1];
}
} else {
// required fields
// TODO(crbug.com/40849405): validate email address.
if (IsEmptyOrWhitespace(email) || IsEmptyOrWhitespace(name)) {
return nullptr;
}
display_identifier = *email;
display_name = *name;
}
webid::RecordApprovedClientsExistence(approved_clients != nullptr);
std::optional<LoginState> approved_value;
if (approved_clients) {
for (const base::Value& entry : *approved_clients) {
if (entry.is_string() && entry.GetString() == client_id) {
approved_value = LoginState::kSignIn;
break;
}
}
if (!approved_value) {
// We did get an approved_clients list, but the client ID was not found.
// This means we are certain that the client is not approved; set to
// kSignUp instead of leaving as nullopt.
approved_value = LoginState::kSignUp;
}
webid::RecordApprovedClientsSize(approved_clients->size());
}
return base::MakeRefCounted<IdentityRequestAccount>(
*id, display_identifier, display_name, *email, *name,
given_name ? *given_name : "", picture ? GURL(*picture) : GURL(),
phone ? *phone : "", username ? *username : "", std::move(account_hints),
std::move(domain_hints), std::move(labels), approved_value,
/*browser_trusted_login_state=*/LoginState::kSignUp);
}
// Parses accounts from given Value. Returns true if parse is successful and
// adds parsed accounts to the |account_list|.
bool ParseAccounts(const base::Value::List& accounts,
std::vector<IdentityRequestAccountPtr>& account_list,
const std::string& client_id,
bool from_accounts_push,
AccountsResponseInvalidReason& parsing_error) {
DCHECK(account_list.empty());
base::flat_set<std::string> account_ids;
for (auto& account : accounts) {
const base::Value::Dict* account_dict = account.GetIfDict();
if (!account_dict) {
parsing_error = AccountsResponseInvalidReason::kAccountIsNotDict;
return false;
}
IdentityRequestAccountPtr parsed_account =
ParseAccount(*account_dict, client_id);
if (parsed_account) {
if (account_ids.count(parsed_account->id)) {
parsing_error = AccountsResponseInvalidReason::kAccountsShareSameId;
return false;
}
parsed_account->from_accounts_push = from_accounts_push;
account_ids.insert(parsed_account->id);
account_list.push_back(std::move(parsed_account));
} else {
parsing_error =
AccountsResponseInvalidReason::kAccountMissesRequiredField;
return false;
}
}
DCHECK(!account_list.empty());
return true;
}
std::optional<SkColor> ParseCssColor(const std::string* value) {
if (value == nullptr)
return std::nullopt;
SkColor color;
if (!ParseCssColorString(*value, &color)) {
return std::nullopt;
}
return SkColorSetA(color, 0xff);
}
GURL FindBestMatchingIconUrl(const base::Value::List* icons_value,
int brand_icon_ideal_size,
int brand_icon_minimum_size,
const GURL& config_url) {
std::vector<blink::Manifest::ImageResource> icons;
for (const base::Value& icon_value : *icons_value) {
const base::Value::Dict* icon_value_dict = icon_value.GetIfDict();
if (!icon_value_dict) {
continue;
}
const std::string* icon_src = icon_value_dict->FindString(kBrandingIconUrl);
if (!icon_src) {
continue;
}
blink::Manifest::ImageResource icon;
if (!config_url.is_empty()) {
icon.src = config_url.Resolve(*icon_src);
} else {
icon.src = GURL(*icon_src);
}
if (!icon.src.is_valid() || !icon.src.SchemeIsHTTPOrHTTPS()) {
continue;
}
icon.purpose = {blink::mojom::ManifestImageResource_Purpose::MASKABLE};
std::optional<int> icon_size = icon_value_dict->FindInt(kBrandingIconSize);
int icon_size_int = icon_size.value_or(0);
icon.sizes.emplace_back(icon_size_int, icon_size_int);
icons.push_back(icon);
}
return blink::ManifestIconSelector::FindBestMatchingSquareIcon(
icons, brand_icon_ideal_size, brand_icon_minimum_size,
blink::mojom::ManifestImageResource_Purpose::MASKABLE);
}
// Parse IdentityProviderMetadata from given value. Overwrites |idp_metadata|
// with the parsed value.
void ParseIdentityProviderMetadata(const base::Value::Dict& idp_metadata_value,
int brand_icon_ideal_size,
int brand_icon_minimum_size,
IdentityProviderMetadata& idp_metadata) {
idp_metadata.brand_background_color = ParseCssColor(
idp_metadata_value.FindString(kIdpBrandingBackgroundColorKey));
idp_metadata.brand_text_color = ParseCssColor(
idp_metadata_value.FindString(kIdpBrandingForegroundColorKey));
const base::Value::List* icons_value =
idp_metadata_value.FindList(kBrandingIconsKey);
if (!icons_value) {
return;
}
idp_metadata.brand_icon_url =
FindBestMatchingIconUrl(icons_value, brand_icon_ideal_size,
brand_icon_minimum_size, idp_metadata.config_url);
}
// This method follows https://mimesniff.spec.whatwg.org/#json-mime-type.
bool IsJsonMimeType(const std::string& mime_type) {
if (base::EndsWith(mime_type, kPlusJson)) {
return true;
}
return mime_type == kApplicationJson || mime_type == kTextJson;
}
ParseStatus GetResponseError(std::string* response_body,
int response_code,
const std::string& mime_type) {
if (response_code == net::HTTP_NOT_FOUND) {
return ParseStatus::kHttpNotFoundError;
}
if (!response_body) {
return ParseStatus::kNoResponseError;
}
if (!IsJsonMimeType(mime_type)) {
return ParseStatus::kInvalidContentTypeError;
}
return ParseStatus::kSuccess;
}
ParseStatus GetParsingError(
const data_decoder::DataDecoder::ValueOrError& result) {
if (!result.has_value())
return ParseStatus::kInvalidResponseError;
return result->GetIfDict() ? ParseStatus::kSuccess
: ParseStatus::kInvalidResponseError;
}
void OnJsonParsed(
IdpNetworkRequestManager::ParseJsonCallback parse_json_callback,
int response_code,
data_decoder::DataDecoder::ValueOrError result) {
ParseStatus parse_status = GetParsingError(result);
std::move(parse_json_callback)
.Run({parse_status, response_code}, std::move(result));
}
void OnDownloadedJson(
IdpNetworkRequestManager::ParseJsonCallback parse_json_callback,
std::unique_ptr<std::string> response_body,
int response_code,
const std::string& mime_type,
bool cors_error = false) {
ParseStatus parse_status =
GetResponseError(response_body.get(), response_code, mime_type);
if (parse_status != ParseStatus::kSuccess) {
std::move(parse_json_callback)
.Run({parse_status, response_code, cors_error},
data_decoder::DataDecoder::ValueOrError());
return;
}
data_decoder::DataDecoder::ParseJsonIsolated(
*response_body,
base::BindOnce(&OnJsonParsed, std::move(parse_json_callback),
response_code));
}
GURL ExtractEndpoint(const GURL& provider,
const base::Value::Dict& response,
const char* key) {
const std::string* endpoint = response.FindString(key);
if (!endpoint) {
return GURL();
}
return ResolveConfigUrl(provider, *endpoint);
}
void OnWellKnownParsed(
IdpNetworkRequestManager::FetchWellKnownCallback callback,
const GURL& well_known_url,
FetchStatus fetch_status,
data_decoder::DataDecoder::ValueOrError result) {
if (callback.IsCancelled())
return;
IdpNetworkRequestManager::WellKnown well_known;
std::set<GURL> urls;
if (fetch_status.parse_status != ParseStatus::kSuccess) {
std::move(callback).Run(fetch_status, std::move(well_known));
return;
}
const base::Value::Dict* dict = result->GetIfDict();
if (!dict) {
std::move(callback).Run(
{ParseStatus::kInvalidResponseError, fetch_status.response_code},
std::move(well_known));
return;
}
well_known.accounts =
ExtractEndpoint(well_known_url, *dict, kAccountsEndpointKey);
well_known.login_url = ExtractEndpoint(well_known_url, *dict, kLoginUrlKey);
if (!well_known.accounts.is_empty() && !well_known.login_url.is_empty() &&
!dict->Find(kProviderUrlListKey)) {
std::move(callback).Run(fetch_status, std::move(well_known));
return;
}
const base::Value::List* list = dict->FindList(kProviderUrlListKey);
if (!list) {
std::move(callback).Run(
{ParseStatus::kInvalidResponseError, fetch_status.response_code},
std::move(well_known));
return;
}
if (list->empty()) {
std::move(callback).Run(
{ParseStatus::kEmptyListError, fetch_status.response_code},
std::move(well_known));
return;
}
for (const auto& value : *list) {
const std::string* url_str = value.GetIfString();
if (!url_str) {
std::move(callback).Run(
{ParseStatus::kInvalidResponseError, fetch_status.response_code},
std::move(well_known));
return;
}
GURL url(*url_str);
if (!url.is_valid()) {
url = well_known_url.Resolve(*url_str);
}
urls.insert(url);
}
well_known.provider_urls = std::move(urls);
std::move(callback).Run({ParseStatus::kSuccess, fetch_status.response_code},
std::move(well_known));
}
void OnConfigParsed(const GURL& provider,
blink::mojom::RpMode rp_mode,
int idp_brand_icon_ideal_size,
int idp_brand_icon_minimum_size,
IdpNetworkRequestManager::FetchConfigCallback callback,
FetchStatus fetch_status,
data_decoder::DataDecoder::ValueOrError result) {
if (fetch_status.parse_status != ParseStatus::kSuccess) {
std::move(callback).Run(fetch_status, Endpoints(),
IdentityProviderMetadata());
return;
}
const base::Value::Dict& response = result->GetDict();
Endpoints endpoints;
endpoints.token = ExtractEndpoint(provider, response, kIdAssertionEndpoint);
endpoints.accounts =
ExtractEndpoint(provider, response, kAccountsEndpointKey);
endpoints.client_metadata =
ExtractEndpoint(provider, response, kClientMetadataEndpointKey);
endpoints.metrics = ExtractEndpoint(provider, response, kMetricsEndpoint);
endpoints.disconnect =
ExtractEndpoint(provider, response, kDisconnectEndpoint);
endpoints.issuance = ExtractEndpoint(provider, response, kVcIssuanceEndpoint);
const base::Value::Dict* idp_metadata_value =
response.FindDict(kIdpBrandingKey);
IdentityProviderMetadata idp_metadata;
idp_metadata.config_url = provider;
if (idp_metadata_value) {
ParseIdentityProviderMetadata(*idp_metadata_value,
idp_brand_icon_ideal_size,
idp_brand_icon_minimum_size, idp_metadata);
}
idp_metadata.idp_login_url =
ExtractEndpoint(provider, response, kLoginUrlKey);
if (webid::IsDelegationEnabled()) {
const base::Value::List* formats = response.FindList(kFormatsKey);
if (formats) {
for (const auto& format : *formats) {
if (format.is_string()) {
idp_metadata.formats.push_back(format.GetString());
}
}
}
}
if (webid::IsIdPRegistrationEnabled()) {
const base::Value::List* types = response.FindList(kTypesKey);
if (types) {
for (const auto& type : *types) {
if (type.is_string()) {
idp_metadata.types.push_back(type.GetString());
}
}
}
}
const std::string* requested_label = nullptr;
if (webid::IsUseOtherAccountAndLabelsNewSyntaxEnabled()) {
requested_label = response.FindString(kAccountLabelKey);
} else {
const base::Value::Dict* accounts_dict = response.FindDict(kAccountsKey);
if (accounts_dict) {
requested_label = accounts_dict->FindString(kIncludeKey);
}
}
if (requested_label) {
idp_metadata.requested_label = *requested_label;
}
std::optional<bool> supports_add_account;
if (webid::IsUseOtherAccountAndLabelsNewSyntaxEnabled()) {
supports_add_account = response.FindBool(kSupportsUseOtherAccountKey);
} else {
const base::Value::Dict* modes_dict = response.FindDict(kModesKey);
const base::Value::Dict* selected_mode_dict = nullptr;
if (modes_dict) {
switch (rp_mode) {
case blink::mojom::RpMode::kPassive:
selected_mode_dict = modes_dict->FindDict(kPassiveModeKey);
break;
case blink::mojom::RpMode::kActive:
selected_mode_dict = modes_dict->FindDict(kActiveModeKey);
break;
}
}
if (selected_mode_dict) {
supports_add_account =
selected_mode_dict->FindBool(kSupportsUseOtherAccountKey);
}
}
if (supports_add_account) {
idp_metadata.supports_add_account = *supports_add_account;
}
std::move(callback).Run({ParseStatus::kSuccess, fetch_status.response_code},
endpoints, std::move(idp_metadata));
}
void OnClientMetadataParsed(
bool is_cross_site_iframe,
int rp_brand_icon_ideal_size,
int rp_brand_icon_minimum_size,
IdpNetworkRequestManager::FetchClientMetadataCallback callback,
FetchStatus fetch_status,
data_decoder::DataDecoder::ValueOrError result) {
if (fetch_status.parse_status != ParseStatus::kSuccess) {
std::move(callback).Run(fetch_status, ClientMetadata());
return;
}
IdpNetworkRequestManager::ClientMetadata data;
const base::Value::Dict& response = result->GetDict();
data.privacy_policy_url = ExtractUrl(response, kPrivacyPolicyKey);
data.terms_of_service_url = ExtractUrl(response, kTermsOfServiceKey);
if (is_cross_site_iframe) {
data.client_matches_top_frame_origin =
response.FindBool(kClientMatchesTopFrameOriginKey);
}
const base::Value::List* icons_value = response.FindList(kBrandingIconsKey);
if (icons_value) {
data.brand_icon_url =
FindBestMatchingIconUrl(icons_value, rp_brand_icon_ideal_size,
rp_brand_icon_minimum_size, GURL());
}
std::move(callback).Run({ParseStatus::kSuccess, fetch_status.response_code},
data);
}
void OnAccountsRequestParsed(
std::string client_id,
IdpNetworkRequestManager::AccountsRequestCallback callback,
FetchStatus fetch_status,
data_decoder::DataDecoder::ValueOrError result) {
std::vector<IdentityRequestAccountPtr> account_list;
if (fetch_status.parse_status != ParseStatus::kSuccess) {
webid::RecordAccountsResponseInvalidReason(
AccountsResponseInvalidReason::kResponseIsNotJsonOrDict);
std::move(callback).Run(fetch_status, account_list);
return;
}
const base::Value::Dict& response = result->GetDict();
const base::Value::List* accounts = response.FindList(kAccountsKey);
if (!accounts) {
webid::RecordAccountsResponseInvalidReason(
AccountsResponseInvalidReason::kNoAccountsKey);
std::move(callback).Run(
{ParseStatus::kInvalidResponseError, fetch_status.response_code},
account_list);
return;
}
if (accounts->empty()) {
webid::RecordAccountsResponseInvalidReason(
AccountsResponseInvalidReason::kAccountListIsEmpty);
std::move(callback).Run(
{ParseStatus::kEmptyListError, fetch_status.response_code},
account_list);
return;
}
AccountsResponseInvalidReason parsing_error =
AccountsResponseInvalidReason::kResponseIsNotJsonOrDict;
bool accounts_valid =
ParseAccounts(*accounts, account_list, client_id,
fetch_status.from_accounts_push, parsing_error);
if (!accounts_valid) {
CHECK_NE(parsing_error,
AccountsResponseInvalidReason::kResponseIsNotJsonOrDict);
webid::RecordAccountsResponseInvalidReason(parsing_error);
std::move(callback).Run(
{ParseStatus::kInvalidResponseError, fetch_status.response_code},
std::vector<IdentityRequestAccountPtr>());
return;
}
std::move(callback).Run({ParseStatus::kSuccess, fetch_status.response_code},
std::move(account_list));
}
std::pair<GURL, std::optional<ErrorUrlType>> GetErrorUrlAndType(
const std::string* url,
const GURL& idp_url) {
if (!url || url->empty()) {
return std::make_pair(GURL(), std::nullopt);
}
GURL error_url = idp_url.Resolve(*url);
if (!error_url.is_valid()) {
return std::make_pair(GURL(), std::nullopt);
}
url::Origin error_origin = url::Origin::Create(error_url);
url::Origin idp_origin = url::Origin::Create(idp_url);
if (error_origin == idp_origin) {
return std::make_pair(error_url, ErrorUrlType::kSameOrigin);
}
if (!net::SchemefulSite::IsSameSite(error_origin, idp_origin)) {
return std::make_pair(GURL(), ErrorUrlType::kCrossSite);
}
return std::make_pair(error_url, ErrorUrlType::kCrossOriginSameSite);
}
ErrorDialogType GetErrorDialogType(const std::string& code, const GURL& url) {
bool has_url = !url.is_empty();
if (code == kGenericEmpty) {
return has_url ? ErrorDialogType::kGenericEmptyWithUrl
: ErrorDialogType::kGenericEmptyWithoutUrl;
} else if (code == kInvalidRequest) {
return has_url ? ErrorDialogType::kInvalidRequestWithUrl
: ErrorDialogType::kInvalidRequestWithoutUrl;
} else if (code == kUnauthorizedClient) {
return has_url ? ErrorDialogType::kUnauthorizedClientWithUrl
: ErrorDialogType::kUnauthorizedClientWithoutUrl;
} else if (code == kAccessDenied) {
return has_url ? ErrorDialogType::kAccessDeniedWithUrl
: ErrorDialogType::kAccessDeniedWithoutUrl;
} else if (code == kTemporarilyUnavailable) {
return has_url ? ErrorDialogType::kTemporarilyUnavailableWithUrl
: ErrorDialogType::kTemporarilyUnavailableWithoutUrl;
} else if (code == kServerError) {
return has_url ? ErrorDialogType::kServerErrorWithUrl
: ErrorDialogType::kServerErrorWithoutUrl;
}
return has_url ? ErrorDialogType::kGenericNonEmptyWithUrl
: ErrorDialogType::kGenericNonEmptyWithoutUrl;
}
TokenResponseType GetTokenResponseType(const std::string* token,
const std::string* continue_on,
const base::Value::Dict* error) {
if (token && error && !continue_on) {
return TokenResponseType::
kTokenReceivedAndErrorReceivedAndContinueOnNotReceived;
} else if (token && !error && !continue_on) {
return TokenResponseType::
kTokenReceivedAndErrorNotReceivedAndContinueOnNotReceived;
} else if (!token && error && !continue_on) {
return TokenResponseType::
kTokenNotReceivedAndErrorReceivedAndContinueOnNotReceived;
} else if (token && !error && continue_on) {
return TokenResponseType::
kTokenReceivedAndErrorNotReceivedAndContinueOnReceived;
} else if (token && error && continue_on) {
return TokenResponseType::
kTokenReceivedAndErrorReceivedAndContinueOnReceived;
} else if (!token && !error && continue_on) {
return TokenResponseType::
kTokenNotReceivedAndErrorNotReceivedAndContinueOnReceived;
} else if (!token && error && continue_on) {
return TokenResponseType::
kTokenNotReceivedAndErrorReceivedAndContinueOnReceived;
}
DCHECK(!token);
DCHECK(!error);
DCHECK(!continue_on);
return TokenResponseType::
kTokenNotReceivedAndErrorNotReceivedAndContinueOnNotReceived;
}
bool IsOkResponseCode(int response_code) {
return response_code / 100 == 2;
}
ErrorDialogType GetErrorDialogTypeAndSetTokenError(int response_code,
TokenResult& token_result) {
if (response_code == net::HTTP_INTERNAL_SERVER_ERROR) {
token_result.error = TokenError{kServerError, GURL()};
return ErrorDialogType::kServerErrorWithoutUrl;
}
if (response_code == net::HTTP_SERVICE_UNAVAILABLE) {
token_result.error = TokenError{kTemporarilyUnavailable, GURL()};
return ErrorDialogType::kTemporarilyUnavailableWithoutUrl;
}
token_result.error = TokenError{kGenericEmpty, GURL()};
return ErrorDialogType::kGenericEmptyWithoutUrl;
}
void OnTokenRequestParsed(
IdpNetworkRequestManager::TokenRequestCallback callback,
IdpNetworkRequestManager::ContinueOnCallback continue_on_callback,
IdpNetworkRequestManager::RecordErrorMetricsCallback
record_error_metrics_callback,
const GURL& token_url,
FetchStatus fetch_status,
data_decoder::DataDecoder::ValueOrError result) {
TokenResult token_result;
bool parse_succeeded = fetch_status.parse_status == ParseStatus::kSuccess;
DCHECK(!parse_succeeded || result.has_value());
// We need to handle a number of cases, in order:
// 1) Result has a custom error field - return error
// 2) Parsing the result failed - return error
// 3) HTTP error code - return error
// 4) Result has token - return success
// 5) Result has continue_on URL - return success
// 6) Neither token nor continue_on nor HTTP error - return error
const base::Value::Dict* response =
parse_succeeded ? &result->GetDict() : nullptr;
bool can_use_response =
response && IsOkResponseCode(fetch_status.response_code);
const std::string* token =
can_use_response ? response->FindString(kTokenKey) : nullptr;
// continue_on_callback is only set if authz is enabled.
const std::string* continue_on = can_use_response && continue_on_callback
? response->FindString(kContinueOnKey)
: nullptr;
const base::Value::Dict* response_error =
response ? response->FindDict(kErrorKey) : nullptr;
TokenResponseType token_response_type =
GetTokenResponseType(token, continue_on, response_error);
if (response_error) {
std::string error_code = ExtractString(*response_error, kErrorCodeKey);
const std::string* url = response_error->FindString(kErrorUrlKey);
GURL error_url;
std::optional<ErrorUrlType> error_url_type;
std::tie(error_url, error_url_type) = GetErrorUrlAndType(url, token_url);
token_result.error = TokenError{error_code, error_url};
std::move(record_error_metrics_callback)
.Run(token_response_type, GetErrorDialogType(error_code, error_url),
error_url_type);
std::move(callback).Run({ParseStatus::kSuccess, fetch_status.response_code},
token_result);
return;
}
if (!parse_succeeded || !IsOkResponseCode(fetch_status.response_code)) {
ErrorDialogType type = GetErrorDialogTypeAndSetTokenError(
fetch_status.response_code, token_result);
std::move(record_error_metrics_callback)
.Run(token_response_type, type, /*error_url_type=*/std::nullopt);
if (parse_succeeded) {
fetch_status.parse_status = ParseStatus::kInvalidResponseError;
}
std::move(callback).Run(fetch_status, token_result);
return;
}
DCHECK(response);
if (token) {
token_result.token = *token;
std::move(record_error_metrics_callback)
.Run(token_response_type, /*error_dialog_type=*/std::nullopt,
/*error_url_type=*/std::nullopt);
std::move(callback).Run({ParseStatus::kSuccess, fetch_status.response_code},
token_result);
return;
}
if (continue_on) {
GURL url = token_url.Resolve(*continue_on);
if (url.is_valid()) {
std::move(record_error_metrics_callback)
.Run(token_response_type, /*error_dialog_type=*/std::nullopt,
/*error_url_type=*/std::nullopt);
std::move(continue_on_callback)
.Run({ParseStatus::kSuccess, fetch_status.response_code},
std::move(url));
return;
}
}
ErrorDialogType type = GetErrorDialogTypeAndSetTokenError(
fetch_status.response_code, token_result);
std::move(record_error_metrics_callback)
.Run(token_response_type, type, /*error_url_type=*/std::nullopt);
std::move(callback).Run(
{ParseStatus::kInvalidResponseError, fetch_status.response_code},
token_result);
}
void OnLogoutCompleted(IdpNetworkRequestManager::LogoutCallback callback,
std::unique_ptr<std::string> response_body,
int response_code,
const std::string& mime_type,
bool cors_error) {
std::move(callback).Run();
}
void OnDisconnectResponseParsed(
IdpNetworkRequestManager::DisconnectCallback callback,
FetchStatus fetch_status,
data_decoder::DataDecoder::ValueOrError result) {
if (fetch_status.parse_status != ParseStatus::kSuccess) {
std::move(callback).Run(fetch_status, /*account_id=*/"");
return;
}
const base::Value::Dict& response = result->GetDict();
const std::string* account_id = response.FindString(kDisconnectAccountId);
if (account_id && !account_id->empty()) {
std::move(callback).Run(fetch_status, *account_id);
return;
}
std::move(callback).Run(
{ParseStatus::kInvalidResponseError, fetch_status.response_code},
/*account_id=*/"");
}
} // namespace
IdpNetworkRequestManager::Endpoints::Endpoints() = default;
IdpNetworkRequestManager::Endpoints::~Endpoints() = default;
IdpNetworkRequestManager::Endpoints::Endpoints(const Endpoints& other) =
default;
IdpNetworkRequestManager::WellKnown::WellKnown() = default;
IdpNetworkRequestManager::WellKnown::~WellKnown() = default;
IdpNetworkRequestManager::WellKnown::WellKnown(const WellKnown& other) =
default;
IdpNetworkRequestManager::ClientMetadata::ClientMetadata() = default;
IdpNetworkRequestManager::ClientMetadata::~ClientMetadata() = default;
IdpNetworkRequestManager::ClientMetadata::ClientMetadata(
const ClientMetadata& other) = default;
IdpNetworkRequestManager::TokenResult::TokenResult() = default;
IdpNetworkRequestManager::TokenResult::~TokenResult() = default;
IdpNetworkRequestManager::TokenResult::TokenResult(const TokenResult& other) =
default;
// static
std::unique_ptr<IdpNetworkRequestManager> IdpNetworkRequestManager::Create(
RenderFrameHostImpl* host) {
// Use the browser process URL loader factory because it has cross-origin
// read blocking disabled. This is safe because even though these are
// renderer-initiated fetches, the browser parses the responses and does not
// leak the values to the renderer. The renderer should only learn information
// when the user selects an account to sign in.
return std::make_unique<IdpNetworkRequestManager>(
host->GetLastCommittedOrigin(),
host->GetMainFrame()->GetLastCommittedOrigin(),
host->GetStoragePartition()->GetURLLoaderFactoryForBrowserProcess(),
host->GetBrowserContext()->GetFederatedIdentityPermissionContext(),
host->BuildClientSecurityState(), host->GetFrameTreeNodeId());
}
IdpNetworkRequestManager::IdpNetworkRequestManager(
const url::Origin& relying_party_origin,
const url::Origin& rp_embedding_origin,
scoped_refptr<network::SharedURLLoaderFactory> loader_factory,
FederatedIdentityPermissionContextDelegate* permission_delegate,
network::mojom::ClientSecurityStatePtr client_security_state,
content::FrameTreeNodeId frame_tree_node_id)
: relying_party_origin_(relying_party_origin),
rp_embedding_origin_(rp_embedding_origin),
loader_factory_(loader_factory),
permission_delegate_(permission_delegate),
client_security_state_(std::move(client_security_state)),
frame_tree_node_id_(frame_tree_node_id) {
DCHECK(client_security_state_);
// If COEP:credentialless was used, this would break FedCM credentialled
// requests. We clear the Cross-Origin-Embedder-Policy because FedCM responses
// are not really embedded in the page. They do not enter the renderer
// process. This is safe because FedCM does not leak any data to the
// requesting page except for the final issued token, and we only get that
// token if the server is a new FedCM server, on which we can rely to validate
// requestors if they want to.
client_security_state_->cross_origin_embedder_policy =
network::CrossOriginEmbedderPolicy();
}
IdpNetworkRequestManager::~IdpNetworkRequestManager() = default;
// static
std::optional<GURL> IdpNetworkRequestManager::ComputeWellKnownUrl(
const GURL& provider) {
GURL well_known_url;
if (net::IsLocalhost(provider)) {
well_known_url = provider.GetWithEmptyPath();
} else {
std::string etld_plus_one = GetDomainAndRegistry(
provider, net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES);
if (etld_plus_one.empty())
return std::nullopt;
well_known_url = GURL(provider.scheme() + "://" + etld_plus_one);
}
GURL::Replacements replacements;
replacements.SetPathStr(kWellKnownPath);
return well_known_url.ReplaceComponents(replacements);
}
void IdpNetworkRequestManager::FetchWellKnown(const GURL& provider,
FetchWellKnownCallback callback) {
std::optional<GURL> well_known_url =
IdpNetworkRequestManager::ComputeWellKnownUrl(provider);
if (!well_known_url) {
// Pass net::HTTP_OK as the |response_code| so we do not add a console error
// message about a fetch we didn't even attempt.
FetchStatus fetch_status = {ParseStatus::kHttpNotFoundError, net::HTTP_OK};
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, base::BindOnce(&OnWellKnownParsed, std::move(callback),
/*well_known_url=*/GURL(), fetch_status,
data_decoder::DataDecoder::ValueOrError()));
return;
}
std::unique_ptr<network::ResourceRequest> resource_request =
CreateUncredentialedResourceRequest(*well_known_url,
/*send_origin=*/false,
/* follow_redirects= */ true);
DownloadJsonAndParse(
std::move(resource_request),
/*url_encoded_post_data=*/std::nullopt,
base::BindOnce(&OnWellKnownParsed, std::move(callback), *well_known_url),
maxResponseSizeInKiB * 1024);
}
void IdpNetworkRequestManager::FetchConfig(const GURL& provider,
blink::mojom::RpMode rp_mode,
int idp_brand_icon_ideal_size,
int idp_brand_icon_minimum_size,
FetchConfigCallback callback) {
std::unique_ptr<network::ResourceRequest> resource_request =
CreateUncredentialedResourceRequest(provider,
/* send_origin= */ false);
DownloadJsonAndParse(
std::move(resource_request),
/*url_encoded_post_data=*/std::nullopt,
base::BindOnce(&OnConfigParsed, provider, rp_mode,
idp_brand_icon_ideal_size, idp_brand_icon_minimum_size,
std::move(callback)),
maxResponseSizeInKiB * 1024);
}
void IdpNetworkRequestManager::SendAccountsRequest(
const url::Origin& idp_origin,
const GURL& accounts_url,
const std::string& client_id,
AccountsRequestCallback callback) {
if (webid::IsLightweightModeEnabled()) {
base::Value::List accounts = permission_delegate_->GetAccounts(idp_origin);
FetchStatus success_status = {
.parse_status = ParseStatus::kSuccess,
.response_code = 200,
.from_accounts_push = true,
};
if (accounts.size() > 0) {
OnAccountsRequestParsed(
client_id, std::move(callback), success_status,
data_decoder::DataDecoder::ValueOrError(
base::Value::Dict().Set(kAccountsKey, std::move(accounts))));
return;
}
// If there were no stored accounts and the supplied accounts URL is empty,
// behave as though we received an empty accounts_endpoint response.
if (accounts_url.is_empty()) {
OnAccountsRequestParsed(
client_id, std::move(callback), success_status,
data_decoder::DataDecoder::ValueOrError(
base::Value::Dict().Set(kAccountsKey, base::Value::List())));
return;
}
}
std::unique_ptr<network::ResourceRequest> resource_request =
CreateCredentialedResourceRequest(
accounts_url, CredentialedResourceRequestType::kNoOrigin);
DownloadJsonAndParse(
std::move(resource_request),
/*url_encoded_post_data=*/std::nullopt,
base::BindOnce(&OnAccountsRequestParsed, client_id, std::move(callback)),
maxResponseSizeInKiB * 1024);
}
void IdpNetworkRequestManager::SendTokenRequest(
const GURL& token_url,
const std::string& account,
const std::string& url_encoded_post_data,
bool idp_blindness,
TokenRequestCallback callback,
ContinueOnCallback continue_on,
RecordErrorMetricsCallback record_error_metrics_callback) {
std::unique_ptr<network::ResourceRequest> resource_request =
CreateCredentialedResourceRequest(
token_url, idp_blindness
? CredentialedResourceRequestType::kNoOrigin
: CredentialedResourceRequestType::kOriginWithCORS);
if (idp_blindness) {
// IdP blindness can only be used when the feature is enabled.
DCHECK(webid::IsDelegationEnabled());
// We have to set this to a Origin: null because the underlying loader
// will not let us send a request without Origin header if the request
// method is POST.
resource_request->request_initiator = url::Origin();
}
DownloadJsonAndParse(
std::move(resource_request), url_encoded_post_data,
base::BindOnce(&OnTokenRequestParsed, std::move(callback),
std::move(continue_on),
std::move(record_error_metrics_callback), token_url),
maxResponseSizeInKiB * 1024,
// We should parse the response body for the ID assertion endpoint request
// even if the response code is non-2xx because the server may include the
// error details with the Error API.
/*allow_http_error_results=*/true);
}
void IdpNetworkRequestManager::SendSuccessfulTokenRequestMetrics(
const GURL& metrics_endpoint_url,
base::TimeDelta api_call_to_show_dialog_time,
base::TimeDelta show_dialog_to_continue_clicked_time,
base::TimeDelta account_selected_to_token_response_time,
base::TimeDelta api_call_to_token_response_time) {
std::string url_encoded_post_data = base::StringPrintf(
"outcome=success"
"&time_to_show_ui=%d"
"&time_to_continue=%d"
"&time_to_receive_token=%d"
"&turnaround_time=%d",
static_cast<int>(api_call_to_show_dialog_time.InMilliseconds()),
static_cast<int>(show_dialog_to_continue_clicked_time.InMilliseconds()),
static_cast<int>(
account_selected_to_token_response_time.InMilliseconds()),
static_cast<int>(api_call_to_token_response_time.InMilliseconds()));
std::unique_ptr<network::ResourceRequest> resource_request =
CreateCredentialedResourceRequest(
metrics_endpoint_url,
CredentialedResourceRequestType::kOriginWithoutCORS);
// Typically, this IdpNetworkRequestManager will be destroyed after
// we return, but because the SimpleURLLoader is owned by the callback
// object, the load will not be aborted.
// The result of the download is not important, so we pass an empty
// DownloadCallback.
DownloadUrl(std::move(resource_request), url_encoded_post_data,
DownloadCallback(), maxResponseSizeInKiB * 1024);
}
void IdpNetworkRequestManager::SendFailedTokenRequestMetrics(
const GURL& metrics_endpoint_url,
bool did_show_ui,
webid::MetricsEndpointErrorCode error_code) {
std::string url_encoded_post_data = base::StringPrintf(
"outcome=failure&error_code=%d&did_show_ui=%s",
static_cast<int>(error_code), base::ToString(did_show_ui));
std::unique_ptr<network::ResourceRequest> resource_request =
CreateUncredentialedResourceRequest(metrics_endpoint_url,
/*send_origin=*/false);
// Typically, this IdpNetworkRequestManager will be destroyed after
// we return, but because the SimpleURLLoader is owned by the callback
// object, the load will not be aborted.
// The result of the download is not important, so we pass an empty
// DownloadCallback.
DownloadUrl(std::move(resource_request), url_encoded_post_data,
DownloadCallback(), maxResponseSizeInKiB * 1024);
}
void IdpNetworkRequestManager::SendLogout(const GURL& logout_url,
LogoutCallback callback) {
// TODO(kenrb): Add browser test verifying that the response to this can
// clear cookies. https://crbug.com/1155312.
auto resource_request = CreateCredentialedResourceRequest(
logout_url, CredentialedResourceRequestType::kNoOrigin);
resource_request->headers.SetHeader(net::HttpRequestHeaders::kAccept, "*/*");
DownloadUrl(std::move(resource_request),
/*url_encoded_post_data=*/std::nullopt,
base::BindOnce(&OnLogoutCompleted, std::move(callback)),
maxResponseSizeInKiB * 1024);
}
void IdpNetworkRequestManager::SendDisconnectRequest(
const GURL& disconnect_url,
const std::string& account_hint,
const std::string& client_id,
DisconnectCallback callback) {
auto resource_request = CreateCredentialedResourceRequest(
disconnect_url, CredentialedResourceRequestType::kOriginWithCORS);
std::string url_encoded_post_data =
"client_id=" + client_id + "&account_hint=" + account_hint;
DownloadJsonAndParse(
std::move(resource_request), url_encoded_post_data,
base::BindOnce(&OnDisconnectResponseParsed, std::move(callback)),
maxResponseSizeInKiB * 1024);
}
bool IdpNetworkRequestManager::IsCrossSiteIframe() const {
return webid::IsIframeOriginEnabled() && !rp_embedding_origin_.opaque() &&
!net::SchemefulSite::IsSameSite(relying_party_origin_,
rp_embedding_origin_);
}
void IdpNetworkRequestManager::DownloadAndDecodeImage(const GURL& url,
ImageCallback callback) {
std::unique_ptr<network::ResourceRequest> resource_request =
CreateUncredentialedResourceRequest(url, /*send_origin=*/false);
DownloadUrl(
std::move(resource_request),
/*url_encoded_post_data=*/std::nullopt,
base::BindOnce(&IdpNetworkRequestManager::OnDownloadedImage,
weak_ptr_factory_.GetWeakPtr(), std::move(callback)),
maxResponseSizeInKiB * 1024);
}
void IdpNetworkRequestManager::FetchAccountPicturesAndBrandIcons(
const std::vector<IdentityRequestAccountPtr>& accounts,
std::unique_ptr<IdentityProviderInfo> idp_info,
const GURL& rp_brand_icon_url,
FetchAccountPicturesAndBrandIconsCallback callback) {
GURL idp_brand_icon_url = idp_info->metadata.brand_icon_url;
GURL config_url = idp_info->metadata.config_url;
auto barrier_callback = base::BarrierClosure(
// Wait for all accounts plus the brand icon URLs.
accounts.size() + 2,
base::BindOnce(&IdpNetworkRequestManager::
OnAllAccountPicturesAndBrandIconUrlReceived,
weak_ptr_factory_.GetWeakPtr(), std::move(callback),
std::move(idp_info), accounts, rp_brand_icon_url));
for (const auto& account : accounts) {
if (webid::IsLightweightModeEnabled() && account->from_accounts_push) {
FetchCachedAccountImage(url::Origin::Create(config_url), account->picture,
barrier_callback);
} else {
FetchImage(account->picture, barrier_callback);
}
}
FetchImage(idp_brand_icon_url, barrier_callback);
FetchImage(rp_brand_icon_url, barrier_callback);
}
void IdpNetworkRequestManager::FetchIdpBrandIcon(
std::unique_ptr<IdentityProviderInfo> idp_info,
FetchIdpBrandIconCallback callback) {
GURL idp_brand_icon_url = idp_info->metadata.brand_icon_url;
FetchImage(idp_brand_icon_url,
base::BindOnce(&IdpNetworkRequestManager::OnIdpBrandIconReceived,
weak_ptr_factory_.GetWeakPtr(), std::move(idp_info),
std::move(callback)));
}
void IdpNetworkRequestManager::FetchImage(const GURL& url,
base::OnceClosure callback) {
if (url.is_valid()) {
DownloadAndDecodeImage(
url, base::BindOnce(&IdpNetworkRequestManager::OnImageReceived,
weak_ptr_factory_.GetWeakPtr(), std::move(callback),
url));
} else {
// We have to still call the callback to make sure the barrier
// callback gets the right number of calls.
std::move(callback).Run();
}
}
void IdpNetworkRequestManager::FetchCachedAccountImage(
const url::Origin& idp_origin,
const GURL& url,
base::OnceClosure callback) {
if (url.is_valid()) {
DownloadAndDecodeCachedImage(
idp_origin, url,
base::BindOnce(&IdpNetworkRequestManager::OnImageReceived,
weak_ptr_factory_.GetWeakPtr(), std::move(callback),
url));
} else {
std::move(callback).Run();
}
}
void IdpNetworkRequestManager::OnImageReceived(base::OnceClosure callback,
GURL url,
const gfx::Image& image) {
downloaded_images_[url] = image;
std::move(callback).Run();
}
void IdpNetworkRequestManager::OnAllAccountPicturesAndBrandIconUrlReceived(
FetchAccountPicturesAndBrandIconsCallback callback,
std::unique_ptr<IdentityProviderInfo> idp_info,
std::vector<IdentityRequestAccountPtr>&& accounts,
const GURL& rp_brand_icon_url) {
for (auto& account : accounts) {
auto it = downloaded_images_.find(account->picture);
if (it != downloaded_images_.end()) {
// We do not use std::move here in case multiple accounts use the same
// picture URL, and the underlying gfx::Image data is refcounted anyway.
account->decoded_picture = it->second;
}
}
gfx::Image rp_brand_icon;
auto it = downloaded_images_.find(rp_brand_icon_url);
if (it != downloaded_images_.end()) {
rp_brand_icon = it->second;
}
it = downloaded_images_.find(idp_info->metadata.brand_icon_url);
if (it != downloaded_images_.end()) {
idp_info->metadata.brand_decoded_icon = it->second;
}
std::move(callback).Run(std::move(accounts), std::move(idp_info),
rp_brand_icon);
}
void IdpNetworkRequestManager::OnIdpBrandIconReceived(
std::unique_ptr<IdentityProviderInfo> idp_info,
FetchIdpBrandIconCallback callback) {
auto it = downloaded_images_.find(idp_info->metadata.brand_icon_url);
if (it != downloaded_images_.end()) {
idp_info->metadata.brand_decoded_icon = it->second;
}
std::move(callback).Run(std::move(idp_info));
}
void IdpNetworkRequestManager::DownloadAndDecodeCachedImage(
const url::Origin& idp_origin,
const GURL& url,
ImageCallback callback) {
std::unique_ptr<network::ResourceRequest> resource_request =
CreateCachedAccountPictureRequest(idp_origin, url, /*cache_only=*/true);
DownloadUrl(
std::move(resource_request),
/*url_encoded_post_data=*/std::nullopt,
base::BindOnce(&IdpNetworkRequestManager::OnDownloadedImage,
weak_ptr_factory_.GetWeakPtr(), std::move(callback)),
maxResponseSizeInKiB * 1024);
}
void IdpNetworkRequestManager::DownloadJsonAndParse(
std::unique_ptr<network::ResourceRequest> resource_request,
std::optional<std::string> url_encoded_post_data,
ParseJsonCallback parse_json_callback,
size_t max_download_size,
bool allow_http_error_results) {
DownloadUrl(std::move(resource_request), std::move(url_encoded_post_data),
base::BindOnce(&OnDownloadedJson, std::move(parse_json_callback)),
max_download_size, allow_http_error_results);
}
void IdpNetworkRequestManager::DownloadUrl(
std::unique_ptr<network::ResourceRequest> resource_request,
std::optional<std::string> url_encoded_post_data,
DownloadCallback callback,
size_t max_download_size,
bool allow_http_error_results) {
if (url_encoded_post_data) {
resource_request->method = net::HttpRequestHeaders::kPostMethod;
resource_request->headers.SetHeader(net::HttpRequestHeaders::kContentType,
kUrlEncodedContentType);
}
network::ResourceRequest* resource_request_ptr = resource_request.get();
std::unique_ptr<network::SimpleURLLoader> url_loader =
network::SimpleURLLoader::Create(std::move(resource_request),
CreateTrafficAnnotation());
network::SimpleURLLoader* url_loader_ptr = url_loader.get();
// Notify DevTools about the request
auto request_id = base::UnguessableToken::Create();
devtools_instrumentation::MaybeAssignResourceRequestId(
frame_tree_node_id_, request_id.ToString(), *resource_request_ptr);
if (resource_request_ptr->devtools_request_id.has_value()) {
urlloader_devtools_request_id_map_[url_loader_ptr] = request_id;
devtools_instrumentation::WillSendFedCmNetworkRequest(
frame_tree_node_id_, *resource_request_ptr);
}
if (url_encoded_post_data) {
url_loader->AttachStringForUpload(*url_encoded_post_data,
kUrlEncodedContentType);
if (allow_http_error_results) {
url_loader->SetAllowHttpErrorResults(true);
}
}
// Callback is a member of IdpNetworkRequestManager in order to cancel
// callback if IdpNetworkRequestManager object is destroyed prior to callback
// being run.
url_loader_ptr->DownloadToString(
loader_factory_.get(),
base::BindOnce(&IdpNetworkRequestManager::OnDownloadedUrl,
weak_ptr_factory_.GetWeakPtr(), std::move(url_loader),
std::move(callback)),
max_download_size);
}
void IdpNetworkRequestManager::OnDownloadedUrl(
std::unique_ptr<network::SimpleURLLoader> url_loader,
IdpNetworkRequestManager::DownloadCallback callback,
std::unique_ptr<std::string> response_body) {
auto* response_info = url_loader->ResponseInfo();
// Use the HTTP response code, if available. If it is not available, use the
// NetError(). Note that it is acceptable to put these in the same int because
// NetErrors are not positive, so they do not conflict with HTTP error codes.
int response_code = response_info && response_info->headers
? response_info->headers->response_code()
: url_loader->NetError();
std::optional<network::URLLoaderCompletionStatus> status =
url_loader->CompletionStatus();
// Notify DevTools about the response
auto it = urlloader_devtools_request_id_map_.find(url_loader.get());
if (it != urlloader_devtools_request_id_map_.end()) {
auto request_id = it->second;
const std::string& response_body_str =
response_body ? *response_body : std::string();
auto completion_status = status.value_or(
network::URLLoaderCompletionStatus(url_loader->NetError()));
devtools_instrumentation::DidReceiveFedCmNetworkResponse(
frame_tree_node_id_, request_id.ToString(), url_loader->GetFinalURL(),
response_info, response_body_str, completion_status);
// Remove the entry from the map
urlloader_devtools_request_id_map_.erase(it);
}
if (!callback) {
// For the metrics endpoint, we do not care about the result.
return;
}
std::string mime_type;
if (response_info && response_info->headers) {
response_info->headers->GetMimeType(&mime_type);
}
// Check for CORS error
bool cors_error = false;
if (status && status.value().cors_error_status.has_value()) {
cors_error = true;
}
std::move(callback).Run(std::move(response_body), response_code,
std::move(mime_type), cors_error);
}
void IdpNetworkRequestManager::FetchClientMetadata(
const GURL& endpoint,
const std::string& client_id,
int rp_brand_icon_ideal_size,
int rp_brand_icon_minimum_size,
FetchClientMetadataCallback callback) {
std::string parameters =
"?client_id=" + base::EscapeQueryParamValue(client_id, true);
if (IsCrossSiteIframe()) {
parameters += "&top_frame_origin=" + rp_embedding_origin_.Serialize();
}
GURL target_url = endpoint.Resolve(parameters);
std::unique_ptr<network::ResourceRequest> resource_request =
CreateUncredentialedResourceRequest(target_url,
/* send_origin= */ true);
DownloadJsonAndParse(
std::move(resource_request),
/*url_encoded_post_data=*/std::nullopt,
base::BindOnce(&OnClientMetadataParsed, IsCrossSiteIframe(),
rp_brand_icon_ideal_size, rp_brand_icon_minimum_size,
std::move(callback)),
maxResponseSizeInKiB * 1024);
}
void IdpNetworkRequestManager::OnDownloadedImage(
ImageCallback callback,
std::unique_ptr<std::string> response_body,
int response_code,
const std::string& mime_type,
bool cors_error) {
if (!response_body || response_code != net::HTTP_OK) {
std::move(callback).Run(gfx::Image());
return;
}
data_decoder::DecodeImageIsolated(
base::as_byte_span(*response_body),
data_decoder::mojom::ImageCodec::kDefault, /*shrink_to_fit=*/false,
data_decoder::kDefaultMaxSizeInBytes, gfx::Size(),
base::BindOnce(&IdpNetworkRequestManager::OnDecodedImage,
weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}
void IdpNetworkRequestManager::OnDecodedImage(ImageCallback callback,
const SkBitmap& decoded_bitmap) {
std::move(callback).Run(gfx::Image::CreateFrom1xBitmap(decoded_bitmap));
}
std::unique_ptr<network::ResourceRequest>
IdpNetworkRequestManager::CreateUncredentialedResourceRequest(
const GURL& target_url,
bool send_origin,
bool follow_redirects) const {
auto resource_request = std::make_unique<network::ResourceRequest>();
resource_request->url = target_url;
resource_request->credentials_mode = network::mojom::CredentialsMode::kOmit;
resource_request->headers.SetHeader(net::HttpRequestHeaders::kAccept,
kApplicationJson);
resource_request->destination =
network::mojom::RequestDestination::kWebIdentity;
// See https://github.com/fedidcg/FedCM/issues/379 for why the Origin header
// is sent instead of the Referrer header.
if (send_origin) {
resource_request->headers.SetHeader(net::HttpRequestHeaders::kOrigin,
relying_party_origin_.Serialize());
DCHECK(!follow_redirects);
}
if (follow_redirects) {
resource_request->redirect_mode = network::mojom::RedirectMode::kFollow;
} else {
resource_request->redirect_mode = network::mojom::RedirectMode::kError;
}
resource_request->request_initiator = url::Origin();
resource_request->trusted_params = network::ResourceRequest::TrustedParams();
resource_request->trusted_params->isolation_info = net::IsolationInfo::Create(
net::IsolationInfo::RequestType::kOther,
/*top_frame_origin=*/relying_party_origin_,
/*frame_origin=*/url::Origin::Create(target_url), net::SiteForCookies(),
/*nonce=*/std::nullopt,
net::NetworkIsolationPartition::kFedCmUncredentialedRequests);
DCHECK(client_security_state_);
resource_request->trusted_params->client_security_state =
client_security_state_.Clone();
return resource_request;
}
std::unique_ptr<network::ResourceRequest>
IdpNetworkRequestManager::CreateCredentialedResourceRequest(
const GURL& target_url,
CredentialedResourceRequestType type) const {
auto resource_request = std::make_unique<network::ResourceRequest>();
auto target_origin = url::Origin::Create(target_url);
auto site_for_cookies = net::SiteForCookies::FromOrigin(target_origin);
// Setting the initiator to relying_party_origin_ ensures that we don't send
// SameSite=Strict cookies.
resource_request->request_initiator = relying_party_origin_;
resource_request->destination =
network::mojom::RequestDestination::kWebIdentity;
resource_request->url = target_url;
resource_request->site_for_cookies = site_for_cookies;
// TODO(crbug.com/40284123): Figure out why when using CORS we still need to
// explicitly pass the Origin header.
if (type != CredentialedResourceRequestType::kNoOrigin) {
resource_request->headers.SetHeader(net::HttpRequestHeaders::kOrigin,
relying_party_origin_.Serialize());
}
if (type == CredentialedResourceRequestType::kOriginWithCORS) {
resource_request->mode = network::mojom::RequestMode::kCors;
resource_request->request_initiator = relying_party_origin_;
}
resource_request->redirect_mode = network::mojom::RedirectMode::kError;
resource_request->headers.SetHeader(net::HttpRequestHeaders::kAccept,
kApplicationJson);
resource_request->credentials_mode =
network::mojom::CredentialsMode::kInclude;
resource_request->trusted_params = network::ResourceRequest::TrustedParams();
net::IsolationInfo::RequestType request_type =
net::IsolationInfo::RequestType::kOther;
if (webid::IsSameSiteLaxEnabled()) {
// We use kMainFrame so that we can send SameSite=Lax cookies.
request_type = net::IsolationInfo::RequestType::kMainFrame;
}
resource_request->trusted_params->isolation_info = net::IsolationInfo::Create(
request_type, /*top_frame_origin=*/target_origin,
/*frame_origin=*/target_origin, site_for_cookies);
DCHECK(client_security_state_);
resource_request->trusted_params->client_security_state =
client_security_state_.Clone();
return resource_request;
}
std::unique_ptr<network::ResourceRequest>
IdpNetworkRequestManager::CreateCachedAccountPictureRequest(
const url::Origin& idp_origin,
const GURL& target_url,
bool cache_only) const {
auto resource_request = std::make_unique<network::ResourceRequest>();
resource_request->url = target_url;
resource_request->credentials_mode = network::mojom::CredentialsMode::kOmit;
resource_request->destination =
network::mojom::RequestDestination::kWebIdentity;
resource_request->redirect_mode = network::mojom::RedirectMode::kError;
resource_request->request_initiator = url::Origin();
resource_request->trusted_params = network::ResourceRequest::TrustedParams();
resource_request->trusted_params->isolation_info = net::IsolationInfo::Create(
net::IsolationInfo::RequestType::kOther,
/*top_frame_origin=*/idp_origin,
/*frame_origin=*/url::Origin::Create(target_url), net::SiteForCookies(),
/*nonce=*/std::nullopt,
net::NetworkIsolationPartition::kFedCmUncredentialedRequests);
DCHECK(client_security_state_);
resource_request->trusted_params->client_security_state =
client_security_state_.Clone();
if (cache_only) {
resource_request->load_flags |= net::LOAD_ONLY_FROM_CACHE;
}
return resource_request;
}
void IdpNetworkRequestManager::CacheAccountPictures(
const url::Origin& idp_origin,
const std::vector<GURL>& picture_urls) {
for (const GURL& url : picture_urls) {
if (url.is_valid()) {
DownloadUrl(CreateCachedAccountPictureRequest(idp_origin, url,
/*cache_only=*/false),
/*url_encoded_post_data=*/std::nullopt, DownloadCallback(),
maxResponseSizeInKiB * 1024);
}
}
}
} // namespace content