chromium/ios/chrome/browser/ui/authentication/authentication_flow.mm

// 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