OOBE: Prevent bypass of the auto-enrollment check

Introduce a mechanism to prevent skipping the auto-enrollment
check during OOBE.

Key changes:
- A new feature flag, `kOobeAutoEnrollmentCheckForced`, guards the
  new logic.
- The preference `kAutoEnrollmentCheckExited` is introduced and
  used to indicate that the enrollment state determination process
  has run.
- OOBE is marked completed only if `kAutoEnrollmentCheckExited` is
  set to true.
- Checks are added at:
   - Before any user sign-in.
   - When the Gaia screen is shown.
- If OOBE is not marked as complete (indicating the check was skipped),
  Sign-in fatal error screen is displayed providing the user with the
  option to "Restart and Powerwash" the device.

Bug: 439252187
Change-Id: I8b0d094d81890f94a0b5af677614b4aaf78bd984
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7015507
Reviewed-by: Danila Kuzmin <dkuzmin@google.com>
Commit-Queue: Osama Fathy <osamafathy@google.com>
Cr-Commit-Position: refs/heads/main@{#1537880}
diff --git a/ash/constants/ash_features.cc b/ash/constants/ash_features.cc
index 8fa698d1..e7bf715 100644
--- a/ash/constants/ash_features.cc
+++ b/ash/constants/ash_features.cc
@@ -1565,6 +1565,9 @@
 // Enables or disables the OOBE QuickStart flow on the login screen.
 BASE_FEATURE(kOobeQuickStartOnLoginScreen, base::FEATURE_DISABLED_BY_DEFAULT);
 
+// Enables the enforcement of AutoEnrollment check in OOBE.
+BASE_FEATURE(kOobeAutoEnrollmentCheckForced, base::FEATURE_DISABLED_BY_DEFAULT);
+
 // Enables or disables Orca for ARC apps.
 BASE_FEATURE(kOrcaArc, base::FEATURE_ENABLED_BY_DEFAULT);
 
@@ -3272,6 +3275,10 @@
          base::FeatureList::IsEnabled(kOobeInputMethods);
 }
 
+bool IsOobeAutoEnrollmentCheckForcedEnabled() {
+  return base::FeatureList::IsEnabled(kOobeAutoEnrollmentCheckForced);
+}
+
 bool IsOobeSplitModifierKeyboardInfoEnabled() {
   return base::FeatureList::IsEnabled(kOobeSplitModifierKeyboardInfo);
 }
diff --git a/ash/constants/ash_features.h b/ash/constants/ash_features.h
index 048fd939..797fe1a 100644
--- a/ash/constants/ash_features.h
+++ b/ash/constants/ash_features.h
@@ -697,6 +697,8 @@
 COMPONENT_EXPORT(ASH_CONSTANTS) BASE_DECLARE_FEATURE(kOobeDisplaySize);
 COMPONENT_EXPORT(ASH_CONSTANTS) BASE_DECLARE_FEATURE(kOobeInputMethods);
 COMPONENT_EXPORT(ASH_CONSTANTS)
+BASE_DECLARE_FEATURE(kOobeAutoEnrollmentCheckForced);
+COMPONENT_EXPORT(ASH_CONSTANTS)
 BASE_DECLARE_FEATURE(kOobeSplitModifierKeyboardInfo);
 COMPONENT_EXPORT(ASH_CONSTANTS) BASE_DECLARE_FEATURE(kOrcaArc);
 COMPONENT_EXPORT(ASH_CONSTANTS) BASE_DECLARE_FEATURE(kOrcaElaborate);
@@ -1279,6 +1281,7 @@
 COMPONENT_EXPORT(ASH_CONSTANTS) bool IsOobeDisplaySizeEnabled();
 COMPONENT_EXPORT(ASH_CONSTANTS) bool IsOobeSplitModifierKeyboardInfoEnabled();
 COMPONENT_EXPORT(ASH_CONSTANTS) bool IsOobeInputMethodsEnabled();
+COMPONENT_EXPORT(ASH_CONSTANTS) bool IsOobeAutoEnrollmentCheckForcedEnabled();
 COMPONENT_EXPORT(ASH_CONSTANTS) bool IsOsSyncConsentRevampEnabled();
 COMPONENT_EXPORT(ASH_CONSTANTS) bool IsParentAccessJellyEnabled();
 COMPONENT_EXPORT(ASH_CONSTANTS) bool IsPcieBillboardNotificationEnabled();
