[go: nahoru, domu]

blob: 2798cc7eec57d2c6b2d843343c1b575d3abbe283 [file] [log] [blame]
// Copyright 2015 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/signin/ios/browser/account_consistency_service.h"
#import <WebKit/WebKit.h>
#import "base/apple/foundation_util.h"
#include "base/command_line.h"
#include "base/functional/bind.h"
#include "base/logging.h"
#include "base/metrics/histogram_functions.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/sys_string_conversions.h"
#include "components/content_settings/core/browser/cookie_settings.h"
#include "components/google/core/common/google_util.h"
#include "components/signin/core/browser/account_reconcilor.h"
#include "components/signin/core/browser/chrome_connected_header_helper.h"
#include "components/signin/core/browser/signin_header_helper.h"
#include "components/signin/ios/browser/features.h"
#include "components/signin/public/identity_manager/accounts_cookie_mutator.h"
#include "components/signin/public/identity_manager/accounts_in_cookie_jar_info.h"
#include "google_apis/gaia/gaia_constants.h"
#include "google_apis/gaia/gaia_urls.h"
#include "ios/web/common/web_view_creation_util.h"
#include "ios/web/public/browser_state.h"
#import "ios/web/public/navigation/web_state_policy_decider.h"
#import "ios/web/public/web_state.h"
#include "ios/web/public/web_state_observer.h"
#include "net/base/mac/url_conversions.h"
#include "net/base/registry_controlled_domains/registry_controlled_domain.h"
#include "net/cookies/canonical_cookie.h"
#include "url/gurl.h"
namespace {
// The validity of the Gaia cookie on the Google domain is one hour to
// ensure that Mirror account consistency is respected in light of the more
// restrictive Intelligent Tracking Prevention (ITP) guidelines in iOS 14
// that may remove or invalidate Gaia cookies on the Google domain.
constexpr base::TimeDelta kDelayThresholdToUpdateGaiaCookie = base::Hours(1);
const char* kGoogleUrl = "https://google.com";
const char* kYoutubeUrl = "https://youtube.com";
const char* kGaiaDomain = "accounts.google.com";
// Returns the registered, organization-identifying host, but no subdomains,
// from the given GURL. Returns an empty string if the GURL is invalid.
static std::string GetDomainFromUrl(const GURL& url) {
if (gaia::HasGaiaSchemeHostPort(url)) {
return kGaiaDomain;
}
return net::registry_controlled_domains::GetDomainAndRegistry(
url, net::registry_controlled_domains::EXCLUDE_PRIVATE_REGISTRIES);
}
// The Gaia cookie state on navigation for a signed-in Chrome user.
// These values are persisted to logs. Entries should not be renumbered and
// numeric values should never be reused.
enum class GaiaCookieStateOnSignedInNavigation {
kGaiaCookiePresentOnNavigation = 0,
kGaiaCookieAbsentOnGoogleAssociatedDomainNavigation = 1,
kGaiaCookieAbsentOnAddSessionNavigation = 2,
kGaiaCookieRestoredOnShowInfobar = 3,
kMaxValue = kGaiaCookieRestoredOnShowInfobar
};
// Records the state of Gaia cookies for a navigation in UMA histogram.
void LogIOSGaiaCookiesState(GaiaCookieStateOnSignedInNavigation state) {
base::UmaHistogramEnumeration("Signin.IOSGaiaCookieStateOnSignedInNavigation",
state);
}
// Allows for manual testing by reducing the polling interval for verifying the
// existence of the GAIA cookie.
base::TimeDelta GetDelayThresholdToUpdateGaiaCookie() {
const base::CommandLine* command_line =
base::CommandLine::ForCurrentProcess();
if (command_line->HasSwitch(
signin::kDelayThresholdMinutesToUpdateGaiaCookie)) {
std::string delayString = command_line->GetSwitchValueASCII(
signin::kDelayThresholdMinutesToUpdateGaiaCookie);
int commandLineDelay = 0;
if (base::StringToInt(delayString, &commandLineDelay)) {
return base::Minutes(commandLineDelay);
}
}
return kDelayThresholdToUpdateGaiaCookie;
}
} // namespace
// WebStatePolicyDecider that monitors the HTTP headers on Gaia responses,
// reacting on the X-Chrome-Manage-Accounts header and notifying its delegate.
// It also notifies the AccountConsistencyService of domains it should add the
// CHROME_CONNECTED cookie to.
class AccountConsistencyService::AccountConsistencyHandler
: public web::WebStatePolicyDecider,
public web::WebStateObserver {
public:
AccountConsistencyHandler(web::WebState* web_state,
AccountConsistencyService* service,
AccountReconcilor* account_reconcilor,
signin::IdentityManager* identity_manager,
ManageAccountsDelegate* delegate);
void WebStateDestroyed(web::WebState* web_state) override;
AccountConsistencyHandler(const AccountConsistencyHandler&) = delete;
AccountConsistencyHandler& operator=(const AccountConsistencyHandler&) =
delete;
private:
// web::WebStateObserver override.
void PageLoaded(
web::WebState* web_state,
web::PageLoadCompletionStatus load_completion_status) override;
// web::WebStatePolicyDecider override.
void ShouldAllowRequest(
NSURLRequest* request,
web::WebStatePolicyDecider::RequestInfo request_info,
web::WebStatePolicyDecider::PolicyDecisionCallback callback) override;
// Decides on navigation corresponding to |response| whether the navigation
// should continue and updates authentication cookies on Google domains.
void ShouldAllowResponse(
NSURLResponse* response,
web::WebStatePolicyDecider::ResponseInfo response_info,
web::WebStatePolicyDecider::PolicyDecisionCallback callback) override;
void WebStateDestroyed() override;
// Handles the AddAccount request depending on |has_cookie_changed|.
void HandleAddAccountRequest(GURL url, BOOL has_cookie_changed);
// The consistency web sign-in needs to be shown once the page is loaded.
// It is required to avoid having the keyboard showing up on top of the web
// sign-in dialog.
bool show_consistency_web_signin_ = false;
AccountConsistencyService* account_consistency_service_; // Weak.
AccountReconcilor* account_reconcilor_; // Weak.
signin::IdentityManager* identity_manager_;
web::WebState* web_state_;
ManageAccountsDelegate* delegate_; // Weak.
base::WeakPtrFactory<AccountConsistencyHandler> weak_ptr_factory_;
};
AccountConsistencyService::AccountConsistencyHandler::AccountConsistencyHandler(
web::WebState* web_state,
AccountConsistencyService* service,
AccountReconcilor* account_reconcilor,
signin::IdentityManager* identity_manager,
ManageAccountsDelegate* delegate)
: web::WebStatePolicyDecider(web_state),
account_consistency_service_(service),
account_reconcilor_(account_reconcilor),
identity_manager_(identity_manager),
web_state_(web_state),
delegate_(delegate),
weak_ptr_factory_(this) {
web_state->AddObserver(this);
}
void AccountConsistencyService::AccountConsistencyHandler::ShouldAllowRequest(
NSURLRequest* request,
web::WebStatePolicyDecider::RequestInfo request_info,
web::WebStatePolicyDecider::PolicyDecisionCallback callback) {
GURL url = net::GURLWithNSURL(request.URL);
if (signin::IsUrlEligibleForMirrorCookie(url) &&
identity_manager_->HasPrimaryAccount(signin::ConsentLevel::kSignin)) {
// CHROME_CONNECTED cookies are added asynchronously on google.com and
// youtube.com domains when Chrome detects that the user is signed-in. By
// continuing to fulfill the navigation once the cookie request is sent,
// Chrome adopts a best-effort strategy for signing the user into the web if
// necessary.
account_consistency_service_->AddChromeConnectedCookies();
}
std::move(callback).Run(PolicyDecision::Allow());
}
void AccountConsistencyService::AccountConsistencyHandler::ShouldAllowResponse(
NSURLResponse* response,
web::WebStatePolicyDecider::ResponseInfo response_info,
web::WebStatePolicyDecider::PolicyDecisionCallback callback) {
NSHTTPURLResponse* http_response =
base::apple::ObjCCast<NSHTTPURLResponse>(response);
if (!http_response) {
std::move(callback).Run(PolicyDecision::Allow());
return;
}
GURL url = net::GURLWithNSURL(http_response.URL);
// User is showing intent to navigate to a Google-owned domain. Set GAIA and
// CHROME_CONNECTED cookies if the user is signed in (this is filtered in
// ChromeConnectedHelper).
if (signin::IsUrlEligibleForMirrorCookie(url)) {
account_consistency_service_->SetChromeConnectedCookieWithUrls(
{url, GURL(kGoogleUrl)});
}
if (!gaia::HasGaiaSchemeHostPort(url)) {
std::move(callback).Run(PolicyDecision::Allow());
return;
}
NSString* manage_accounts_header = [[http_response allHeaderFields]
objectForKey:
[NSString stringWithUTF8String:signin::kChromeManageAccountsHeader]];
if (!manage_accounts_header) {
// Header that detects whether a user has been prompted to enter their
// credentials on a Gaia sign-on page.
NSString* x_autologin_header = [[http_response allHeaderFields]
objectForKey:[NSString stringWithUTF8String:signin::kAutoLoginHeader]];
if (x_autologin_header) {
show_consistency_web_signin_ = true;
}
std::move(callback).Run(PolicyDecision::Allow());
return;
}
signin::ManageAccountsParams params = signin::BuildManageAccountsParams(
base::SysNSStringToUTF8(manage_accounts_header));
account_reconcilor_->OnReceivedManageAccountsResponse(params.service_type);
switch (params.service_type) {
case signin::GAIA_SERVICE_TYPE_INCOGNITO: {
GURL continue_url = GURL(params.continue_url);
DLOG_IF(ERROR, !params.continue_url.empty() && !continue_url.is_valid())
<< "Invalid continuation URL: \"" << continue_url << "\"";
if (delegate_)
delegate_->OnGoIncognito(continue_url);
break;
}
case signin::GAIA_SERVICE_TYPE_SIGNUP:
case signin::GAIA_SERVICE_TYPE_ADDSESSION:
// This situation is only possible if the all cookies have been deleted by
// ITP restrictions and Chrome has not triggered a cookie refresh.
if (identity_manager_->HasPrimaryAccount(signin::ConsentLevel::kSignin)) {
LogIOSGaiaCookiesState(GaiaCookieStateOnSignedInNavigation::
kGaiaCookieAbsentOnAddSessionNavigation);
GURL continue_url = GURL(params.continue_url);
DLOG_IF(ERROR, !params.continue_url.empty() && !continue_url.is_valid())
<< "Invalid continuation URL: \"" << continue_url << "\"";
if (account_consistency_service_->RestoreGaiaCookies(base::BindOnce(
&AccountConsistencyHandler::HandleAddAccountRequest,
weak_ptr_factory_.GetWeakPtr(), continue_url))) {
// Continue URL will be processed in a callback once Gaia cookies
// have been restored.
return;
}
}
if (delegate_)
delegate_->OnAddAccount();
break;
case signin::GAIA_SERVICE_TYPE_SIGNOUT:
case signin::GAIA_SERVICE_TYPE_DEFAULT:
if (delegate_)
delegate_->OnManageAccounts();
break;
case signin::GAIA_SERVICE_TYPE_NONE:
NOTREACHED();
break;
}
// WKWebView loads a blank page even if the response code is 204
// ("No Content"). http://crbug.com/368717
//
// Manage accounts responses are handled via native UI. Abort this request
// for the following reasons:
// * Avoid loading a blank page in WKWebView.
// * Avoid adding this request to history.
std::move(callback).Run(PolicyDecision::Cancel());
}
void AccountConsistencyService::AccountConsistencyHandler::
HandleAddAccountRequest(GURL url, BOOL has_cookie_changed) {
if (!has_cookie_changed) {
// If the cookies on the device did not need to be updated then the user
// is not in an inconsistent state (where the identities on the device
// are different than those on the web). Fallback to asking the user to
// add an account.
if (delegate_)
delegate_->OnAddAccount();
return;
}
web_state_->OpenURL(web::WebState::OpenURLParams(
url, web::Referrer(), WindowOpenDisposition::CURRENT_TAB,
ui::PAGE_TRANSITION_AUTO_TOPLEVEL, false));
if (delegate_)
delegate_->OnRestoreGaiaCookies();
LogIOSGaiaCookiesState(
GaiaCookieStateOnSignedInNavigation::kGaiaCookieRestoredOnShowInfobar);
}
void AccountConsistencyService::AccountConsistencyHandler::PageLoaded(
web::WebState* web_state,
web::PageLoadCompletionStatus load_completion_status) {
const GURL& url = web_state->GetLastCommittedURL();
if (load_completion_status == web::PageLoadCompletionStatus::FAILURE ||
!google_util::IsGoogleDomainUrl(
url, google_util::ALLOW_SUBDOMAIN,
google_util::DISALLOW_NON_STANDARD_PORTS)) {
return;
}
if (delegate_ && show_consistency_web_signin_ &&
gaia::HasGaiaSchemeHostPort(url)) {
delegate_->OnShowConsistencyPromo(url, web_state);
}
show_consistency_web_signin_ = false;
}
void AccountConsistencyService::AccountConsistencyHandler::WebStateDestroyed(
web::WebState* web_state) {}
void AccountConsistencyService::AccountConsistencyHandler::WebStateDestroyed() {
account_consistency_service_->RemoveWebStateHandler(web_state());
}
AccountConsistencyService::AccountConsistencyService(
web::BrowserState* browser_state,
AccountReconcilor* account_reconcilor,
scoped_refptr<content_settings::CookieSettings> cookie_settings,
signin::IdentityManager* identity_manager)
: browser_state_(browser_state),
account_reconcilor_(account_reconcilor),
cookie_settings_(cookie_settings),
identity_manager_(identity_manager),
active_cookie_manager_requests_for_testing_(0) {
identity_manager_->AddObserver(this);
if (identity_manager_->HasPrimaryAccount(signin::ConsentLevel::kSignin)) {
AddChromeConnectedCookies();
} else {
RemoveAllChromeConnectedCookies(base::OnceClosure());
}
}
AccountConsistencyService::~AccountConsistencyService() {}
BOOL AccountConsistencyService::RestoreGaiaCookies(
base::OnceCallback<void(BOOL)> cookies_restored_callback) {
// We currently enforce a time threshold to update the Gaia cookie
// for signed-in users to prevent calling the expensive method
// |GetAllCookies| in the cookie manager.
if (last_gaia_cookie_update_time_.is_null() ||
base::Time::Now() - last_gaia_cookie_update_time_ >
GetDelayThresholdToUpdateGaiaCookie()) {
network::mojom::CookieManager* cookie_manager =
browser_state_->GetCookieManager();
cookie_manager->GetCookieList(
GaiaUrls::GetInstance()->secure_google_url(),
net::CookieOptions::MakeAllInclusive(),
net::CookiePartitionKeyCollection::Todo(),
base::BindOnce(
&AccountConsistencyService::TriggerGaiaCookieChangeIfDeleted,
base::Unretained(this), std::move(cookies_restored_callback)));
last_gaia_cookie_update_time_ = base::Time::Now();
return YES;
}
return NO;
}
void AccountConsistencyService::TriggerGaiaCookieChangeIfDeleted(
base::OnceCallback<void(BOOL)> cookies_restored_callback,
const net::CookieAccessResultList& cookie_list,
const net::CookieAccessResultList& unused_excluded_cookies) {
gaia_cookies_restored_callbacks_.push_back(
std::move(cookies_restored_callback));
for (const auto& cookie : cookie_list) {
// There may be other cookies besides `kGaiaSigninCookieName` that are
// required. However these cookies are not specified by the server, and thus
// Chrome cannot monitor them. We assume here that ITP will remove all the
// cookies fom the domain at once, and so it is sufficient to monitor only
// one cookie.
if (cookie.cookie.Name() == GaiaConstants::kGaiaSigninCookieName) {
LogIOSGaiaCookiesState(
GaiaCookieStateOnSignedInNavigation::kGaiaCookiePresentOnNavigation);
RunGaiaCookiesRestoredCallbacks(/*has_cookie_changed=*/NO);
return;
}
}
// The SAPISID cookie may have been deleted previous to this update due to
// ITP restrictions marking Google domains as potential trackers.
LogIOSGaiaCookiesState(
GaiaCookieStateOnSignedInNavigation::
kGaiaCookieAbsentOnGoogleAssociatedDomainNavigation);
// Re-generate cookie to ensure that the user is properly signed in.
identity_manager_->GetAccountsCookieMutator()->ForceTriggerOnCookieChange();
}
void AccountConsistencyService::RunGaiaCookiesRestoredCallbacks(
BOOL has_cookie_changed) {
std::vector<base::OnceCallback<void(BOOL)>> callbacks;
std::swap(gaia_cookies_restored_callbacks_, callbacks);
for (base::OnceCallback<void(BOOL)>& callback : callbacks) {
std::move(callback).Run(has_cookie_changed);
}
}
void AccountConsistencyService::SetWebStateHandler(
web::WebState* web_state,
ManageAccountsDelegate* delegate) {
DCHECK(!is_shutdown_) << "SetWebStateHandler called after Shutdown";
DCHECK(handlers_map_.find(web_state) == handlers_map_.end());
handlers_map_.insert(std::make_pair(
web_state,
std::make_unique<AccountConsistencyHandler>(
web_state, this, account_reconcilor_, identity_manager_, delegate)));
}
void AccountConsistencyService::RemoveWebStateHandler(
web::WebState* web_state) {
DCHECK(!is_shutdown_) << "RemoveWebStateHandler called after Shutdown";
auto iter = handlers_map_.find(web_state);
DCHECK(iter != handlers_map_.end());
std::unique_ptr<AccountConsistencyHandler> handler = std::move(iter->second);
handlers_map_.erase(iter);
web_state->RemoveObserver(handler.get());
}
void AccountConsistencyService::RemoveAllChromeConnectedCookies(
base::OnceClosure callback) {
DCHECK(!browser_state_->IsOffTheRecord());
network::mojom::CookieManager* cookie_manager =
browser_state_->GetCookieManager();
network::mojom::CookieDeletionFilterPtr filter =
network::mojom::CookieDeletionFilter::New();
filter->cookie_name = signin::kChromeConnectedCookieName;
++active_cookie_manager_requests_for_testing_;
cookie_manager->DeleteCookies(
std::move(filter),
base::BindOnce(&AccountConsistencyService::OnDeleteCookiesFinished,
base::Unretained(this), std::move(callback)));
}
void AccountConsistencyService::OnDeleteCookiesFinished(
base::OnceClosure callback,
uint32_t unused_num_cookies_deleted) {
--active_cookie_manager_requests_for_testing_;
if (!callback.is_null()) {
std::move(callback).Run();
}
}
void AccountConsistencyService::SetChromeConnectedCookieWithUrls(
const std::vector<const GURL>& urls) {
for (const GURL& url : urls) {
SetChromeConnectedCookieWithUrl(url);
}
}
void AccountConsistencyService::Shutdown() {
DCHECK(handlers_map_.empty()) << "Handlers not unregistered at shutdown";
identity_manager_->RemoveObserver(this);
is_shutdown_ = true;
}
void AccountConsistencyService::SetChromeConnectedCookieWithUrl(
const GURL& url) {
const std::string domain = GetDomainFromUrl(url);
std::string cookie_value = signin::BuildMirrorRequestCookieIfPossible(
url,
identity_manager_->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin)
.gaia,
signin::AccountConsistencyMethod::kMirror, cookie_settings_.get(),
signin::PROFILE_MODE_DEFAULT);
if (cookie_value.empty()) {
return;
}
std::unique_ptr<net::CanonicalCookie> cookie =
net::CanonicalCookie::CreateSanitizedCookie(
url,
/*name=*/signin::kChromeConnectedCookieName, cookie_value,
/*domain=*/domain,
/*path=*/std::string(),
/*creation_time=*/base::Time::Now(),
// Create expiration date of Now+2y to roughly follow the SAPISID
// cookie.
/*expiration_time=*/base::Time::Now() + base::Days(730),
/*last_access_time=*/base::Time(),
/*secure=*/true,
/*httponly=*/false, net::CookieSameSite::LAX_MODE,
net::COOKIE_PRIORITY_DEFAULT, /*same_party=*/false,
/*partition_key=*/absl::nullopt);
net::CookieOptions options;
options.set_include_httponly();
options.set_same_site_cookie_context(
net::CookieOptions::SameSiteCookieContext::MakeInclusive());
++active_cookie_manager_requests_for_testing_;
network::mojom::CookieManager* cookie_manager =
browser_state_->GetCookieManager();
cookie_manager->SetCanonicalCookie(
*cookie, url, options,
base::BindOnce(
&AccountConsistencyService::OnChromeConnectedCookieFinished,
base::Unretained(this)));
}
void AccountConsistencyService::OnChromeConnectedCookieFinished(
net::CookieAccessResult cookie_access_result) {
DCHECK(cookie_access_result.status.IsInclude());
--active_cookie_manager_requests_for_testing_;
}
void AccountConsistencyService::AddChromeConnectedCookies() {
DCHECK(!browser_state_->IsOffTheRecord());
// These cookie requests are preventive. Chrome cannot be sure that
// CHROME_CONNECTED cookies are set on google.com and youtube.com domains due
// to ITP restrictions.
SetChromeConnectedCookieWithUrls({GURL(kGoogleUrl), GURL(kYoutubeUrl)});
}
void AccountConsistencyService::OnBrowsingDataRemoved() {
// SAPISID cookie has been removed, notify the GCMS.
// TODO(https://crbug.com/930582) : Remove the need to expose this method
// or move it to the network::CookieManager.
identity_manager_->GetAccountsCookieMutator()->ForceTriggerOnCookieChange();
}
void AccountConsistencyService::OnPrimaryAccountChanged(
const signin::PrimaryAccountChangeEvent& event) {
switch (event.GetEventTypeFor(signin::ConsentLevel::kSignin)) {
case signin::PrimaryAccountChangeEvent::Type::kSet:
case signin::PrimaryAccountChangeEvent::Type::kNone:
AddChromeConnectedCookies();
break;
case signin::PrimaryAccountChangeEvent::Type::kCleared:
RemoveAllChromeConnectedCookies(base::OnceClosure());
break;
}
}
void AccountConsistencyService::OnAccountsInCookieUpdated(
const signin::AccountsInCookieJarInfo& accounts_in_cookie_jar_info,
const GoogleServiceAuthError& error) {
AddChromeConnectedCookies();
// If signed-in accounts have been recently restored through GAIA cookie
// restoration then run the relevant callback to finish the update process.
if (accounts_in_cookie_jar_info.signed_in_accounts.size() > 0) {
RunGaiaCookiesRestoredCallbacks(/*has_cookie_changed=*/YES);
}
}