#include "google_apis/gaia/fake_gaia.h"
#include <algorithm>
#include <string_view>
#include <utility>
#include <vector>
#include "base/base64.h"
#include "base/base_paths.h"
#include "base/containers/contains.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/json/json_writer.h"
#include "base/logging.h"
#include "base/path_service.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/test/values_test_util.h"
#include "base/values.h"
#include "google_apis/gaia/gaia_auth_consumer.h"
#include "google_apis/gaia/gaia_auth_test_util.h"
#include "google_apis/gaia/gaia_constants.h"
#include "google_apis/gaia/gaia_features.h"
#include "google_apis/gaia/gaia_id.h"
#include "google_apis/gaia/gaia_urls.h"
#include "net/base/url_util.h"
#include "net/cookies/parsed_cookie.h"
#include "net/http/http_status_code.h"
#include "net/test/embedded_test_server/http_request.h"
#include "net/test/embedded_test_server/http_response.h"
#include "url/third_party/mozilla/url_parse.h"
#define REGISTER_RESPONSE_HANDLER(url, method) \
request_handlers_.insert(std::make_pair( \
url.GetPath(), \
base::BindRepeating(&FakeGaia::method, base::Unretained(this))))
#define REGISTER_PATH_RESPONSE_HANDLER(path, method) \
request_handlers_.insert(std::make_pair( \
path, base::BindRepeating(&FakeGaia::method, base::Unretained(this))))
namespace {
using ::net::test_server::BasicHttpResponse;
using ::net::test_server::HttpRequest;
using MultiloginAction = ::FakeGaia::MultiloginCall::Action;
const char kTestAuthCode[] = "fake-auth-code";
const char kTestAuthLoginAccessToken[] = "fake-access-token";
const char kTestRefreshToken[] = "fake-refresh-token";
const char kTestSessionSIDCookie[] = "fake-session-SID-cookie";
const char kTestSessionLSIDCookie[] = "fake-session-LSID-cookie";
const char kTestSession1PSIDTSCookie[] = "fake-session-1p-SIDTS-cookie";
const char kTestSession3PSIDTSCookie[] = "fake-session-3p-SIDTS-cookie";
const char kTestReauthProofToken[] = "fake-reauth-proof-token";
const char kTestCookieAttributes[] =
"; Path=/; HttpOnly; SameSite=None; Secure";
const char kDefaultEmail[] = "email12345@foo.com";
const base::FilePath::CharType kEmbeddedSetupChromeos[] =
FILE_PATH_LITERAL("google_apis/test/embedded_setup_chromeos.html");
const char kAuthHeaderBearer[] = "Bearer ";
const char kAuthHeaderBoundOAuth[] = "BoundOAuth ";
const char kAuthHeaderMultiOAuth[] = "MultiOAuth ";
const char kAuthHeaderOAuth[] = "OAuth ";
const char kFakeRemoveLocalAccountPath[] = "FakeRemoveLocalAccount";
const char kFakeSAMLContinuePath[] = "FakeSAMLContinue";
const char kFakeTokenBindingAssertionChallenge[] =
"fake-token-binding-assertion-challenge";
const char kXSSIPrefix[] = ")]}'\n";
typedef std::map<std::string, std::string> CookieMap;
bool GetAccessToken(const HttpRequest& request,
const char* auth_token_prefix,
std::string* access_token) {
auto auth_header_entry = request.headers.find("Authorization");
if (auth_header_entry != request.headers.end()) {
if (base::StartsWith(auth_header_entry->second, auth_token_prefix,
base::CompareCase::SENSITIVE)) {
*access_token =
auth_header_entry->second.substr(strlen(auth_token_prefix));
return true;
}
}
return false;
}
void SetCookies(BasicHttpResponse* http_response,
const std::string& sid_cookie,
const std::string& lsid_cookie) {
http_response->AddCustomHeader(
"Set-Cookie", base::StringPrintf("SID=%s%s", sid_cookie.c_str(),
kTestCookieAttributes));
http_response->AddCustomHeader(
"Set-Cookie", base::StringPrintf("LSID=%s%s", lsid_cookie.c_str(),
kTestCookieAttributes));
}
base::Value::Dict GetCookieForMultilogin(const std::string& name,
const std::string& value) {
return base::Value::Dict()
.Set("name", name)
.Set("value", value)
.Set("domain", ".google.fr")
.Set("path", "/")
.Set("isSecure", true)
.Set("isHttpOnly", false)
.Set("priority", "HIGH")
.Set("maxAge", 63070000);
}
base::Value::List GetCookiesForMultilogin(
const FakeGaia::Configuration& configuration) {
CHECK(!configuration.session_sid_cookie.empty());
CHECK(!configuration.session_lsid_cookie.empty());
base::Value::List cookies;
cookies.Append(
GetCookieForMultilogin("SID", configuration.session_sid_cookie));
cookies.Append(
GetCookieForMultilogin("LSID", configuration.session_lsid_cookie));
if (!configuration.session_1p_sidts_cookie.empty()) {
cookies.Append(GetCookieForMultilogin(
"__Secure-1PSIDTS", configuration.session_1p_sidts_cookie));
}
if (!configuration.session_3p_sidts_cookie.empty()) {
cookies.Append(GetCookieForMultilogin(
"__Secure-3PSIDTS", configuration.session_3p_sidts_cookie));
}
return cookies;
}
base::Value::Dict GetFailedAccountForMultilogin(const std::string& gaia_id,
const std::string& status,
const std::string& challenge) {
return base::Value::Dict()
.Set("obfuscated_id", gaia_id)
.Set("status", status)
.Set("token_binding_retry_response",
base::Value::Dict().Set("challenge", challenge));
}
base::Value::List GetFailedAccountsForMultilogin(
const gaia::MultiOAuthHeader& multi_oauth_header) {
CHECK_GT(multi_oauth_header.account_requests().size(), 0);
base::Value::List failed_accounts;
failed_accounts.reserve(multi_oauth_header.account_requests().size());
for (const gaia::MultiOAuthHeader_AccountRequest& account_request :
multi_oauth_header.account_requests()) {
failed_accounts.Append(GetFailedAccountForMultilogin(
account_request.gaia_id(), "RECOVERABLE",
kFakeTokenBindingAssertionChallenge));
}
return failed_accounts;
}
base::Value::List GetDeviceBoundSessionInfoForMultilogin(
const FakeGaia::Configuration& configuration) {
auto device_bound_session_info = base::Value::Dict()
.Set("domain", "GOOGLE_COM")
.Set("is_device_bound", true);
if (!configuration.reuse_bound_session) {
base::Value::List credentials;
if (!configuration.session_1p_sidts_cookie.empty()) {
credentials.Append(base::Value::Dict()
.Set("type", "cookie")
.Set("name", "__Secure-1PSIDTS")
.Set("scope", base::Value::Dict()
.Set("domain", ".google.com")
.Set("path", "/")));
}
if (!configuration.session_3p_sidts_cookie.empty()) {
credentials.Append(base::Value::Dict()
.Set("type", "cookie")
.Set("name", "__Secure-3PSIDTS")
.Set("scope", base::Value::Dict()
.Set("domain", ".google.com")
.Set("path", "/")));
}
auto register_session_payload =
base::Value::Dict()
.Set("session_identifier", "sidts_session")
.Set("refresh_url", "/RotateBoundCookies");
if (configuration.spec_compliant_device_bound_session) {
register_session_payload.Set("scope",
base::Value::Dict()
.Set("origin", "https://google.com")
.Set("include_site", true));
register_session_payload.Set("allowed_refresh_initiators",
base::Value::List().Append("*"));
for (auto& credential : credentials) {
credential.GetDict().Set("attributes",
"Secure; HttpOnly; Domain=.google.com; "
"Path=/; SameSite=None");
}
}
register_session_payload.Set("credentials", std::move(credentials));
device_bound_session_info.Set("register_session_payload",
std::move(register_session_payload));
}
return base::Value::List().Append(std::move(device_bound_session_info));
}
MultiloginAction GetMultiloginAction(
const std::optional<gaia::MultiOAuthHeader>& header) {
if (!header.has_value()) {
return MultiloginAction::kReturnUnboundCookies;
}
CHECK_GT(header->account_requests().size(), 0);
const gaia::MultiOAuthHeader::AccountRequest& account_request =
header->account_requests(0);
if (account_request.token_binding_assertion().empty()) {
return MultiloginAction::kReturnUnboundCookies;
}
if (account_request.token_binding_assertion() ==
GaiaConstants::kTokenBindingAssertionSentinel) {
return MultiloginAction::kReturnBindingChallenge;
}
return MultiloginAction::kReturnBoundCookies;
}
std::string FormatSyncTrustedRecoveryMethods(
const std::vector<std::vector<uint8_t>>& public_keys) {
std::string result;
for (const std::vector<uint8_t>& public_key : public_keys) {
if (!result.empty()) {
base::StrAppend(&result, {","});
}
base::StrAppend(&result,
{"{\"publicKey\":\"", base::Base64Encode(public_key),
"\",\"type\":3}"});
}
return result;
}
std::string FormatSyncTrustedVaultKeysHeader(
const GaiaId& gaia_id,
const FakeGaia::SyncTrustedVaultKeys& sync_trusted_vault_keys) {
const char format[] =
"{"
"\"obfuscatedGaiaId\":\"%s\","
"\"fakeEncryptionKeyMaterial\":\"%s\","
"\"fakeEncryptionKeyVersion\":%d,"
"\"fakeTrustedRecoveryMethods\":[%s]"
"}";
return base::StringPrintf(
format, gaia_id.ToString().c_str(),
base::Base64Encode(sync_trusted_vault_keys.encryption_key).c_str(),
sync_trusted_vault_keys.encryption_key_version,
FormatSyncTrustedRecoveryMethods(
sync_trusted_vault_keys.trusted_public_keys)
.c_str());
}
bool GetBoundAccessToken(const HttpRequest& request,
std::string* access_token) {
std::string encoded_token;
if (!GetAccessToken(request, kAuthHeaderBoundOAuth, &encoded_token)) {
return false;
}
std::string decoded_token;
if (!base::Base64UrlDecode(encoded_token,
base::Base64UrlDecodePolicy::DISALLOW_PADDING,
&decoded_token)) {
return false;
}
gaia::BoundOAuthToken bound_oauth_token;
if (!bound_oauth_token.ParseFromString(decoded_token) ||
!bound_oauth_token.has_token()) {
return false;
}
*access_token = bound_oauth_token.token();
return true;
}
std::optional<gaia::MultiOAuthHeader> GetMultiOAuthHeader(
const HttpRequest& request) {
auto it = request.headers.find("Authorization");
if (it == request.headers.end()) {
return std::nullopt;
}
std::optional<std::string_view> encoded_header =
base::RemovePrefix(it->second, kAuthHeaderMultiOAuth);
if (!encoded_header.has_value()) {
return std::nullopt;
}
std::string decoded_header;
if (!base::Base64UrlDecode(*encoded_header,
base::Base64UrlDecodePolicy::DISALLOW_PADDING,
&decoded_header)) {
return std::nullopt;
}
gaia::MultiOAuthHeader multi_oauth_header;
if (!multi_oauth_header.ParseFromString(decoded_header)) {
return std::nullopt;
}
return multi_oauth_header;
}
void FormatJSONResponse(const base::ValueView& value,
net::HttpStatusCode status,
BasicHttpResponse* http_response,
const std::string& prefix = "") {
std::string response_json = base::WriteJson(value).value_or("");
http_response->set_content(base::StrCat({prefix, response_json}));
http_response->set_code(status);
}
void FormatOkJSONResponse(const base::ValueView& value,
BasicHttpResponse* http_response,
const std::string& prefix = "") {
FormatJSONResponse(value, net::HTTP_OK, http_response, prefix);
}
}
FakeGaia::AccessTokenInfo::AccessTokenInfo() = default;
FakeGaia::AccessTokenInfo::AccessTokenInfo(const AccessTokenInfo& other) =
default;
FakeGaia::AccessTokenInfo::~AccessTokenInfo() = default;
FakeGaia::Configuration::Configuration() = default;
FakeGaia::Configuration::~Configuration() = default;
FakeGaia::MultiloginCall::MultiloginCall() = default;
FakeGaia::MultiloginCall::~MultiloginCall() = default;
FakeGaia::MultiloginCall::MultiloginCall(const MultiloginCall& other) = default;
void FakeGaia::Configuration::Update(const Configuration& update) {
auto maybe_update_field = [this,
&update](std::string Configuration::* field_ptr) {
if (!(update.*field_ptr).empty()) {
this->*field_ptr = update.*field_ptr;
}
};
maybe_update_field(&Configuration::auth_sid_cookie);
maybe_update_field(&Configuration::auth_lsid_cookie);
maybe_update_field(&Configuration::auth_code);
maybe_update_field(&Configuration::refresh_token);
maybe_update_field(&Configuration::access_token);
maybe_update_field(&Configuration::id_token);
maybe_update_field(&Configuration::session_sid_cookie);
maybe_update_field(&Configuration::session_lsid_cookie);
if (!update.emails.empty()) {
emails = update.emails;
}
if (!update.signed_out_gaia_ids.empty()) {
signed_out_gaia_ids = update.signed_out_gaia_ids;
}
}
FakeGaia::SyncTrustedVaultKeys::SyncTrustedVaultKeys() = default;
FakeGaia::SyncTrustedVaultKeys::~SyncTrustedVaultKeys() = default;
FakeGaia::FakeGaia() : issue_oauth_code_cookie_(false) {
base::FilePath source_root_dir;
base::PathService::Get(base::DIR_SRC_TEST_DATA_ROOT, &source_root_dir);
CHECK(base::ReadFileToString(
source_root_dir.Append(base::FilePath(kEmbeddedSetupChromeos)),
&embedded_setup_chromeos_response_));
}
FakeGaia::~FakeGaia() = default;
void FakeGaia::SetConfigurationHelper(const std::string& email,
const std::string& auth_sid_cookie,
const std::string& auth_lsid_cookie) {
FakeGaia::Configuration params;
params.auth_sid_cookie = auth_sid_cookie;
params.auth_lsid_cookie = auth_lsid_cookie;
params.auth_code = kTestAuthCode;
params.refresh_token = kTestRefreshToken;
params.access_token = kTestAuthLoginAccessToken;
params.session_sid_cookie = kTestSessionSIDCookie;
params.session_lsid_cookie = kTestSessionLSIDCookie;
params.session_1p_sidts_cookie = kTestSession1PSIDTSCookie;
params.session_3p_sidts_cookie = kTestSession3PSIDTSCookie;
params.emails = {email};
SetConfiguration(params);
}
void FakeGaia::SetConfiguration(const Configuration& params) {
configuration_ = params;
}
void FakeGaia::UpdateConfiguration(const Configuration& params) {
configuration_.Update(params);
}
void FakeGaia::MapEmailToGaiaId(const std::string& email,
const GaiaId& gaia_id) {
DCHECK(!email.empty());
DCHECK(!gaia_id.empty());
email_to_gaia_id_map_[email] = gaia_id;
}
void FakeGaia::SetSyncTrustedVaultKeys(
const std::string& email,
const SyncTrustedVaultKeys& sync_trusted_vault_keys) {
DCHECK(!email.empty());
email_to_sync_trusted_vault_keys_map_[email] = sync_trusted_vault_keys;
}
GaiaId FakeGaia::GetGaiaIdOfEmail(const std::string& email) const {
const auto it = email_to_gaia_id_map_.find(email);
return it == email_to_gaia_id_map_.end() ? GetDefaultGaiaId() : it->second;
}
std::string FakeGaia::GetEmailOfGaiaId(const GaiaId& gaia_id) const {
for (const auto& email_and_gaia_id : email_to_gaia_id_map_) {
if (email_and_gaia_id.second == gaia_id)
return email_and_gaia_id.first;
}
return kDefaultEmail;
}
void FakeGaia::AddGoogleAccountsSigninHeader(
BasicHttpResponse* http_response,
const std::vector<std::string>& emails) const {
DCHECK(http_response);
std::vector<std::string> accounts;
for (size_t i = 0; i < emails.size(); ++i) {
accounts.push_back(base::StringPrintf(
"email=\"%s\", obfuscatedid=\"%s\", sessionindex=%d", emails[i],
GetGaiaIdOfEmail(emails[i]).ToString().c_str(), i));
}
http_response->AddCustomHeader("google-accounts-signin",
base::JoinString(accounts, ", "));
}
void FakeGaia::SetOAuthCodeCookie(BasicHttpResponse* http_response) const {
DCHECK(http_response);
http_response->AddCustomHeader(
"Set-Cookie",
base::StringPrintf("oauth_code=%s%s", configuration_.auth_code.c_str(),
kTestCookieAttributes));
}
void FakeGaia::AddSyncTrustedKeysHeader(BasicHttpResponse* http_response,
const std::string& email) const {
DCHECK(http_response);
DCHECK(base::Contains(email_to_sync_trusted_vault_keys_map_, email));
http_response->AddCustomHeader(
"fake-sync-trusted-vault-keys",
FormatSyncTrustedVaultKeysHeader(
GetGaiaIdOfEmail(email),
email_to_sync_trusted_vault_keys_map_.at(email)));
}
void FakeGaia::Initialize() {
GaiaUrls* gaia_urls = GaiaUrls::GetInstance();
REGISTER_RESPONSE_HANDLER(gaia_urls->oauth_multilogin_url(),
HandleMultilogin);
REGISTER_RESPONSE_HANDLER(gaia_urls->embedded_setup_chromeos_url(),
HandleEmbeddedSetupChromeos);
REGISTER_RESPONSE_HANDLER(gaia_urls->embedded_setup_chromeos_kid_signup_url(),
HandleEmbeddedSetupChromeos);
REGISTER_RESPONSE_HANDLER(gaia_urls->embedded_setup_chromeos_kid_signin_url(),
HandleEmbeddedSetupChromeos);
REGISTER_RESPONSE_HANDLER(gaia_urls->embedded_reauth_chromeos_url(),
HandleEmbeddedReauthChromeos);
REGISTER_PATH_RESPONSE_HANDLER("/_/embedded/lookup/accountlookup",
HandleEmbeddedLookupAccountLookup);
REGISTER_PATH_RESPONSE_HANDLER("/_/embedded/signin/challenge",
HandleEmbeddedSigninChallenge);
REGISTER_PATH_RESPONSE_HANDLER("/SSO", HandleSSO);
REGISTER_RESPONSE_HANDLER(gaia_urls->saml_redirect_chromeos_url(),
HandleSAMLRedirect);
REGISTER_RESPONSE_HANDLER(
gaia_urls->gaia_url().Resolve(kFakeSAMLContinuePath),
HandleFakeSAMLContinue);
REGISTER_RESPONSE_HANDLER(gaia_urls->oauth2_token_url(), HandleAuthToken);
REGISTER_RESPONSE_HANDLER(gaia_urls->oauth2_token_info_url(),
HandleTokenInfo);
REGISTER_RESPONSE_HANDLER(gaia_urls->oauth2_issue_token_url(),
HandleIssueToken);
REGISTER_RESPONSE_HANDLER(gaia_urls->ListAccountsURLWithSource(std::string()),
HandleListAccounts);
REGISTER_RESPONSE_HANDLER(gaia_urls->oauth_user_info_url(),
HandleOAuthUserInfo);
REGISTER_RESPONSE_HANDLER(
gaia_urls->GetCheckConnectionInfoURLWithSource(std::string()),
HandleGetCheckConnectionInfo);
REGISTER_RESPONSE_HANDLER(gaia_urls->reauth_api_url(),
HandleGetReAuthProofToken);
REGISTER_RESPONSE_HANDLER(
gaia_urls->gaia_url().Resolve(kFakeRemoveLocalAccountPath),
HandleFakeRemoveLocalAccount);
REGISTER_RESPONSE_HANDLER(gaia_urls->oauth2_revoke_url(),
HandleOAuth2TokenRevoke);
REGISTER_RESPONSE_HANDLER(gaia_urls->rotate_bound_cookies_url(),
HandleRotateBoundCookies);
}
FakeGaia::RequestHandlerMap::iterator FakeGaia::FindHandlerByPathPrefix(
const std::string& request_path) {
return std::ranges::find_if(
request_handlers_,
[request_path](std::pair<std::string, HttpRequestHandlerCallback> entry) {
return base::StartsWith(request_path, entry.first,
base::CompareCase::SENSITIVE);
});
}
std::unique_ptr<net::test_server::HttpResponse> FakeGaia::HandleRequest(
const HttpRequest& request) {
GURL request_url = GURL("http://localhost").Resolve(request.relative_url);
std::string request_path = request_url.GetPath();
auto http_response = std::make_unique<BasicHttpResponse>();
auto fixed_response = fixed_responses_.find(request_path);
if (fixed_response != fixed_responses_.end()) {
const auto& [code, content] = fixed_response->second;
http_response->set_code(code);
http_response->set_content(content);
http_response->set_content_type("text/html");
return std::move(http_response);
}
RequestHandlerMap::iterator iter = request_handlers_.find(request_path);
if (iter == request_handlers_.end()) {
iter = FindHandlerByPathPrefix(request_path);
}
if (iter == request_handlers_.end()) {
LOG(ERROR) << "Unhandled request " << request_path;
return nullptr;
}
LOG(WARNING) << "Serving request " << request_path;
iter->second.Run(request, http_response.get());
return std::move(http_response);
}
void FakeGaia::IssueOAuthToken(const std::string& auth_token,
const AccessTokenInfo& token_info) {
access_token_info_map_.insert(std::make_pair(auth_token, token_info));
}
bool FakeGaia::HasAccessTokenForAuthToken(const std::string& auth_token) const {
return access_token_info_map_.contains(auth_token);
}
void FakeGaia::RegisterSamlUser(const std::string& account_id,
const GURL& saml_idp) {
saml_account_idp_map_[account_id] = saml_idp;
}
void FakeGaia::RemoveSamlIdpForUser(const std::string& account_id) {
saml_account_idp_map_.erase(account_id);
}
void FakeGaia::RegisterSamlDomainRedirectUrl(const std::string& domain,
const GURL& saml_redirect_url) {
saml_domain_url_map_[domain] = saml_redirect_url;
}
void FakeGaia::RegisterSamlSsoProfileRedirectUrl(
const std::string& sso_profile,
const GURL& saml_redirect_url) {
saml_sso_profile_url_map_[sso_profile] = saml_redirect_url;
}
bool FakeGaia::GetQueryParameter(const std::string& query,
const std::string& key,
std::string* value) {
GURL query_url("http://localhost?" + query);
return net::GetValueForKeyInQuery(query_url, key, value);
}
std::string FakeGaia::GetDeviceIdByRefreshToken(
const std::string& refresh_token) const {
auto it = refresh_token_to_device_id_map_.find(refresh_token);
return it != refresh_token_to_device_id_map_.end() ? it->second
: std::string();
}
void FakeGaia::SetFixedResponse(const GURL& gaia_url,
net::HttpStatusCode http_status_code,
const std::string& http_response_body) {
if (http_status_code == net::HTTP_OK && http_response_body.empty()) {
fixed_responses_.erase(gaia_url.GetPath());
} else {
fixed_responses_[gaia_url.GetPath()] =
std::make_pair(http_status_code, http_response_body);
}
}
base::queue<FakeGaia::MultiloginCall> FakeGaia::GetAndResetMultiloginCalls() {
base::queue<MultiloginCall> result;
result.swap(multilogin_calls_);
return result;
}
GURL FakeGaia::GetFakeRemoveLocalAccountURL(const GaiaId& gaia_id) const {
GURL url =
GaiaUrls::GetInstance()->gaia_url().Resolve(kFakeRemoveLocalAccountPath);
return net::AppendQueryParameter(url, "gaia_id", gaia_id.ToString());
}
void FakeGaia::SetRefreshTokenToDeviceIdMap(
const RefreshTokenToDeviceIdMap& refresh_token_to_device_id_map) {
refresh_token_to_device_id_map_ = refresh_token_to_device_id_map;
}
const FakeGaia::AccessTokenInfo* FakeGaia::FindAccessTokenInfo(
const std::string& auth_token,
const std::string& client_id,
const std::string& scope_string) const {
if (auth_token.empty() || client_id.empty())
return nullptr;
std::vector<std::string> scope_list = base::SplitString(
scope_string, " ", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);
ScopeSet scopes(scope_list.begin(), scope_list.end());
for (AccessTokenInfoMap::const_iterator entry(
access_token_info_map_.lower_bound(auth_token));
entry != access_token_info_map_.upper_bound(auth_token); ++entry) {
if (entry->second.audience == client_id &&
(scope_string.empty() || entry->second.any_scope ||
entry->second.scopes == scopes)) {
return &(entry->second);
}
}
return nullptr;
}
const FakeGaia::AccessTokenInfo* FakeGaia::GetAccessTokenInfo(
const std::string& access_token) const {
for (auto& [key, value] : access_token_info_map_) {
if (value.token == access_token)
return &value;
}
return nullptr;
}
void FakeGaia::HandleEmbeddedSetupChromeos(const HttpRequest& request,
BasicHttpResponse* http_response) {
GURL request_url = GURL("http://localhost").Resolve(request.relative_url);
std::string client_id;
if (!GetQueryParameter(request_url.GetQuery(), "client_id", &client_id) ||
GaiaUrls::GetInstance()->oauth2_chrome_client_id() != client_id) {
LOG(ERROR) << "Missing or invalid param 'client_id' in "
"/embedded/setup/chromeos call";
return;
}
GetQueryParameter(request_url.GetQuery(), "Email", &prefilled_email_);
GetQueryParameter(request_url.GetQuery(), "rart", &reauth_request_token_);
GetQueryParameter(request_url.GetQuery(), "pwl",
&passwordless_support_level_);
http_response->set_code(net::HTTP_OK);
http_response->set_content(GetEmbeddedSetupChromeosResponseContent());
http_response->set_content_type("text/html");
}
void FakeGaia::HandleEmbeddedReauthChromeos(const HttpRequest& request,
BasicHttpResponse* http_response) {
GURL request_url = GURL("http://localhost").Resolve(request.relative_url);
std::string client_id;
if (!GetQueryParameter(request_url.GetQuery(), "client_id", &client_id) ||
GaiaUrls::GetInstance()->oauth2_chrome_client_id() != client_id) {
LOG(ERROR) << "Missing or invalid param 'client_id' in "
"/embedded/reauth/chromeos call";
return;
}
GetQueryParameter(request_url.GetQuery(), "is_supervised", &is_supervised_);
GetQueryParameter(request_url.GetQuery(), "is_device_owner",
&is_device_owner_);
GetQueryParameter(request_url.GetQuery(), "Email", &prefilled_email_);
GetQueryParameter(request_url.GetQuery(), "rart", &reauth_request_token_);
GetQueryParameter(request_url.GetQuery(), "pwl",
&passwordless_support_level_);
http_response->set_code(net::HTTP_OK);
http_response->set_content(GetEmbeddedSetupChromeosResponseContent());
http_response->set_content_type("text/html");
}
void FakeGaia::HandleEmbeddedLookupAccountLookup(
const HttpRequest& request,
BasicHttpResponse* http_response) {
std::string email;
const bool is_saml =
GetQueryParameter(request.content, "identifier", &email) &&
base::Contains(saml_account_idp_map_, email);
if (!is_saml)
return;
GURL url(saml_account_idp_map_[email]);
url = net::AppendQueryParameter(url, "SAMLRequest", "fake_request");
url = net::AppendQueryParameter(url, "RelayState",
GaiaUrls::GetInstance()
->gaia_url()
.Resolve(kFakeSAMLContinuePath)
.spec());
std::string redirect_url = url.spec();
http_response->AddCustomHeader("Google-Accounts-SAML", "Start");
http_response->AddCustomHeader("continue", redirect_url);
}
void FakeGaia::HandleEmbeddedSigninChallenge(const HttpRequest& request,
BasicHttpResponse* http_response) {
std::string email;
GetQueryParameter(request.content, "identifier", &email);
std::string reauth_request_token;
if (GetQueryParameter(request.content, "rart", &reauth_request_token)) {
http_response->AddCustomHeader(
"Set-Cookie", base::StringPrintf("RAPT=%s%s", kTestReauthProofToken,
kTestCookieAttributes));
}
if (!configuration_.auth_sid_cookie.empty() &&
!configuration_.auth_lsid_cookie.empty()) {
SetCookies(http_response, configuration_.auth_sid_cookie,
configuration_.auth_lsid_cookie);
}
AddGoogleAccountsSigninHeader(http_response, {email});
if (issue_oauth_code_cookie_)
SetOAuthCodeCookie(http_response);
if (base::Contains(email_to_sync_trusted_vault_keys_map_, email)) {
AddSyncTrustedKeysHeader(http_response, email);
}
}
void FakeGaia::HandleSSO(const HttpRequest& request,
BasicHttpResponse* http_response) {
if (!configuration_.auth_sid_cookie.empty() &&
!configuration_.auth_lsid_cookie.empty()) {
SetCookies(http_response, configuration_.auth_sid_cookie,
configuration_.auth_lsid_cookie);
}
std::string relay_state;
GetQueryParameter(request.content, "RelayState", &relay_state);
std::string redirect_url = relay_state;
http_response->set_code(net::HTTP_TEMPORARY_REDIRECT);
http_response->AddCustomHeader("Location", redirect_url);
http_response->AddCustomHeader("Google-Accounts-SAML", "End");
AddGoogleAccountsSigninHeader(http_response, configuration_.emails);
if (issue_oauth_code_cookie_)
SetOAuthCodeCookie(http_response);
}
void FakeGaia::HandleFakeSAMLContinue(const HttpRequest& request,
BasicHttpResponse* http_response) {
http_response->set_content(fake_saml_continue_response_);
http_response->set_code(net::HTTP_OK);
}
void FakeGaia::HandleAuthToken(const HttpRequest& request,
BasicHttpResponse* http_response) {
std::string grant_type;
if (!GetQueryParameter(request.content, "grant_type", &grant_type)) {
http_response->set_code(net::HTTP_BAD_REQUEST);
LOG(ERROR) << "No 'grant_type' param in /oauth2/v4/token";
return;
}
if (grant_type == "authorization_code") {
std::string auth_code;
if (!GetQueryParameter(request.content, "code", &auth_code) ||
auth_code != configuration_.auth_code) {
http_response->set_code(net::HTTP_BAD_REQUEST);
LOG(ERROR) << "No 'code' param in /oauth2/v4/token";
return;
}
std::string device_id;
if (GetQueryParameter(request.content, "device_id", &device_id)) {
std::string device_type;
if (!GetQueryParameter(request.content, "device_type", &device_type)) {
http_response->set_code(net::HTTP_BAD_REQUEST);
LOG(ERROR) << "'device_type' should be set if 'device_id' is set.";
return;
}
if (device_type != "chrome") {
http_response->set_code(net::HTTP_BAD_REQUEST);
LOG(ERROR) << "'device_type' is not 'chrome'.";
return;
}
}
if (!device_id.empty()) {
refresh_token_to_device_id_map_[configuration_.refresh_token] = device_id;
}
auto response_dict = base::Value::Dict()
.Set("refresh_token", configuration_.refresh_token)
.Set("access_token", configuration_.access_token)
.Set("expires_in", 3600);
if (!configuration_.id_token.empty()) {
response_dict.Set("id_token", configuration_.id_token);
}
FormatOkJSONResponse(response_dict, http_response);
return;
}
std::string scope;
GetQueryParameter(request.content, "scope", &scope);
std::string refresh_token;
std::string client_id;
if (GetQueryParameter(request.content, "refresh_token", &refresh_token) &&
GetQueryParameter(request.content, "client_id", &client_id)) {
const AccessTokenInfo* token_info =
FindAccessTokenInfo(refresh_token, client_id, scope);
if (token_info) {
auto response_dict = base::Value::Dict()
.Set("access_token", token_info->token)
.Set("expires_in", 3600)
.Set("id_token", token_info->id_token);
FormatOkJSONResponse(response_dict, http_response);
return;
}
}
LOG(ERROR) << "Bad request for /oauth2/v4/token - "
<< "refresh_token = " << refresh_token << ", scope = " << scope
<< ", client_id = " << client_id;
http_response->set_code(net::HTTP_BAD_REQUEST);
}
void FakeGaia::HandleTokenInfo(const HttpRequest& request,
BasicHttpResponse* http_response) {
const AccessTokenInfo* token_info = nullptr;
std::string access_token;
if (GetQueryParameter(request.content, "access_token", &access_token))
token_info = GetAccessTokenInfo(access_token);
if (token_info) {
auto response_dict =
base::Value::Dict()
.Set("issued_to", token_info->issued_to)
.Set("audience", token_info->audience)
.Set("user_id", token_info->user_id.ToString())
.Set("scope", base::JoinString(std::vector<std::string_view>(
token_info->scopes.begin(),
token_info->scopes.end()),
" "))
.Set("expires_in", token_info->expires_in)
.Set("email", token_info->email)
.Set("id_token", token_info->id_token);
FormatOkJSONResponse(response_dict, http_response);
} else {
http_response->set_code(net::HTTP_BAD_REQUEST);
}
}
void FakeGaia::HandleIssueToken(const HttpRequest& request,
BasicHttpResponse* http_response) {
std::string access_token;
std::string scope;
std::string client_id;
if ((GetAccessToken(request, kAuthHeaderBearer, &access_token) ||
GetBoundAccessToken(request, &access_token)) &&
GetQueryParameter(request.content, "scope", &scope) &&
GetQueryParameter(request.content, "client_id", &client_id)) {
const AccessTokenInfo* token_info =
FindAccessTokenInfo(access_token, client_id, scope);
if (token_info) {
auto response_dict =
base::Value::Dict()
.Set("issueAdvice", "auto")
.Set("expiresIn", base::NumberToString(token_info->expires_in))
.Set("token", token_info->token)
.Set("grantedScopes", scope)
.Set("id_token", token_info->id_token);
FormatOkJSONResponse(response_dict, http_response);
return;
}
}
http_response->set_code(net::HTTP_BAD_REQUEST);
}
void FakeGaia::HandleListAccounts(const HttpRequest& request,
BasicHttpResponse* http_response) {
std::vector<gaia::CookieParams> params;
for (const std::string& email : configuration_.emails) {
params.push_back({
.email = email,
.gaia_id = GetGaiaIdOfEmail(email),
.valid = true,
.signed_out = false,
.verified = true,
});
}
for (const GaiaId& gaia_id : configuration_.signed_out_gaia_ids) {
DCHECK_NE(GetDefaultGaiaId(), gaia_id);
params.push_back({.email = GetEmailOfGaiaId(gaia_id),
.gaia_id = gaia_id,
.valid = true,
.signed_out = true,
.verified = true});
}
http_response->set_content(
gaia::CreateListAccountsResponseInBinaryFormat(params));
http_response->set_code(net::HTTP_OK);
}
void FakeGaia::HandleOAuthUserInfo(const HttpRequest& request,
BasicHttpResponse* http_response) {
const AccessTokenInfo* token_info = nullptr;
std::string access_token;
if (GetAccessToken(request, kAuthHeaderBearer, &access_token) ||
GetAccessToken(request, kAuthHeaderOAuth, &access_token)) {
token_info = GetAccessTokenInfo(access_token);
}
if (token_info) {
auto response_dict =
base::Value::Dict()
.Set("id", GetGaiaIdOfEmail(token_info->email).ToString())
.Set("email", token_info->email)
.Set("verified_email", token_info->email)
.Set("id_token", token_info->id_token);
FormatOkJSONResponse(response_dict, http_response);
} else {
http_response->set_code(net::HTTP_BAD_REQUEST);
}
}
void FakeGaia::HandleSAMLRedirect(const HttpRequest& request,
BasicHttpResponse* http_response) {
GURL request_url = GURL("http://localhost").Resolve(request.relative_url);
std::optional<GURL> redirect_url = GetSamlRedirectUrl(request_url);
if (!redirect_url) {
http_response->set_code(net::HTTP_BAD_REQUEST);
return;
}
redirect_url =
net::AppendQueryParameter(*redirect_url, "SAMLRequest", "fake_request");
redirect_url = net::AppendQueryParameter(*redirect_url, "RelayState",
GaiaUrls::GetInstance()
->gaia_url()
.Resolve(kFakeSAMLContinuePath)
.spec());
const std::string& final_url = redirect_url->spec();
http_response->set_code(net::HTTP_TEMPORARY_REDIRECT);
http_response->AddCustomHeader("Google-Accounts-SAML", "Start");
http_response->AddCustomHeader("Location", final_url);
}
void FakeGaia::HandleGetCheckConnectionInfo(const HttpRequest& request,
BasicHttpResponse* http_response) {
FormatOkJSONResponse(base::Value::List(), http_response);
}
void FakeGaia::HandleGetReAuthProofToken(const HttpRequest& request,
BasicHttpResponse* http_response) {
base::Value::Dict response_dict;
base::Value::Dict error;
switch (next_reauth_status_) {
case GaiaAuthConsumer::ReAuthProofTokenStatus::kSuccess:
response_dict.Set("encodedRapt", "abc123");
FormatOkJSONResponse(response_dict, http_response);
break;
case GaiaAuthConsumer::ReAuthProofTokenStatus::kInvalidGrant:
error.Set("message", "INVALID_GRANT");
response_dict.Set("error", std::move(error));
FormatJSONResponse(response_dict, net::HTTP_BAD_REQUEST, http_response);
break;
case GaiaAuthConsumer::ReAuthProofTokenStatus::kInvalidRequest:
error.Set("message", "INVALID_REQUEST");
response_dict.Set("error", std::move(error));
FormatJSONResponse(response_dict, net::HTTP_BAD_REQUEST, http_response);
break;
case GaiaAuthConsumer::ReAuthProofTokenStatus::kUnauthorizedClient:
error.Set("message", "UNAUTHORIZED_CLIENT");
response_dict.Set("error", std::move(error));
FormatJSONResponse(response_dict, net::HTTP_FORBIDDEN, http_response);
break;
case GaiaAuthConsumer::ReAuthProofTokenStatus::kInsufficientScope:
error.Set("message", "INSUFFICIENT_SCOPE");
response_dict.Set("error", std::move(error));
FormatJSONResponse(response_dict, net::HTTP_FORBIDDEN, http_response);
break;
case GaiaAuthConsumer::ReAuthProofTokenStatus::kCredentialNotSet:
response_dict.Set("error", std::move(error));
FormatJSONResponse(response_dict, net::HTTP_FORBIDDEN, http_response);
break;
default:
LOG(FATAL) << "Unsupported ReAuthProofTokenStatus: "
<< static_cast<int>(next_reauth_status_);
}
}
void FakeGaia::HandleMultilogin(const HttpRequest& request,
BasicHttpResponse* http_response) {
CHECK(http_response);
if (configuration_.oauth_multilogin_response_status.has_value()) {
switch (*configuration_.oauth_multilogin_response_status) {
case OAuthMultiloginResponseStatus::kInvalidInput:
FormatJSONResponse(base::Value::Dict().Set("status", "INVALID_INPUT"),
net::HTTP_BAD_REQUEST, http_response);
return;
case OAuthMultiloginResponseStatus::kError:
FormatJSONResponse(base::Value::Dict().Set("status", "ERROR"),
net::HTTP_INTERNAL_SERVER_ERROR, http_response);
return;
default:
NOTREACHED() << "Unsupported OAutMultilogin status override: "
<< static_cast<int>(
*configuration_.oauth_multilogin_response_status);
}
}
if (configuration_.session_sid_cookie.empty() ||
configuration_.session_lsid_cookie.empty()) {
http_response->set_code(net::HTTP_BAD_REQUEST);
return;
}
http_response->set_code(net::HTTP_UNAUTHORIZED);
GURL request_url = GURL("http://localhost").Resolve(request.relative_url);
std::string request_query = request_url.GetQuery();
std::string source;
if (!GetQueryParameter(request_query, "source", &source)) {
LOG(ERROR) << "Missing or invalid 'source' param in /Multilogin call";
return;
}
const std::optional<gaia::MultiOAuthHeader> multi_oauth_header =
GetMultiOAuthHeader(request);
const MultiloginAction action = GetMultiloginAction(multi_oauth_header);
switch (action) {
case MultiloginAction::kReturnUnboundCookies: {
const base::Value::Dict response =
base::Value::Dict()
.Set("status", "OK")
.Set("cookies", GetCookiesForMultilogin(configuration_));
FormatOkJSONResponse(response, http_response, kXSSIPrefix);
break;
}
case MultiloginAction::kReturnBindingChallenge: {
CHECK(multi_oauth_header.has_value());
const base::Value::Dict response =
base::Value::Dict()
.Set("status", "RETRY")
.Set("failed_accounts",
GetFailedAccountsForMultilogin(*multi_oauth_header));
FormatJSONResponse(response, net::HTTP_BAD_REQUEST, http_response,
kXSSIPrefix);
break;
}
case MultiloginAction::kReturnBoundCookies: {
const base::Value::Dict response =
base::Value::Dict()
.Set("status", "OK")
.Set("cookies", GetCookiesForMultilogin(configuration_))
.Set("device_bound_session_info",
GetDeviceBoundSessionInfoForMultilogin(configuration_));
FormatOkJSONResponse(response, http_response, kXSSIPrefix);
break;
}
}
MultiloginCall call;
call.header = multi_oauth_header;
call.action = action;
multilogin_calls_.push(std::move(call));
}
void FakeGaia::HandleFakeRemoveLocalAccount(
const net::test_server::HttpRequest& request,
net::test_server::BasicHttpResponse* http_response) {
DCHECK(http_response);
std::string gaia_id_str;
GetQueryParameter(request.GetURL().GetQuery(), "gaia_id", &gaia_id_str);
GaiaId gaia_id(gaia_id_str);
if (!std::erase(configuration_.signed_out_gaia_ids, gaia_id)) {
http_response->set_code(net::HTTP_BAD_REQUEST);
return;
}
http_response->AddCustomHeader(
"Google-Accounts-RemoveLocalAccount",
base::StringPrintf("obfuscatedid=\"%s\"", gaia_id.ToString().c_str()));
http_response->set_content("");
http_response->set_code(net::HTTP_OK);
}
void FakeGaia::HandleOAuth2TokenRevoke(
const net::test_server::HttpRequest& request,
net::test_server::BasicHttpResponse* http_response) {
CHECK(http_response);
static constexpr std::string_view kTokenPrefix = "token=";
if (!request.content.starts_with(kTokenPrefix)) {
http_response->set_code(net::HTTP_BAD_REQUEST);
return;
}
const std::string token = request.content.substr(kTokenPrefix.size());
if (access_token_info_map_.erase(token) == 0) {
FormatJSONResponse(base::Value::Dict().Set("error", "invalid_token"),
net::HTTP_NOT_FOUND, http_response);
return;
}
http_response->set_code(net::HTTP_OK);
}
void FakeGaia::HandleRotateBoundCookies(
const net::test_server::HttpRequest& request,
net::test_server::BasicHttpResponse* http_response) {
CHECK(http_response);
if (configuration_.rotated_cookies.empty()) {
http_response->set_code(net::HTTP_BAD_REQUEST);
return;
}
const GURL url("https://google.com");
for (const std::string& cookie_name : configuration_.rotated_cookies) {
const std::unique_ptr<net::CanonicalCookie> cookie =
net::CanonicalCookie::CreateSanitizedCookie(
url, cookie_name, "dummy_value", url.GetHost(), url.GetPath(),
base::Time::Now(),
base::Time::Now() + base::Hours(2),
base::Time::Now(),
true, true,
net::CookieSameSite::UNSPECIFIED, net::COOKIE_PRIORITY_HIGH,
std::nullopt, nullptr);
http_response->AddCustomHeader(
"Set-Cookie", net::CanonicalCookie::BuildCookieAttributesLine(*cookie));
}
http_response->set_code(net::HTTP_OK);
}
std::string FakeGaia::GetEmbeddedSetupChromeosResponseContent() const {
if (embedded_setup_chromeos_iframe_url_.is_empty())
return embedded_setup_chromeos_response_;
const std::string iframe =
base::StringPrintf("<iframe src=\"%s\" style=\"%s\"></iframe>",
embedded_setup_chromeos_iframe_url_.spec().c_str(),
"width:0; height:0; border:none;");
std::string response_with_iframe = embedded_setup_chromeos_response_;
size_t pos_of_body_closing_tag = response_with_iframe.find("</body>");
CHECK(pos_of_body_closing_tag != std::string::npos);
response_with_iframe.insert(pos_of_body_closing_tag, iframe);
return response_with_iframe;
}
std::optional<GURL> FakeGaia::GetSamlRedirectUrl(
const GURL& request_url) const {
std::string sso_profile;
GetQueryParameter(request_url.GetQuery(), "sso_profile", &sso_profile);
auto itr_sso = saml_sso_profile_url_map_.find(sso_profile);
if (itr_sso != saml_sso_profile_url_map_.end()) {
return itr_sso->second;
}
std::string domain;
GetQueryParameter(request_url.GetQuery(), "domain", &domain);
auto itr_domain = saml_domain_url_map_.find(domain);
if (itr_domain != saml_domain_url_map_.end()) {
return itr_domain->second;
}
return std::nullopt;
}