diff --git a/chrome/app/chromeos_strings.grdp b/chrome/app/chromeos_strings.grdp
index 04c4ab5..3eeed688 100644
--- a/chrome/app/chromeos_strings.grdp
+++ b/chrome/app/chromeos_strings.grdp
@@ -1881,6 +1881,13 @@
   <message name="IDS_LOGIN_FATAL_ERROR_NO_AUTH_TOKEN" desc="Message to show when the authentication could not be completed because the user's auth token retrieved.">
     Sign-in failed because your access token could not be retrieved. Please check your network connection and try again.
   </message>
+  <message name="IDS_LOGIN_FATAL_ERROR_AUTO_ENROLLMENT_SKIPPED" desc="Message to show when the auto enrollment check is skipped.">
+    Sign-in failed due to a device configuration check issue. Please powerwash your <ph name="DEVICE_TYPE">$1<ex>Chromebook</ex></ph> and try again.
+  </message>
+  <message name="IDS_LOGIN_FATAL_ERROR_RESTART_AND_POWERWASH_BUTTON" desc="Button to trigger a reboot and a powerwash.">
+    Restart and Powerwash
+  </message>
+
   <message name="IDS_LOGIN_SAML_INTERSTITIAL_MESSAGE" desc="Message to show on the SAML interstitial page to tell the user to continue signing in using their enterprise account. MANAGER can be a domain or an email address.">
     This device is managed by <ph name="MANAGER">$1<ex>acmecorp.com</ex></ph> and requires you to sign in every time.
   </message>
diff --git a/chrome/app/chromeos_strings_grdp/IDS_LOGIN_FATAL_ERROR_AUTO_ENROLLMENT_SKIPPED.png.sha1 b/chrome/app/chromeos_strings_grdp/IDS_LOGIN_FATAL_ERROR_AUTO_ENROLLMENT_SKIPPED.png.sha1
new file mode 100644
index 0000000..fff79db
--- /dev/null
+++ b/chrome/app/chromeos_strings_grdp/IDS_LOGIN_FATAL_ERROR_AUTO_ENROLLMENT_SKIPPED.png.sha1
@@ -0,0 +1 @@
+a6a7c64365d35a5e272dc6fa07a2c57fc3ab5824
\ No newline at end of file
diff --git a/chrome/app/chromeos_strings_grdp/IDS_LOGIN_FATAL_ERROR_RESTART_AND_POWERWASH_BUTTON.png.sha1 b/chrome/app/chromeos_strings_grdp/IDS_LOGIN_FATAL_ERROR_RESTART_AND_POWERWASH_BUTTON.png.sha1
new file mode 100644
index 0000000..fff79db
--- /dev/null
+++ b/chrome/app/chromeos_strings_grdp/IDS_LOGIN_FATAL_ERROR_RESTART_AND_POWERWASH_BUTTON.png.sha1
@@ -0,0 +1 @@
+a6a7c64365d35a5e272dc6fa07a2c57fc3ab5824
\ No newline at end of file
diff --git a/chrome/browser/ash/login/enrollment/auto_enrollment_check_screen.cc b/chrome/browser/ash/login/enrollment/auto_enrollment_check_screen.cc
index 588d0cdf..cfd1313 100644
--- a/chrome/browser/ash/login/enrollment/auto_enrollment_check_screen.cc
+++ b/chrome/browser/ash/login/enrollment/auto_enrollment_check_screen.cc
@@ -6,12 +6,14 @@
 
 #include <optional>
 
+#include "ash/constants/ash_features.h"
 #include "base/functional/bind.h"
 #include "base/functional/callback.h"
 #include "base/location.h"
 #include "base/notreached.h"
 #include "base/task/single_thread_task_runner.h"
 #include "chrome/browser/ash/login/error_screens_histogram_helper.h"
+#include "chrome/browser/ash/login/login_pref_names.h"
 #include "chrome/browser/ash/login/screen_manager.h"
 #include "chrome/browser/ash/login/screens/error_screen.h"
 #include "chrome/browser/ash/login/screens/network_error.h"
@@ -19,6 +21,7 @@
 #include "chrome/browser/ash/policy/enrollment/auto_enrollment_controller.h"
 #include "chrome/browser/ash/policy/enrollment/auto_enrollment_state.h"
 #include "chrome/browser/ash/policy/enrollment/auto_enrollment_type_checker.h"
+#include "chrome/browser/browser_process.h"
 #include "chromeos/ash/components/network/network_handler.h"
 #include "chromeos/ash/components/network/network_state.h"
 #include "chromeos/ash/components/network/network_state_handler.h"
@@ -260,4 +263,12 @@
   auto_enrollment_controller_->Start();
 }
 
