blob: e0fc705248553264c1f2a7344e618d22abe6b57a [file] [log] [blame]
// Copyright 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ios/chrome/browser/signin/authentication_service.h"
#include "base/auto_reset.h"
#include "base/bind.h"
#include "base/location.h"
#include "base/metrics/histogram_macros.h"
#include "base/single_thread_task_runner.h"
#include "base/strings/sys_string_conversions.h"
#include "base/threading/thread_task_runner_handle.h"
#include "components/pref_registry/pref_registry_syncable.h"
#include "components/prefs/pref_service.h"
#import "components/signin/ios/browser/features.h"
#import "components/signin/public/base/signin_pref_names.h"
#include "components/signin/public/identity_manager/account_info.h"
#include "components/signin/public/identity_manager/device_accounts_synchronizer.h"
#import "components/signin/public/identity_manager/primary_account_mutator.h"
#include "components/sync/driver/sync_service.h"
#include "components/sync/driver/sync_user_settings.h"
#include "google_apis/gaia/gaia_auth_util.h"
#include "ios/chrome/browser/crash_report/crash_keys_helper.h"
#include "ios/chrome/browser/pref_names.h"
#import "ios/chrome/browser/signin/authentication_service_delegate.h"
#import "ios/chrome/browser/signin/chrome_account_manager_service.h"
#include "ios/chrome/browser/sync/sync_setup_service.h"
#include "ios/chrome/browser/system_flags.h"
#import "ios/chrome/browser/ui/authentication/signin/signin_utils.h"
#include "ios/public/provider/chrome/browser/chrome_browser_provider.h"
#import "ios/public/provider/chrome/browser/signin/chrome_identity.h"
#include "ios/public/provider/chrome/browser/signin/chrome_identity_service.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
namespace {
// Enum describing the different sync states per login methods.
enum LoginMethodAndSyncState {
// Legacy values retained to keep definitions in histograms.xml in sync.
CLIENT_LOGIN_SYNC_OFF,
CLIENT_LOGIN_SYNC_ON,
SHARED_AUTHENTICATION_SYNC_OFF,
SHARED_AUTHENTICATION_SYNC_ON,
// NOTE: Add new login methods and sync states only immediately above this
// line. Also, make sure the enum list in tools/histogram/histograms.xml is
// updated with any change in here.
LOGIN_METHOD_AND_SYNC_STATE_COUNT
};
// A fake account id used in the list of last signed in accounts when migrating
// an email for which the corresponding account was removed.
constexpr char kFakeAccountIdForRemovedAccount[] = "0000000000000";
// Returns the account id associated with |identity|.
CoreAccountId ChromeIdentityToAccountID(
signin::IdentityManager* identity_manager,
ChromeIdentity* identity) {
std::string gaia_id = base::SysNSStringToUTF8([identity gaiaID]);
return identity_manager->FindExtendedAccountInfoByGaiaId(gaia_id).account_id;
}
} // namespace
AuthenticationService::AuthenticationService(
PrefService* pref_service,
SyncSetupService* sync_setup_service,
ChromeAccountManagerService* account_manager_service,
signin::IdentityManager* identity_manager,
syncer::SyncService* sync_service)
: pref_service_(pref_service),
sync_setup_service_(sync_setup_service),
account_manager_service_(account_manager_service),
identity_manager_(identity_manager),
sync_service_(sync_service),
user_approved_account_list_manager_(pref_service),
weak_pointer_factory_(this) {
DCHECK(pref_service_);
DCHECK(sync_setup_service_);
DCHECK(identity_manager_);
DCHECK(sync_service_);
}
AuthenticationService::~AuthenticationService() {
DCHECK(!delegate_);
}
// static
void AuthenticationService::RegisterPrefs(
user_prefs::PrefRegistrySyncable* registry) {
registry->RegisterBooleanPref(prefs::kSigninShouldPromptForSigninAgain,
false);
registry->RegisterListPref(prefs::kSigninLastAccounts);
registry->RegisterBooleanPref(prefs::kSigninLastAccountsMigrated, false);
}
void AuthenticationService::Initialize(
std::unique_ptr<AuthenticationServiceDelegate> delegate) {
CHECK(delegate);
CHECK(!initialized());
delegate_ = std::move(delegate);
initialized_ = true;
// The preference |kSigninAllowed| is not available for pre-MICE users. Except
// for policy exceptions pre-MICE users cannot choose to be in a permanently
// not signed-in state (ie. kSigninAllowed = false).
// If a user sets the preference in MICE and then is rolled back to a pre-MICE
// state, they will be returned to the default state (ie. kSigninAllowed =
// true).
if (!signin::IsMobileIdentityConsistencyEnabled()) {
pref_service_->ClearPref(prefs::kSigninAllowed);
}
MigrateAccountsStoredInPrefsIfNeeded();
HandleForgottenIdentity(nil, true /* should_prompt */);
crash_keys::SetCurrentlySignedIn(
HasPrimaryIdentity(signin::ConsentLevel::kSignin));
identity_service_observation_.Observe(
ios::GetChromeBrowserProvider().GetChromeIdentityService());
// Reload credentials to ensure the accounts from the token service are
// up-to-date.
// As UpdateHaveAccountsChangedAtColdStart is only called while the
// application is cold starting, |keychain_reload| must be set to true.
ReloadCredentialsFromIdentities(/*keychain_reload=*/true);
identity_manager_observation_.Observe(identity_manager_);
OnApplicationWillEnterForeground();
}
void AuthenticationService::Shutdown() {
user_approved_account_list_manager_.Shutdown();
identity_manager_observation_.Reset();
delegate_.reset();
}
void AuthenticationService::OnApplicationWillEnterForeground() {
if (HasPrimaryIdentity(signin::ConsentLevel::kSignin)) {
bool can_sync_start = sync_setup_service_->CanSyncFeatureStart();
LoginMethodAndSyncState loginMethodAndSyncState =
can_sync_start ? SHARED_AUTHENTICATION_SYNC_ON
: SHARED_AUTHENTICATION_SYNC_OFF;
UMA_HISTOGRAM_ENUMERATION("Signin.IOSLoginMethodAndSyncState",
loginMethodAndSyncState,
LOGIN_METHOD_AND_SYNC_STATE_COUNT);
}
UMA_HISTOGRAM_COUNTS_100(
"Signin.IOSNumberOfDeviceAccounts",
[account_manager_service_->GetAllIdentities() count]);
// Clear signin errors on the accounts that had a specific MDM device status.
// This will trigger services to fetch data for these accounts again.
using std::swap;
std::map<CoreAccountId, NSDictionary*> cached_mdm_infos;
swap(cached_mdm_infos_, cached_mdm_infos);
if (!cached_mdm_infos.empty()) {
signin::DeviceAccountsSynchronizer* device_accounts_synchronizer =
identity_manager_->GetDeviceAccountsSynchronizer();
for (const auto& cached_mdm_info : cached_mdm_infos) {
device_accounts_synchronizer->ReloadAccountFromSystem(
cached_mdm_info.first);
}
}
}
void AuthenticationService::SetReauthPromptForSignInAndSync() {
pref_service_->SetBoolean(prefs::kSigninShouldPromptForSigninAgain, true);
}
void AuthenticationService::ResetReauthPromptForSignInAndSync() {
pref_service_->SetBoolean(prefs::kSigninShouldPromptForSigninAgain, false);
}
bool AuthenticationService::ShouldReauthPromptForSignInAndSync() const {
return pref_service_->GetBoolean(prefs::kSigninShouldPromptForSigninAgain);
}
bool AuthenticationService::IsAccountListApprovedByUser() const {
DCHECK(HasPrimaryIdentity(signin::ConsentLevel::kSignin));
std::vector<CoreAccountInfo> accounts_info =
identity_manager_->GetAccountsWithRefreshTokens();
return user_approved_account_list_manager_.IsAccountListApprouvedByUser(
accounts_info);
}
void AuthenticationService::ApproveAccountList() {
DCHECK(HasPrimaryIdentity(signin::ConsentLevel::kSignin));
if (IsAccountListApprovedByUser())
return;
std::vector<CoreAccountInfo> current_accounts_info =
identity_manager_->GetAccountsWithRefreshTokens();
user_approved_account_list_manager_.SetApprovedAccountList(
current_accounts_info);
}
void AuthenticationService::MigrateAccountsStoredInPrefsIfNeeded() {
if (identity_manager_->GetAccountIdMigrationState() ==
signin::IdentityManager::AccountIdMigrationState::MIGRATION_NOT_STARTED) {
return;
}
DCHECK_EQ(signin::IdentityManager::AccountIdMigrationState::MIGRATION_DONE,
identity_manager_->GetAccountIdMigrationState());
if (pref_service_->GetBoolean(prefs::kSigninLastAccountsMigrated)) {
// Already migrated.
return;
}
std::vector<CoreAccountId> account_ids =
user_approved_account_list_manager_.GetApprovedAccountIDList();
std::vector<base::Value> accounts_pref_value;
for (const auto& account_id : account_ids) {
if (identity_manager_->HasAccountWithRefreshToken(account_id)) {
accounts_pref_value.emplace_back(account_id.ToString());
} else {
// The account for |email| was removed since the last application cold
// start. Insert |kFakeAccountIdForRemovedAccount| to ensure the user
// account list has to be approved by the user and the removal won't be
// silently ignored.
accounts_pref_value.emplace_back(kFakeAccountIdForRemovedAccount);
}
}
pref_service_->Set(prefs::kSigninLastAccounts,
base::Value(std::move(accounts_pref_value)));
pref_service_->SetBoolean(prefs::kSigninLastAccountsMigrated, true);
}
bool AuthenticationService::HasPrimaryIdentity(
signin::ConsentLevel consent_level) const {
return GetPrimaryIdentity(consent_level) != nil;
}
bool AuthenticationService::HasPrimaryIdentityManaged(
signin::ConsentLevel consent_level) const {
return identity_manager_
->FindExtendedAccountInfo(
identity_manager_->GetPrimaryAccountInfo(consent_level))
.IsManaged();
}
ChromeIdentity* AuthenticationService::GetPrimaryIdentity(
signin::ConsentLevel consent_level) const {
// There is no authenticated identity if there is no signed in user or if the
// user signed in via the client login flow.
if (!identity_manager_->HasPrimaryAccount(consent_level)) {
return nil;
}
std::string authenticated_gaia_id =
identity_manager_->GetPrimaryAccountInfo(consent_level).gaia;
if (authenticated_gaia_id.empty())
return nil;
return account_manager_service_->GetIdentityWithGaiaID(authenticated_gaia_id);
}
void AuthenticationService::SignIn(ChromeIdentity* identity) {
CHECK(signin::IsSigninAllowed(pref_service_));
DCHECK(account_manager_service_->IsValidIdentity(identity));
ResetReauthPromptForSignInAndSync();
// Load all credentials from SSO library. This must load the credentials
// for the primary account too.
identity_manager_->GetDeviceAccountsSynchronizer()
->ReloadAllAccountsFromSystemWithPrimaryAccount(CoreAccountId());
const CoreAccountId account_id = identity_manager_->PickAccountIdForAccount(
base::SysNSStringToUTF8(identity.gaiaID),
base::SysNSStringToUTF8(identity.userEmail));
// Ensure that the account the user is trying to sign into has been loaded
// from the SSO library and that hosted_domain is set (should be the proper
// hosted domain or kNoHostedDomainFound that are both non-empty strings).
CHECK(identity_manager_->HasAccountWithRefreshToken(account_id));
const AccountInfo account_info =
identity_manager_->FindExtendedAccountInfoByAccountId(account_id);
CHECK(!account_info.IsEmpty());
CHECK(!account_info.hosted_domain.empty());
// |PrimaryAccountManager::SetAuthenticatedAccountId| simply ignores the call
// if there is already a signed in user. Check that there is no signed in
// account or that the new signed in account matches the old one to avoid a
// mismatch between the old and the new authenticated accounts.
if (!identity_manager_->HasPrimaryAccount(signin::ConsentLevel::kSignin)) {
DCHECK(identity_manager_->GetPrimaryAccountMutator());
// Initial sign-in to Chrome does not automatically turn on Sync features.
// The Sync service will be enabled in a separate request to
// |GrantSyncConsent|.
identity_manager_->GetPrimaryAccountMutator()->SetPrimaryAccount(
account_id, signin::ConsentLevel::kSignin);
}
// The primary account should now be set to the expected account_id.
CHECK_EQ(account_id, identity_manager_->GetPrimaryAccountId(
signin::ConsentLevel::kSignin));
crash_keys::SetCurrentlySignedIn(true);
}
void AuthenticationService::GrantSyncConsent(ChromeIdentity* identity) {
DCHECK(account_manager_service_->IsValidIdentity(identity));
DCHECK(identity_manager_->HasPrimaryAccount(signin::ConsentLevel::kSignin));
const CoreAccountId account_id = identity_manager_->PickAccountIdForAccount(
base::SysNSStringToUTF8(identity.gaiaID),
base::SysNSStringToUTF8(identity.userEmail));
const bool success =
identity_manager_->GetPrimaryAccountMutator()->SetPrimaryAccount(
account_id, signin::ConsentLevel::kSync);
CHECK(success);
CHECK_EQ(account_id,
identity_manager_->GetPrimaryAccountId(signin::ConsentLevel::kSync));
// Sets the Sync setup handle to prepare for configuring the Sync data types
// before Sync-the-feature actually starts.
// TODO(crbug.com/1206680): Add EarlGrey tests to ensure that the Sync feature
// only starts after GrantSyncConsent is called.
sync_setup_service_->PrepareForFirstSyncSetup();
// Kick-off sync: The authentication error UI (sign in infobar and warning
// badge in settings screen) check the sync auth error state. Sync
// needs to be kicked off so that it resets the auth error quickly once
// |identity| is reauthenticated.
sync_service_->GetUserSettings()->SetSyncRequested(true);
}
void AuthenticationService::SignOut(
signin_metrics::ProfileSignout signout_source,
bool force_clear_browsing_data,
ProceduralBlock completion) {
if (!identity_manager_->HasPrimaryAccount(signin::ConsentLevel::kSignin)) {
if (completion)
completion();
return;
}
const bool is_managed =
HasPrimaryIdentityManaged(signin::ConsentLevel::kSignin);
// Get first setup complete value before to stop the sync service.
const bool is_first_setup_complete =
sync_setup_service_->IsFirstSetupComplete();
sync_service_->StopAndClear();
auto* account_mutator = identity_manager_->GetPrimaryAccountMutator();
// GetPrimaryAccountMutator() returns nullptr on ChromeOS only.
DCHECK(account_mutator);
account_mutator->ClearPrimaryAccount(
signout_source, signin_metrics::SignoutDelete::kIgnoreMetric);
crash_keys::SetCurrentlySignedIn(false);
cached_mdm_infos_.clear();
bool clear_browsing_data;
if (base::FeatureList::IsEnabled(signin::kSimplifySignOutIOS)) {
// With kSimplifySignOutIOS feature, browsing data for managed account needs
// to be cleared only if sync has started at least once.
clear_browsing_data =
force_clear_browsing_data || (is_managed && is_first_setup_complete);
} else {
clear_browsing_data = force_clear_browsing_data || is_managed;
}
if (clear_browsing_data) {
delegate_->ClearBrowsingData(completion);
} else if (completion) {
completion();
}
}
NSDictionary* AuthenticationService::GetCachedMDMInfo(
ChromeIdentity* identity) const {
auto it = cached_mdm_infos_.find(
ChromeIdentityToAccountID(identity_manager_, identity));
if (it == cached_mdm_infos_.end()) {
return nil;
}
if (!identity_manager_->HasAccountWithRefreshTokenInPersistentErrorState(
it->first)) {
// Account has no error, invalidate the cache.
cached_mdm_infos_.erase(it);
return nil;
}
return it->second;
}
bool AuthenticationService::HasCachedMDMErrorForIdentity(
ChromeIdentity* identity) const {
return GetCachedMDMInfo(identity) != nil;
}
bool AuthenticationService::ShowMDMErrorDialogForIdentity(
ChromeIdentity* identity) {
NSDictionary* cached_info = GetCachedMDMInfo(identity);
if (!cached_info) {
return false;
}
ios::ChromeIdentityService* identity_service =
ios::GetChromeBrowserProvider().GetChromeIdentityService();
identity_service->HandleMDMNotification(identity, cached_info, ^(bool){
});
return true;
}
void AuthenticationService::ResetChromeIdentityServiceObserverForTesting() {
DCHECK(!identity_service_observation_.IsObserving());
identity_service_observation_.Observe(
ios::GetChromeBrowserProvider().GetChromeIdentityService());
}
base::WeakPtr<AuthenticationService> AuthenticationService::GetWeakPtr() {
return weak_pointer_factory_.GetWeakPtr();
}
void AuthenticationService::OnPrimaryAccountChanged(
const signin::PrimaryAccountChangeEvent& event_details) {
switch (event_details.GetEventTypeFor(signin::ConsentLevel::kSignin)) {
case signin::PrimaryAccountChangeEvent::Type::kSet:
// TODO(crbug.com/1217673): Re-add DCHECK if approved account list
// must be empty prior to a set call.
ApproveAccountList();
break;
case signin::PrimaryAccountChangeEvent::Type::kCleared:
user_approved_account_list_manager_.ClearApprovedAccountList();
break;
case signin::PrimaryAccountChangeEvent::Type::kNone:
break;
}
}
void AuthenticationService::OnIdentityListChanged(bool keychain_reload) {
if (!identity_manager_->HasPrimaryAccount(signin::ConsentLevel::kSignin)) {
// IdentityManager::HasPrimaryAccount() needs to be called instead of
// AuthenticationService::HasPrimaryIdentity() or
// AuthenticationService::GetPrimaryIdentity().
// If the primary identity has just been removed, GetPrimaryIdentity()
// would return NO (since this method tests if the primary identity exists
// in ChromeIdentityService).
// In this case, we do need to call ReloadCredentialsFromIdentities().
return;
}
// The list of identities may change while in an authorized call. Signing out
// the authenticated user at this time may lead to crashes (e.g.
// http://crbug.com/398431 ).
// Handle the change of the identity list on the next message loop cycle.
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE,
base::BindOnce(&AuthenticationService::ReloadCredentialsFromIdentities,
GetWeakPtr(), keychain_reload));
}
bool AuthenticationService::HandleMDMNotification(ChromeIdentity* identity,
NSDictionary* user_info) {
ios::ChromeIdentityService* identity_service =
ios::GetChromeBrowserProvider().GetChromeIdentityService();
ios::MDMDeviceStatus status = identity_service->GetMDMDeviceStatus(user_info);
NSDictionary* cached_info = GetCachedMDMInfo(identity);
if (cached_info &&
identity_service->GetMDMDeviceStatus(cached_info) == status) {
// Same status as the last error, ignore it to avoid spamming users.
return false;
}
base::WeakPtr<AuthenticationService> weak_ptr = GetWeakPtr();
ios::MDMStatusCallback callback = ^(bool is_blocked) {
if (is_blocked && weak_ptr.get()) {
// If the identity is blocked, sign out of the account. As only managed
// account can be blocked, this will clear the associated browsing data.
if (identity ==
weak_ptr->GetPrimaryIdentity(signin::ConsentLevel::kSignin)) {
weak_ptr->SignOut(signin_metrics::ABORT_SIGNIN,
/*force_clear_browsing_data=*/false, nil);
}
}
};
if (identity_service->HandleMDMNotification(identity, user_info, callback)) {
cached_mdm_infos_[ChromeIdentityToAccountID(identity_manager_, identity)] =
user_info;
return true;
}
return false;
}
void AuthenticationService::OnAccessTokenRefreshFailed(
ChromeIdentity* identity,
NSDictionary* user_info) {
if (HandleMDMNotification(identity, user_info)) {
return;
}
ios::ChromeIdentityService* identity_service =
ios::GetChromeBrowserProvider().GetChromeIdentityService();
if (!identity_service->IsInvalidGrantError(user_info)) {
// If the failure is not due to an invalid grant, the identity is not
// invalid and there is nothing to do.
return;
}
// Handle the failure of access token refresh on the next message loop cycle.
// |identity| is now invalid and the authentication service might need to
// react to this loss of identity.
// Note that no reload of the credentials is necessary here, as |identity|
// might still be accessible in SSO, and |OnIdentityListChanged| will handle
// this when |identity| will actually disappear from SSO.
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE, base::BindOnce(&AuthenticationService::HandleForgottenIdentity,
base::Unretained(this), identity, true));
}
void AuthenticationService::OnChromeIdentityServiceWillBeDestroyed() {
identity_service_observation_.Reset();
}
void AuthenticationService::HandleForgottenIdentity(
ChromeIdentity* invalid_identity,
bool should_prompt) {
if (!identity_manager_->HasPrimaryAccount(signin::ConsentLevel::kSignin)) {
// User is not signed in. Nothing to do here.
return;
}
ChromeIdentity* authenticated_identity =
GetPrimaryIdentity(signin::ConsentLevel::kSignin);
if (authenticated_identity && authenticated_identity != invalid_identity) {
// |authenticated_identity| exists and is a valid identity. Nothing to do
// here.
return;
}
// Reauth prompt should only be set when the user is syncing, since reauth
// turns on sync by default.
should_prompt = should_prompt && identity_manager_->HasPrimaryAccount(
signin::ConsentLevel::kSync);
// Sign the user out.
SignOut(signin_metrics::ACCOUNT_REMOVED_FROM_DEVICE,
/*force_clear_browsing_data=*/false, nil);
if (should_prompt)
SetReauthPromptForSignInAndSync();
}
void AuthenticationService::ReloadCredentialsFromIdentities(
bool keychain_reload) {
if (is_reloading_credentials_)
return;
base::AutoReset<bool> auto_reset(&is_reloading_credentials_, true);
HandleForgottenIdentity(nil, keychain_reload);
if (!HasPrimaryIdentity(signin::ConsentLevel::kSignin))
return;
DCHECK(
!user_approved_account_list_manager_.GetApprovedAccountIDList().empty());
identity_manager_->GetDeviceAccountsSynchronizer()
->ReloadAllAccountsFromSystemWithPrimaryAccount(
identity_manager_->GetPrimaryAccountId(
signin::ConsentLevel::kSignin));
if (!keychain_reload) {
// The changes come from Chrome, so we can approve this new account list,
// since this change comes from the user.
ApproveAccountList();
}
}