// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ios/chrome/browser/ui/authentication/authentication_flow.h"
#import "base/check_op.h"
#import "base/feature_list.h"
#import "base/ios/block_types.h"
#import "base/memory/raw_ptr.h"
#import "base/metrics/histogram_functions.h"
#import "base/metrics/user_metrics.h"
#import "base/notreached.h"
#import "base/strings/sys_string_conversions.h"
#import "components/bookmarks/common/bookmark_features.h"
#import "components/reading_list/features/reading_list_switches.h"
#import "components/signin/public/base/signin_switches.h"
#import "components/signin/public/identity_manager/tribool.h"
#import "components/sync/service/sync_service.h"
#import "components/sync/service/sync_user_settings.h"
#import "ios/chrome/browser/flags/ios_chrome_flag_descriptions.h"
#import "ios/chrome/browser/policy/model/browser_policy_connector_ios.h"
#import "ios/chrome/browser/policy/model/cloud/user_policy_switch.h"
#import "ios/chrome/browser/shared/model/application_context/application_context.h"
#import "ios/chrome/browser/shared/model/browser/browser.h"
#import "ios/chrome/browser/shared/model/profile/profile_ios.h"
#import "ios/chrome/browser/signin/model/authentication_service.h"
#import "ios/chrome/browser/signin/model/authentication_service_factory.h"
#import "ios/chrome/browser/signin/model/capabilities_types.h"
#import "ios/chrome/browser/signin/model/chrome_account_manager_service.h"
#import "ios/chrome/browser/signin/model/chrome_account_manager_service_factory.h"
#import "ios/chrome/browser/signin/model/constants.h"
#import "ios/chrome/browser/signin/model/identity_manager_factory.h"
#import "ios/chrome/browser/signin/model/system_identity.h"
#import "ios/chrome/browser/signin/model/system_identity_manager.h"
#import "ios/chrome/browser/sync/model/sync_service_factory.h"
#import "ios/chrome/browser/ui/authentication/authentication_flow_performer.h"
#import "ios/chrome/browser/ui/authentication/history_sync/history_sync_capabilities_fetcher.h"
#import "ios/chrome/grit/ios_strings.h"
#import "ios/public/provider/chrome/browser/signin/signin_error_api.h"
#import "ui/base/l10n/l10n_util.h"
using signin_ui::CompletionCallback;
namespace {
// The states of the sign-in flow state machine.
enum AuthenticationState {
BEGIN,
CHECK_SIGNIN_STEPS,
FETCH_MANAGED_STATUS,
SHOW_MANAGED_CONFIRMATION,
SIGN_OUT_IF_NEEDED,
SIGN_IN,
REGISTER_FOR_USER_POLICY,
FETCH_USER_POLICY,
FETCH_CAPABILITIES,
COMPLETE_WITH_SUCCESS,
COMPLETE_WITH_FAILURE,
CLEANUP_BEFORE_DONE,
DONE
};
// Values of Signin.AccountType histogram. This histogram records if the user
// uses a gmail account or a managed account when signing in.
// These values are persisted to logs. Entries should not be renumbered and
// numeric values should never be reused. Keep in sync with SigninAccountType in
// tools/metrics/histograms/metadata/signin/enums.xml.
enum class SigninAccountType {
// Gmail account.
kRegular = 0,
// Managed account.
kManaged = 1,
// Always the last enumerated type.
kMaxValue = kManaged,
};
// Returns yes if the browser has machine level policies.
bool HasMachineLevelPolicies() {
BrowserPolicyConnectorIOS* policy_connector =
GetApplicationContext()->GetBrowserPolicyConnector();
return policy_connector && policy_connector->HasMachineLevelPolicies();
}
} // namespace
@interface AuthenticationFlow ()
// Whether this flow is curently handling an error.
@property(nonatomic, assign) BOOL handlingError;
// The actions to perform following account sign-in.
@property(nonatomic, assign) PostSignInActionSet postSignInActions;
// Checks which sign-in steps to perform and updates member variables
// accordingly.
- (void)checkSigninSteps;
// Continues the sign-in state machine starting from `_state` and invokes
// `_signInCompletion` when finished.
- (void)continueSignin;
// Runs `_signInCompletion` asynchronously with `success` argument.
- (void)completeSignInWithSuccess:(BOOL)success;
// Cancels the current sign-in flow.
- (void)cancelFlow;
// Handles an authentication error and show an alert to the user.
- (void)handleAuthenticationError:(NSError*)error;
@end
@implementation AuthenticationFlow {
UIViewController* _presentingViewController;
CompletionCallback _signInCompletion;
AuthenticationFlowPerformer* _performer;
// State machine tracking.
AuthenticationState _state;
BOOL _didSignIn;
BOOL _failedOrCancelled;
BOOL _shouldSignOut;
BOOL _alreadySignedInWithTheSameAccount;
// YES if the signed in account is a managed account and the sign-in flow
// includes sync.
BOOL _shouldShowManagedConfirmation;
// YES if user policies have to be fetched.
BOOL _shouldFetchUserPolicy;
// YES if user is opted into bookmark and reading list account storage.
BOOL _shouldShowSigninSnackbar;
raw_ptr<Browser> _browser;
id<SystemIdentity> _identityToSignIn;
signin_metrics::AccessPoint _accessPoint;
NSString* _identityToSignInHostedDomain;
// Token to have access to user policies from dmserver.
NSString* _dmToken;
// ID of the client that is registered for user policy.
NSString* _clientID;
// List of IDs that represents the domain of the user. The list will be used
// to compare with a similiar list from device mangement to understand whether
// user and device are managed by the same domain.
NSArray<NSString*>* _userAffiliationIDs;
// This AuthenticationFlow keeps a reference to `self` while a sign-in flow is
// is in progress to ensure it outlives any attempt to destroy it in
// `_signInCompletion`.
AuthenticationFlow* _selfRetainer;
// Capabilities fetcher for the subsequent History Sync Opt-In screen.
HistorySyncCapabilitiesFetcher* _capabilitiesFetcher;
}
@synthesize handlingError = _handlingError;
@synthesize identity = _identityToSignIn;
#pragma mark - Public methods
- (instancetype)initWithBrowser:(Browser*)browser
identity:(id<SystemIdentity>)identity
accessPoint:(signin_metrics::AccessPoint)accessPoint
postSignInActions:(PostSignInActionSet)postSignInActions
presentingViewController:(UIViewController*)presentingViewController {
if ((self = [super init])) {
DCHECK(browser);
DCHECK(presentingViewController);
DCHECK(identity);
_browser = browser;
_identityToSignIn = identity;
_accessPoint = accessPoint;
_postSignInActions = postSignInActions;
_presentingViewController = presentingViewController;
_state = BEGIN;
}
return self;
}
- (void)startSignInWithCompletion:(CompletionCallback)completion {
DCHECK_EQ(BEGIN, _state);
DCHECK(!_signInCompletion);
DCHECK(completion);
_signInCompletion = [completion copy];
_selfRetainer = self;
// Kick off the state machine.
if (!_performer) {
_performer = [[AuthenticationFlowPerformer alloc] initWithDelegate:self];
}
// Make sure -[AuthenticationFlow startSignInWithCompletion:] doesn't call
// the completion block synchronously.
// Related to http://crbug.com/1246480.
__weak __typeof(self) weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
[weakSelf continueSignin];
});
}
- (void)interruptWithAction:(SigninCoordinatorInterrupt)action {
if (_state == DONE) {
return;
}
__weak __typeof(self) weakSelf = self;
[_performer interruptWithAction:action
completion:^() {
[weakSelf performerInterrupted];
}];
}
- (void)performerInterrupted {
if (_state != DONE) {
// The performer might not have been able to continue the flow if it was
// waiting for a callback (e.g. waiting for AccountReconcilor). In this
// case, we force the flow to finish synchronously.
[self cancelFlow];
}
DCHECK_EQ(DONE, _state);
}
- (void)setPresentingViewController:
(UIViewController*)presentingViewController {
_presentingViewController = presentingViewController;
}
#pragma mark - State machine management
- (AuthenticationState)nextStateFailedOrCancelled {
DCHECK(_failedOrCancelled);
switch (_state) {
case BEGIN:
case CHECK_SIGNIN_STEPS:
case FETCH_MANAGED_STATUS:
case SHOW_MANAGED_CONFIRMATION:
case SIGN_OUT_IF_NEEDED:
case SIGN_IN:
case REGISTER_FOR_USER_POLICY:
case FETCH_USER_POLICY:
return COMPLETE_WITH_FAILURE;
case FETCH_CAPABILITIES:
return COMPLETE_WITH_FAILURE;
case COMPLETE_WITH_SUCCESS:
case COMPLETE_WITH_FAILURE:
return CLEANUP_BEFORE_DONE;
case CLEANUP_BEFORE_DONE:
case DONE:
return DONE;
}
}
- (AuthenticationState)nextState {
DCHECK(!self.handlingError);
if (_failedOrCancelled) {
return [self nextStateFailedOrCancelled];
}
DCHECK(!_failedOrCancelled);
switch (_state) {
case BEGIN:
return CHECK_SIGNIN_STEPS;
case CHECK_SIGNIN_STEPS:
return FETCH_MANAGED_STATUS;
case FETCH_MANAGED_STATUS:
if (_shouldShowManagedConfirmation)
return SHOW_MANAGED_CONFIRMATION;
else if (_shouldSignOut)
return SIGN_OUT_IF_NEEDED;
else
return SIGN_IN;
case SHOW_MANAGED_CONFIRMATION:
if (_shouldSignOut)
return SIGN_OUT_IF_NEEDED;
else
return SIGN_IN;
case SIGN_OUT_IF_NEEDED:
return SIGN_IN;
case SIGN_IN:
if (self.postSignInActions.Has(PostSignInAction::kShowSnackbar)) {
_shouldShowSigninSnackbar = YES;
}
if (_shouldFetchUserPolicy) {
return REGISTER_FOR_USER_POLICY;
} else if ([self shouldFetchCapabilities]) {
return FETCH_CAPABILITIES;
} else {
return COMPLETE_WITH_SUCCESS;
}
case REGISTER_FOR_USER_POLICY:
if (!_dmToken.length || !_clientID.length) {
// Skip fetching user policies when registration failed.
if ([self shouldFetchCapabilities]) {
return FETCH_CAPABILITIES;
} else {
return COMPLETE_WITH_SUCCESS;
}
}
// Fetch user policies when registration is successful.
return FETCH_USER_POLICY;
case FETCH_USER_POLICY:
if ([self shouldFetchCapabilities]) {
return FETCH_CAPABILITIES;
} else {
return COMPLETE_WITH_SUCCESS;
}
case FETCH_CAPABILITIES:
return COMPLETE_WITH_SUCCESS;
case COMPLETE_WITH_SUCCESS:
case COMPLETE_WITH_FAILURE:
return CLEANUP_BEFORE_DONE;
case CLEANUP_BEFORE_DONE:
case DONE:
return DONE;
}
}
- (void)continueSignin {
ChromeBrowserState* browserState = [self originalBrowserState];
if (self.handlingError) {
// The flow should not continue while the error is being handled, e.g. while
// the user is being informed of an issue.
return;
}
_state = [self nextState];
switch (_state) {
case BEGIN:
NOTREACHED_IN_MIGRATION();
return;
case CHECK_SIGNIN_STEPS:
[self checkSigninSteps];
[self continueSignin];
return;
case FETCH_MANAGED_STATUS:
[_performer fetchManagedStatus:browserState
forIdentity:_identityToSignIn];
return;
case SHOW_MANAGED_CONFIRMATION: {
[_performer
showManagedConfirmationForHostedDomain:_identityToSignInHostedDomain
viewController:_presentingViewController
browser:_browser];
return;
}
case SIGN_OUT_IF_NEEDED:
[_performer signOutBrowserState:browserState];
return;
case SIGN_IN:
[self signInIdentity:_identityToSignIn];
return;
case REGISTER_FOR_USER_POLICY:
[_performer registerUserPolicy:browserState
forIdentity:_identityToSignIn];
return;
case FETCH_USER_POLICY:
[_performer fetchUserPolicy:browserState
withDmToken:_dmToken
clientID:_clientID
userAffiliationIDs:_userAffiliationIDs
identity:_identityToSignIn];
return;
case FETCH_CAPABILITIES:
[self fetchCapabilities];
return;
case COMPLETE_WITH_SUCCESS:
[self completeSignInWithSuccess:YES];
return;
case COMPLETE_WITH_FAILURE:
if (_didSignIn) {
[_performer signOutImmediatelyFromBrowserState:browserState];
}
[self completeSignInWithSuccess:NO];
return;
case CLEANUP_BEFORE_DONE: {
// Clean up asynchronously to ensure that `self` does not die while
// the flow is running.
DCHECK([NSThread isMainThread]);
dispatch_async(dispatch_get_main_queue(), ^{
self->_selfRetainer = nil;
});
[self continueSignin];
return;
}
case DONE:
return;
}
NOTREACHED_IN_MIGRATION();
}
- (void)checkSigninSteps {
id<SystemIdentity> currentIdentity =
AuthenticationServiceFactory::GetForBrowserState(
[self originalBrowserState])
->GetPrimaryIdentity(signin::ConsentLevel::kSignin);
if (currentIdentity && ![currentIdentity isEqual:_identityToSignIn]) {
// If the identity to sign-in is different than the current identity,
// sign-out is required.
_shouldSignOut = YES;
}
_alreadySignedInWithTheSameAccount =
[currentIdentity isEqual:_identityToSignIn];
}
- (void)signInIdentity:(id<SystemIdentity>)identity {
ChromeBrowserState* browserState = [self originalBrowserState];
ChromeAccountManagerService* accountManagerService =
ChromeAccountManagerServiceFactory::GetForBrowserState(browserState);
if (accountManagerService->IsValidIdentity(identity)) {
[_performer signInIdentity:identity
atAccessPoint:self.accessPoint
withHostedDomain:_identityToSignInHostedDomain
toBrowserState:browserState];
_didSignIn = YES;
[self continueSignin];
} else {
// Handle the case where the identity is no longer valid.
NSError* error = ios::provider::CreateMissingIdentitySigninError();
[self handleAuthenticationError:error];
}
}
// Fetches capabilities on successful authentication for the upcoming History
// Sync Opt-In screen.
- (void)fetchCapabilities {
CHECK([self shouldFetchCapabilities]);
ChromeBrowserState* browserState = [self originalBrowserState];
// Create the capability fetcher and start fetching capabilities.
__weak __typeof(self) weakSelf = self;
_capabilitiesFetcher = [[HistorySyncCapabilitiesFetcher alloc]
initWithIdentityManager:IdentityManagerFactory::GetForBrowserState(
browserState)];
[_capabilitiesFetcher
startFetchingRestrictionCapabilityWithCallback:base::BindOnce(^(
signin::Tribool
capability) {
// The capability value is ignored.
[weakSelf continueSignin];
})];
}
- (void)completeSignInWithSuccess:(BOOL)success {
DCHECK(_signInCompletion)
<< "`completeSignInWithSuccess` should not be called twice.";
if (success) {
base::UmaHistogramEnumeration("Signin.AccountType.SigninConsent",
_identityToSignInHostedDomain.length > 0
? SigninAccountType::kManaged
: SigninAccountType::kRegular);
}
if (_signInCompletion) {
CompletionCallback signInCompletion = _signInCompletion;
_signInCompletion = nil;
signInCompletion(success);
}
if (_shouldShowSigninSnackbar) {
[_performer completePostSignInActions:_postSignInActions
withIdentity:_identityToSignIn
browser:_browser];
}
[self continueSignin];
}
- (void)cancelFlow {
if (_failedOrCancelled) {
// Avoid double handling of cancel or error.
return;
}
_failedOrCancelled = YES;
[self continueSignin];
}
- (void)handleAuthenticationError:(NSError*)error {
if (_failedOrCancelled) {
// Avoid double handling of cancel or error.
return;
}
DCHECK(error);
_failedOrCancelled = YES;
self.handlingError = YES;
__weak AuthenticationFlow* weakSelf = self;
[_performer showAuthenticationError:error
withCompletion:^{
AuthenticationFlow* strongSelf = weakSelf;
if (!strongSelf)
return;
[strongSelf setHandlingError:NO];
[strongSelf continueSignin];
}
viewController:_presentingViewController
browser:_browser];
}
#pragma mark AuthenticationFlowPerformerDelegate
- (void)didSignOut {
[self continueSignin];
}
- (void)didClearData {
[self continueSignin];
}
- (void)didFetchManagedStatus:(NSString*)hostedDomain {
DCHECK_EQ(FETCH_MANAGED_STATUS, _state);
_shouldShowManagedConfirmation =
[self shouldShowManagedConfirmationForHostedDomain:hostedDomain];
_identityToSignInHostedDomain = hostedDomain;
_shouldFetchUserPolicy =
[self shouldFetchUserPolicy] && hostedDomain.length > 0;
[self continueSignin];
}
- (void)didFailFetchManagedStatus:(NSError*)error {
DCHECK_EQ(FETCH_MANAGED_STATUS, _state);
NSError* flowError =
[NSError errorWithDomain:kAuthenticationErrorDomain
code:AUTHENTICATION_FLOW_ERROR
userInfo:@{
NSLocalizedDescriptionKey :
l10n_util::GetNSString(IDS_IOS_SIGN_IN_FAILED),
NSUnderlyingErrorKey : error
}];
[self handleAuthenticationError:flowError];
}
- (void)didAcceptManagedConfirmation {
[self continueSignin];
}
- (void)didCancelManagedConfirmation {
[self cancelFlow];
}
- (void)didRegisterForUserPolicyWithDMToken:(NSString*)dmToken
clientID:(NSString*)clientID
userAffiliationIDs:
(NSArray<NSString*>*)userAffiliationIDs {
DCHECK_EQ(REGISTER_FOR_USER_POLICY, _state);
_dmToken = dmToken;
_clientID = clientID;
_userAffiliationIDs = userAffiliationIDs;
[self continueSignin];
}
- (void)didFetchUserPolicyWithSuccess:(BOOL)success {
DCHECK_EQ(FETCH_USER_POLICY, _state);
DLOG_IF(ERROR, !success) << "Error fetching policy for user";
[self continueSignin];
}
#pragma mark - Private methods
// The original chrome browser state used for services that don't exist in
// incognito mode.
- (ChromeBrowserState*)originalBrowserState {
return _browser->GetBrowserState()->GetOriginalChromeBrowserState();
}
// Returns YES if the managed confirmation dialog should be shown for the
// hosted domain.
- (BOOL)shouldShowManagedConfirmationForHostedDomain:(NSString*)hostedDomain {
if ([hostedDomain length] == 0) {
// No hosted domain, don't show the dialog as there is no host.
return NO;
}
if (HasMachineLevelPolicies()) {
// Don't show the dialog if the browser has already machine level policies
// as the user already knows that their browser is managed.
return NO;
}
// Show the dialog if User Policy is enabled.
return policy::IsAnyUserPolicyFeatureEnabled();
}
// Returns YES if should fetch user policy.
- (BOOL)shouldFetchUserPolicy {
return policy::IsAnyUserPolicyFeatureEnabled();
}
// Return YES if capabilities should be fetched for the History Sync screen.
- (BOOL)shouldFetchCapabilities {
if (!self.precedingHistorySync ||
!base::FeatureList::IsEnabled(
switches::kMinorModeRestrictionsForHistorySyncOptIn)) {
return NO;
}
syncer::SyncService* syncService =
SyncServiceFactory::GetForBrowserState([self originalBrowserState]);
syncer::SyncUserSettings* userSettings = syncService->GetUserSettings();
if (userSettings->GetSelectedTypes().HasAll(
{syncer::UserSelectableType::kHistory,
syncer::UserSelectableType::kTabs})) {
// History Opt-In is already set and the screen won't be shown.
return NO;
}
return YES;
}
#pragma mark - Used for testing
- (void)setPerformerForTesting:(AuthenticationFlowPerformer*)performer {
_performer = performer;
}
@end