+void AutoEnrollmentCheckScreen::RunExitCallback(Result result) {
+  if (ash::features::IsOobeAutoEnrollmentCheckForcedEnabled()) {
+    g_browser_process->local_state()->SetBoolean(
+        ash::prefs::kAutoEnrollmentCheckExited, true);
+  }
+  exit_callback_.Run(result);
+}
+
 }  // namespace ash
diff --git a/chrome/browser/ash/login/enrollment/auto_enrollment_check_screen.h b/chrome/browser/ash/login/enrollment/auto_enrollment_check_screen.h
index cff69f29..254117a7 100644
--- a/chrome/browser/ash/login/enrollment/auto_enrollment_check_screen.h
+++ b/chrome/browser/ash/login/enrollment/auto_enrollment_check_screen.h
@@ -77,7 +77,7 @@
   // Runs `exit_callback_` - used to prevent `exit_callback_` from running after
   // `this` has been destroyed (by wrapping it with a callback bound to a weak
   // ptr).
-  void RunExitCallback(Result result) { exit_callback_.Run(result); }
+  void RunExitCallback(Result result);
 
  private:
   // Handles update notifications regarding the auto-enrollment check.
diff --git a/chrome/browser/ash/login/enrollment/enrollment_launcher.cc b/chrome/browser/ash/login/enrollment/enrollment_launcher.cc
index 2df4ea8..60bc6d6 100644
--- a/chrome/browser/ash/login/enrollment/enrollment_launcher.cc
+++ b/chrome/browser/ash/login/enrollment/enrollment_launcher.cc
@@ -9,6 +9,7 @@
 #include <string>
 #include <utility>
 
+#include "ash/constants/ash_features.h"
 #include "base/check.h"
 #include "base/check_is_test.h"
 #include "base/functional/bind.h"
@@ -26,6 +27,7 @@
 #include "chrome/browser/ash/attestation/attestation_ca_client.h"
 #include "chrome/browser/ash/login/enrollment/enrollment_uma.h"
 #include "chrome/browser/ash/login/enrollment/oauth2_token_revoker.h"
+#include "chrome/browser/ash/login/login_pref_names.h"
 #include "chrome/browser/ash/login/startup_utils.h"
 #include "chrome/browser/ash/policy/core/browser_policy_connector_ash.h"
 #include "chrome/browser/ash/policy/core/device_cloud_policy_client_factory_ash.h"
@@ -48,6 +50,7 @@
 #include "components/policy/core/common/cloud/cloud_policy_constants.h"
 #include "components/policy/core/common/cloud/dm_auth.h"
 #include "components/policy/core/common/cloud/enterprise_metrics.h"
+#include "components/prefs/pref_service.h"
 #include "google_apis/gaia/gaia_auth_consumer.h"
 #include "google_apis/gaia/gaia_auth_fetcher.h"
 #include "google_apis/gaia/google_service_auth_error.h"
@@ -437,7 +440,14 @@
   }
 
   success_ = true;
-  StartupUtils::MarkOobeCompleted();
+
+  // TODO(crbug.com/454136007): Investigate why OOBE is marked completed here
+  if (!features::IsOobeAutoEnrollmentCheckForcedEnabled() ||
+      g_browser_process->local_state()->GetBoolean(
+          prefs::kAutoEnrollmentCheckExited)) {
+    StartupUtils::MarkOobeCompleted();
+  }
+
   status_consumer_->OnDeviceEnrolled();
 }
 
diff --git a/chrome/browser/ash/login/existing_user_controller.cc b/chrome/browser/ash/login/existing_user_controller.cc
index cd0afe2..bbe93bd0 100644
--- a/chrome/browser/ash/login/existing_user_controller.cc
+++ b/chrome/browser/ash/login/existing_user_controller.cc
@@ -45,6 +45,7 @@
 #include "chrome/browser/ash/login/auth/chrome_login_performer.h"
 #include "chrome/browser/ash/login/demo_mode/demo_login_controller.h"
 #include "chrome/browser/ash/login/helper.h"
+#include "chrome/browser/ash/login/oobe_metrics_helper.h"
 #include "chrome/browser/ash/login/profile_auth_data.h"
 #include "chrome/browser/ash/login/quick_unlock/pin_salt_storage.h"
 #include "chrome/browser/ash/login/quick_unlock/pin_storage_cryptohome.h"
@@ -1398,6 +1399,24 @@
   signin_ui->ShowSigninError(error, details);
 }
 
+void ExistingUserController::ShowOobeNotCompletedError() {
+  CHECK(features::IsOobeAutoEnrollmentCheckForcedEnabled())
+      << "ExistingUserController::ShowOobeNotCompletedError() should only be "
+         "called when OobeAutoEnrollmentCheckForced is enabled";
+  auto* signin_ui = GetLoginDisplayHost()->GetSigninUI();
+  if (!signin_ui) {
+    DCHECK(session_manager::SessionManager::Get()->IsInSecondaryLoginScreen());
+    // Silently ignore the error on the secondary login screen. The screen is
+    // being deprecated anyway.
+    return;
+  }
+  GetLoginDisplayHost()
+      ->GetOobeMetricsHelper()
+      ->RecordOobeNotCompletedErrorTrigger(
+          OobeMetricsHelper::OobeNotCompletedTrigger::kExistingUserController);
+  signin_ui->ShowOobeNotCompletedError();
+}
+
 void ExistingUserController::SendAccessibilityAlert(
     const std::string& alert_text) {
   AutomationManagerAura::GetInstance()->HandleAlert(alert_text);
@@ -1524,6 +1543,27 @@
     return;
   }
 
+  if (features::IsOobeAutoEnrollmentCheckForcedEnabled() &&
+      !StartupUtils::IsOobeCompleted()) {
+    // If OOBE is not yet completed, abort the current login attempt. This
+    // indicates a potential bypass attempt and an error screen is shown.
+    ++num_login_attempts_;
+
+    auto* wizard_controller = GetLoginDisplayHost()->GetWizardController();
+    CHECK(wizard_controller);
+    ShowOobeNotCompletedError();
+
+    // Re-enable clicking on other windows and the status area. Do not start the
+    // auto-login timer though. Without trusted `cros_settings_`, no auto-login
+    // can succeed.
+    if (GetLoginDisplayHost()->GetWebUILoginView()) {
+      GetLoginDisplayHost()
+          ->GetWebUILoginView()
+          ->SetKeyboardEventsAndSystemTrayEnabled(true);
+    }
+    return;
+  }
+
   if (system::DeviceDisablingManager::IsDeviceDisabledDuringNormalOperation()) {
     // If the device is disabled, bail out. A device disabled screen will be
     // shown by the DeviceDisablingManager.
diff --git a/chrome/browser/ash/login/existing_user_controller.h b/chrome/browser/ash/login/existing_user_controller.h
index ba62c451..e573380 100644
--- a/chrome/browser/ash/login/existing_user_controller.h
+++ b/chrome/browser/ash/login/existing_user_controller.h
@@ -200,6 +200,10 @@
   // not localized.
   void ShowError(SigninError error, const std::string& details);
 
+  // Shows an error message because the OOBE is not marked as completed.
+  // This occurs if StartupUtils::IsOobeCompleted() returns false unexpectedly.
+  void ShowOobeNotCompletedError();
+
   // Shows privacy notification in case of auto lunch managed guest session.
   void ShowAutoLaunchManagedGuestSessionNotification();
 
diff --git a/chrome/browser/ash/login/login_pref_names.h b/chrome/browser/ash/login/login_pref_names.h
index dd88776e..09886a3b 100644
--- a/chrome/browser/ash/login/login_pref_names.h
+++ b/chrome/browser/ash/login/login_pref_names.h
@@ -193,6 +193,11 @@
 // serialization obtained from PrefService::SetTime().
 inline constexpr char kLastOnlineSignInTime[] = "last_online_sign_in_time";
 
+// A boolean pref that indicates whether the auto enrollment check has
+// completed and exited. This is used to prevent OOBE completion if the
+// auto enrollment check was bypassed.
+inline constexpr char kAutoEnrollmentCheckExited[] =
+    "AutoEnrollmentCheckExited";
 }  // namespace ash::prefs
 
 #endif  // CHROME_BROWSER_ASH_LOGIN_LOGIN_PREF_NAMES_H_
diff --git a/chrome/browser/ash/login/oobe_metrics_helper.cc b/chrome/browser/ash/login/oobe_metrics_helper.cc
index 0b870a0..e9007e5d 100644
--- a/chrome/browser/ash/login/oobe_metrics_helper.cc
+++ b/chrome/browser/ash/login/oobe_metrics_helper.cc
@@ -492,6 +492,11 @@
   }
 }
 
+void OobeMetricsHelper::RecordOobeNotCompletedErrorTrigger(
+    OobeNotCompletedTrigger trigger) {
+  base::UmaHistogramEnumeration("OOBE.OobeNotCompletedErrorTrigger", trigger);
+}
+
 void OobeMetricsHelper::AddObserver(Observer* observer) {
   observers_.AddObserver(observer);
 }
diff --git a/chrome/browser/ash/login/oobe_metrics_helper.h b/chrome/browser/ash/login/oobe_metrics_helper.h
index 2a88236..6bf2106 100644
--- a/chrome/browser/ash/login/oobe_metrics_helper.h
+++ b/chrome/browser/ash/login/oobe_metrics_helper.h
@@ -34,6 +34,17 @@
   // or deleted. Only additions possible.
   enum class ScreenShownStatus { kSkipped = 0, kShown = 1, kMaxValue = kShown };
 
+  // This enum is tied directly to a UMA enum defined in
+  // //tools/metrics/histograms/enums.xml, and should always reflect it (do not
+  // change one without changing the other). Entries should be never modified
+  // or deleted. Only additions possible.
+  enum class OobeNotCompletedTrigger {
+    kPerformOobeCompletedAction = 0,
+    kGaiaScreen = 1,
+    kExistingUserController = 2,
+    kMaxValue = kExistingUserController
+  };
+
   // The type of flow completed when pre-login OOBE is completed.
   enum class CompletedPreLoginOobeFlowType {
     kAutoEnrollment = 0,
@@ -160,6 +171,8 @@
 
   void RecordChromeVersion();
 
+  void RecordOobeNotCompletedErrorTrigger(OobeNotCompletedTrigger trigger);
+
   void AddObserver(Observer* observer);
 
   void RemoveObserver(Observer* observer);
diff --git a/chrome/browser/ash/login/screens/gaia_screen.cc b/chrome/browser/ash/login/screens/gaia_screen.cc
index f9875e11..47a27ed 100644
--- a/chrome/browser/ash/login/screens/gaia_screen.cc
+++ b/chrome/browser/ash/login/screens/gaia_screen.cc
@@ -13,6 +13,7 @@
 #include "base/memory/weak_ptr.h"
 #include "base/values.h"
 #include "chrome/browser/ash/login/demo_mode/demo_setup_controller.h"
+#include "chrome/browser/ash/login/startup_utils.h"
 #include "chrome/browser/ash/login/wizard_context.h"
 #include "chrome/browser/ash/login/wizard_controller.h"
 #include "chrome/browser/ash/policy/enrollment/account_status_check_fetcher.h"
@@ -84,6 +85,7 @@
       return "EnterpriseEnroll";
     case Result::ENTER_QUICK_START:
       return "EnterQuickStart";
+    case Result::ERROR_OOBE_NOT_COMPLETED:
     case Result::QUICK_START_ONGOING:
       return BaseScreen::kNotApplicable;
   }
@@ -110,6 +112,12 @@
     return true;
   }
 
+  if (features::IsOobeAutoEnrollmentCheckForcedEnabled() &&
+      !StartupUtils::IsOobeCompleted()) {
+    exit_callback_.Run(Result::ERROR_OOBE_NOT_COMPLETED);
+    return true;
+  }
+
   return false;
 }
 
diff --git a/chrome/browser/ash/login/screens/gaia_screen.h b/chrome/browser/ash/login/screens/gaia_screen.h
index 0c1e584..e5a4ef62 100644
--- a/chrome/browser/ash/login/screens/gaia_screen.h
+++ b/chrome/browser/ash/login/screens/gaia_screen.h
@@ -47,6 +47,7 @@
     ENTERPRISE_ENROLL,
     ENTER_QUICK_START,
     QUICK_START_ONGOING,
+    ERROR_OOBE_NOT_COMPLETED,
   };
 
   static std::string GetResultString(Result result);
diff --git a/chrome/browser/ash/login/screens/signin_fatal_error_screen.cc b/chrome/browser/ash/login/screens/signin_fatal_error_screen.cc
index 0f44f69..69e4c06 100644
--- a/chrome/browser/ash/login/screens/signin_fatal_error_screen.cc
+++ b/chrome/browser/ash/login/screens/signin_fatal_error_screen.cc
@@ -7,11 +7,13 @@
 #include "base/values.h"
 #include "chrome/browser/ui/ash/login/login_display_host.h"
 #include "chrome/browser/ui/webui/ash/login/signin_fatal_error_screen_handler.h"
+#include "chromeos/ash/components/dbus/session_manager/session_manager_client.h"
 
 namespace ash {
 namespace {
 
 constexpr char kUserActionScreenDismissed[] = "screen-dismissed";
+constexpr char kUserActionRestartAndPowerwash[] = "restart-and-powerwash";
 constexpr char kUserActionLearnMore[] = "learn-more";
 
 }  // namespace
@@ -65,6 +67,9 @@
   const std::string& action_id = args[0].GetString();
   if (action_id == kUserActionScreenDismissed) {
     exit_callback_.Run();
+  } else if (action_id == kUserActionRestartAndPowerwash) {
+    CHECK(error_state_ == Error::kOobeCompletionSkipped);
+    SessionManagerClient::Get()->StartDeviceWipe(base::DoNothing());
   } else if (action_id == kUserActionLearnMore) {
     if (!help_app_.get()) {
       help_app_ = new HelpAppLauncher(
diff --git a/chrome/browser/ash/login/screens/signin_fatal_error_screen.h b/chrome/browser/ash/login/screens/signin_fatal_error_screen.h
index be8aa5aa..eb49351 100644
--- a/chrome/browser/ash/login/screens/signin_fatal_error_screen.h
+++ b/chrome/browser/ash/login/screens/signin_fatal_error_screen.h
@@ -30,7 +30,8 @@
     kScrapedPasswordVerificationFailure = 1,
     kInsecureContentBlocked = 2,
     kMissingGaiaInfo = 3,
-    kCustom = 4,
+    kOobeCompletionSkipped = 4,
+    kCustom = 5,
   };
 
   explicit SignInFatalErrorScreen(base::WeakPtr<SignInFatalErrorView> view,
diff --git a/chrome/browser/ash/login/startup_utils.cc b/chrome/browser/ash/login/startup_utils.cc
index 699a171..f8befc2 100644
--- a/chrome/browser/ash/login/startup_utils.cc
+++ b/chrome/browser/ash/login/startup_utils.cc
@@ -137,6 +137,8 @@
   registry->RegisterIntegerPref(
       prefs::kAuthenticationFlowAutoReloadInterval,
       constants::kDefaultAuthenticationFlowAutoReloadInterval);
+
+  registry->RegisterBooleanPref(prefs::kAutoEnrollmentCheckExited, false);
 }
 
 // static
@@ -271,6 +273,8 @@
       prefs::kOobeScreenAfterConsumerUpdate);
   g_browser_process->local_state()->ClearPref(
       prefs::kOobeCriticalUpdateCompleted);
+  g_browser_process->local_state()->ClearPref(
+      prefs::kAutoEnrollmentCheckExited);
 }
 
 // static
diff --git a/chrome/browser/ash/login/wizard_controller.cc b/chrome/browser/ash/login/wizard_controller.cc
index 2fe12e5..be4a31f 100644
--- a/chrome/browser/ash/login/wizard_controller.cc
+++ b/chrome/browser/ash/login/wizard_controller.cc
@@ -1546,6 +1546,15 @@
     case GaiaScreen::Result::QUICK_START_ONGOING:
       ShowQuickStartScreen();
       break;
+    case GaiaScreen::Result::ERROR_OOBE_NOT_COMPLETED:
+      GetLoginDisplayHost()
+          ->GetOobeMetricsHelper()
+          ->RecordOobeNotCompletedErrorTrigger(
+              OobeMetricsHelper::OobeNotCompletedTrigger::kGaiaScreen);
+      ShowSignInFatalErrorScreen(
+          SignInFatalErrorScreen::Error::kOobeCompletionSkipped,
+          base::Value::Dict());
+      break;
   }
 }
 
@@ -3049,6 +3058,25 @@
     return;
   }
 
+  if (features::IsOobeAutoEnrollmentCheckForcedEnabled()) {
+    // To prevent auto-enrollment bypass, check if `kAutoEnrollmentCheckExited`
+    // has been set. If it's false, the auto-enrollment check was not properly
+    // completed.
+    if (!GetLocalState()->GetBoolean(prefs::kAutoEnrollmentCheckExited)) {
+      GetLoginDisplayHost()
+          ->GetOobeMetricsHelper()
+          ->RecordOobeNotCompletedErrorTrigger(
+              OobeMetricsHelper::OobeNotCompletedTrigger::
+                  kPerformOobeCompletedAction);
+
+      // Show a fatal error and do not mark OOBE as completed.
+      ShowSignInFatalErrorScreen(
+          SignInFatalErrorScreen::Error::kOobeCompletionSkipped,
+          base::Value::Dict());
+      return;
+    }
+  }
+
   StartupUtils::MarkOobeCompleted();
   GetLoginDisplayHost()->GetOobeMetricsHelper()->RecordPreLoginOobeComplete(
       flow_type);
diff --git a/chrome/browser/resources/chromeos/login/components/oobe_types.ts b/chrome/browser/resources/chromeos/login/components/oobe_types.ts
index fdb05b337..cb8f672 100644
--- a/chrome/browser/resources/chromeos/login/components/oobe_types.ts
+++ b/chrome/browser/resources/chromeos/login/components/oobe_types.ts
@@ -149,7 +149,8 @@
     SCRAPED_PASSWORD_VERIFICATION_FAILURE = 1,
     INSECURE_CONTENT_BLOCKED = 2,
     MISSING_GAIA_INFO = 3,
-    CUSTOM = 4,
+    OOBE_COMPLETION_SKIPPED = 4,
+    CUSTOM = 5,
   }
 
   /**
diff --git a/chrome/browser/resources/chromeos/login/screens/common/signin_fatal_error.ts b/chrome/browser/resources/chromeos/login/screens/common/signin_fatal_error.ts
index 52d8017..2788f2c 100644
--- a/chrome/browser/resources/chromeos/login/screens/common/signin_fatal_error.ts
+++ b/chrome/browser/resources/chromeos/login/screens/common/signin_fatal_error.ts
@@ -127,7 +127,11 @@
   }
 
   private onClick(): void {
-    this.userActed('screen-dismissed');
+    if (this.errorState === OobeTypes.FatalErrorCode.OOBE_COMPLETION_SKIPPED) {
+      this.userActed('restart-and-powerwash');
+    } else {
+      this.userActed('screen-dismissed');
+    }
   }
 
   /**
@@ -137,6 +141,8 @@
   private computeButtonKey(errorState: OobeTypes.FatalErrorCode) {
     if (errorState === OobeTypes.FatalErrorCode.INSECURE_CONTENT_BLOCKED) {
       return 'fatalErrorDoneButton';
+    } else if (errorState == OobeTypes.FatalErrorCode.OOBE_COMPLETION_SKIPPED) {
+      return 'fatalErrorRestartAndPowerwash';
     }
 
     return 'fatalErrorTryAgainButton';
@@ -158,6 +164,8 @@
         const url = params.url;
         return this.i18nDynamic(
             locale, 'fatalErrorMessageInsecureURL', url || '');
+      case OobeTypes.FatalErrorCode.OOBE_COMPLETION_SKIPPED:
+        return this.i18nDynamic(locale, 'fatalErrorAutoEnrollmentSkipped');
       case OobeTypes.FatalErrorCode.CUSTOM:
         return params.errorText || '';
       default:
diff --git a/chrome/browser/ui/ash/login/login_display_host_common.cc b/chrome/browser/ui/ash/login/login_display_host_common.cc
index 174cbaaf..beca742 100644
--- a/chrome/browser/ui/ash/login/login_display_host_common.cc
+++ b/chrome/browser/ui/ash/login/login_display_host_common.cc
@@ -22,6 +22,7 @@
 #include "base/notreached.h"
 #include "base/task/single_thread_task_runner.h"
 #include "base/types/pass_key.h"
+#include "base/values.h"
 #include "chrome/browser/ash/accessibility/accessibility_manager.h"
 #include "chrome/browser/ash/app_mode/kiosk_app_types.h"
 #include "chrome/browser/ash/app_mode/kiosk_controller.h"
@@ -662,6 +663,13 @@
   StartWizard(SignInFatalErrorView::kScreenId);
 }
 
+void LoginDisplayHostCommon::ShowOobeNotCompletedError() {
+  GetWizardController()->GetScreen<SignInFatalErrorScreen>()->SetErrorState(
+      SignInFatalErrorScreen::Error::kOobeCompletionSkipped,
+      base::Value::Dict());
+  StartWizard(SignInFatalErrorView::kScreenId);
+}
+
 void LoginDisplayHostCommon::SAMLConfirmPassword(
     ::login::StringList scraped_passwords,
     std::unique_ptr<UserContext> user_context) {
diff --git a/chrome/browser/ui/ash/login/login_display_host_common.h b/chrome/browser/ui/ash/login/login_display_host_common.h
index 3c5c18f..6895033 100644
--- a/chrome/browser/ui/ash/login/login_display_host_common.h
+++ b/chrome/browser/ui/ash/login/login_display_host_common.h
@@ -85,6 +85,8 @@
       base::OnceCallback<void(std::unique_ptr<UserContext>)> on_skip_migration)
       final;
   void ShowSigninError(SigninError error, const std::string& details) final;
+  void ShowOobeNotCompletedError() final;
+
   void SAMLConfirmPassword(::login::StringList scraped_passwords,
                            std::unique_ptr<UserContext> user_context) final;
   WizardContext* GetWizardContextForTesting() final;
diff --git a/chrome/browser/ui/ash/login/mock_signin_ui.h b/chrome/browser/ui/ash/login/mock_signin_ui.h
index dcfb22e..180b188a03 100644
--- a/chrome/browser/ui/ash/login/mock_signin_ui.h
+++ b/chrome/browser/ui/ash/login/mock_signin_ui.h
@@ -53,6 +53,7 @@
               ShowSigninError,
               (SigninError, const std::string&),
               (override));
+  MOCK_METHOD(void, ShowOobeNotCompletedError, (), (override));
   MOCK_METHOD(void,
               SAMLConfirmPassword,
               (::login::StringList, std::unique_ptr<UserContext>),
diff --git a/chrome/browser/ui/ash/login/signin_ui.h b/chrome/browser/ui/ash/login/signin_ui.h
index 4596e18d..dd0391d 100644
--- a/chrome/browser/ui/ash/login/signin_ui.h
+++ b/chrome/browser/ui/ash/login/signin_ui.h
@@ -83,6 +83,8 @@
   virtual void ShowSigninError(SigninError error,
                                const std::string& details) = 0;
 
+  virtual void ShowOobeNotCompletedError() = 0;
+
   // Show the SAML Confirm Password screen and continue authentication after
   // that (or show the error screen).
   virtual void SAMLConfirmPassword(
diff --git a/chrome/browser/ui/webui/ash/login/signin_fatal_error_screen_handler.cc b/chrome/browser/ui/webui/ash/login/signin_fatal_error_screen_handler.cc
index 76e0e50..46084b0a2 100644
--- a/chrome/browser/ui/webui/ash/login/signin_fatal_error_screen_handler.cc
+++ b/chrome/browser/ui/webui/ash/login/signin_fatal_error_screen_handler.cc
@@ -13,6 +13,7 @@
 #include "chrome/grit/generated_resources.h"
 #include "components/login/localized_values_builder.h"
 #include "components/strings/grit/components_strings.h"
+#include "ui/chromeos/devicetype_utils.h"
 
 namespace ash {
 
@@ -36,6 +37,11 @@
                IDS_LOGIN_FATAL_ERROR_NO_ACCOUNT_DETAILS);
   builder->Add("fatalErrorMessageInsecureURL",
                IDS_LOGIN_FATAL_ERROR_TEXT_INSECURE_URL);
+  builder->AddF("fatalErrorAutoEnrollmentSkipped",
+                IDS_LOGIN_FATAL_ERROR_AUTO_ENROLLMENT_SKIPPED,
+                ui::GetChromeOSDeviceName());
+  builder->Add("fatalErrorRestartAndPowerwash",
+               IDS_LOGIN_FATAL_ERROR_RESTART_AND_POWERWASH_BUTTON);
 }
 
 void SignInFatalErrorScreenHandler::Show(SignInFatalErrorScreen::Error error,
diff --git a/tools/metrics/histograms/metadata/oobe/enums.xml b/tools/metrics/histograms/metadata/oobe/enums.xml
index f5bcb27f..6d31ee3 100644
--- a/tools/metrics/histograms/metadata/oobe/enums.xml
+++ b/tools/metrics/histograms/metadata/oobe/enums.xml
@@ -181,6 +181,12 @@
   <int value="15" label="Unknown"/>
 </enum>
 
+<enum name="OobeNotCompletedErrorTrigger">
+  <int value="0" label="PerformOOBECompletedAction"/>
+  <int value="1" label="GaiaScreen"/>
+  <int value="2" label="ExistingUserController"/>
+</enum>
+
 <enum name="OobeWebViewLoadResult">
   <int value="0" label="Success"/>
   <int value="1" label="Load timeout"/>
diff --git a/tools/metrics/histograms/metadata/oobe/histograms.xml b/tools/metrics/histograms/metadata/oobe/histograms.xml
index 11027eb..0eb11285 100644
--- a/tools/metrics/histograms/metadata/oobe/histograms.xml
+++ b/tools/metrics/histograms/metadata/oobe/histograms.xml
@@ -1068,6 +1068,19 @@
   </summary>
 </histogram>
 
+<histogram name="OOBE.OobeNotCompletedErrorTrigger"
+    enum="OobeNotCompletedErrorTrigger" expires_after="2026-10-22">
+  <owner>osamafathy@google.com</owner>
+  <owner>cros-oobe@google.com</owner>
+  <summary>
+    Records the specific trigger cause when the &quot;OOBE Not Completed&quot;
+    error screen is displayed. This is emitted before showing the error screen.
+    The enum values capture the different locations in the code where the check
+    for OOBE completion was performed and determined to have failed, leading to
+    this error.
+  </summary>
+</histogram>
+
 <histogram name="OOBE.OobeStartToOnboardingStartTime" units="ms"
     expires_after="2026-04-01">
   <owner>osamafathy@google.com</owner>