// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/ui/webui/ash/lock_screen_reauth/lock_screen_reauth_handler.h"
#include <memory>
#include "ash/constants/ash_features.h"
#include "base/check_op.h"
#include "base/functional/bind.h"
#include "base/notreached.h"
#include "base/uuid.h"
#include "base/values.h"
#include "chrome/browser/ash/login/lock/online_reauth/lock_screen_reauth_manager.h"
#include "chrome/browser/ash/login/lock/online_reauth/lock_screen_reauth_manager_factory.h"
#include "chrome/browser/ash/login/login_pref_names.h"
#include "chrome/browser/ash/login/signin_partition_manager.h"
#include "chrome/browser/ash/login/ui/login_display_host_webui.h"
#include "chrome/browser/ash/policy/core/browser_policy_connector_ash.h"
#include "chrome/browser/ash/profiles/profile_helper.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/browser_process_platform_part.h"
#include "chrome/browser/enterprise/util/managed_browser_utils.h"
#include "chrome/browser/signin/identity_manager_factory.h"
#include "chrome/browser/ui/webui/ash/lock_screen_reauth/lock_screen_reauth_dialogs.h"
#include "chrome/browser/ui/webui/ash/login/check_passwords_against_cryptohome_helper.h"
#include "chrome/browser/ui/webui/ash/login/online_login_utils.h"
#include "chrome/common/chrome_features.h"
#include "chrome/common/pref_names.h"
#include "chrome/grit/generated_resources.h"
#include "chrome/installer/util/google_update_settings.h"
#include "chromeos/ash/components/login/auth/challenge_response/cert_utils.h"
#include "chromeos/ash/components/login/auth/public/auth_types.h"
#include "chromeos/ash/components/login/auth/public/challenge_response_key.h"
#include "chromeos/ash/components/login/auth/public/cryptohome_key_constants.h"
#include "chromeos/version/version_loader.h"
#include "components/account_id/account_id.h"
#include "components/user_manager/known_user.h"
#include "components/version_info/version_info.h"
#include "content/public/browser/storage_partition.h"
#include "google_apis/gaia/gaia_auth_util.h"
#include "google_apis/gaia/gaia_urls.h"
#include "net/base/net_errors.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/display/display.h"
#include "ui/display/screen.h"
namespace ash {
namespace {
bool ShouldDoSamlRedirect(const std::string& email) {
// TODO(b/335388700): If automatic re-authentication start is configured we
// have to skip any user verification notice page. For SAML this is currently
// only possible with redirect endpoint. Once reauth endpoint enables this,
// remove auto_start_reauth from this function.
const PrefService* prefs =
user_manager::UserManager::Get()->GetPrimaryUser()->GetProfilePrefs();
bool auto_start_reauth =
prefs && prefs->GetBoolean(::prefs::kLockScreenAutoStartOnlineReauth);
if (!auto_start_reauth) {
return false;
}
if (email.empty()) {
return false;
}
// If there's a populated email, we must check first that this user is using
// SAML in order to decide whether to show the interstitial page.
AccountId account_id =
user_manager::KnownUser(user_manager::UserManager::Get()->GetLocalState())
.GetAccountId(email, std::string() /* id */, AccountType::UNKNOWN);
const user_manager::User* user =
user_manager::UserManager::Get()->FindUser(account_id);
// TODO(b/259675128): we shouldn't rely on `user->using_saml()` when deciding
// which IdP page to show because this flag can be outdated. Admin could have
// changed the IdP to GAIA since last authentication and we wouldn't know
// about it.
return user && user->using_saml();
}
std::string GetSSOProfile() {
policy::BrowserPolicyConnectorAsh* connector =
g_browser_process->platform_part()->browser_policy_connector_ash();
return connector->GetSSOProfile();
}
std::string GetDeviceId(const user_manager::KnownUser& known_user) {
const user_manager::User* user =
user_manager::UserManager::Get()->GetPrimaryUser();
CHECK(user) << "Could not find an active user for lock screen";
std::string device_id = known_user.GetDeviceId(user->GetAccountId());
if (device_id.empty()) {
// TODO(http://b/311342008): Unify the error handling for missing device ids
// post login. We should ideally CHECK() here.
LOG(ERROR) << "Could not find a device id associated with this user";
return base::Uuid::GenerateRandomV4().AsLowercaseString();
}
return device_id;
}
const char kMainElement[] = "$(\'main-element\').";
} // namespace
LockScreenReauthHandler::LockScreenReauthHandler(const std::string& email)
: email_(email) {}
LockScreenReauthHandler::~LockScreenReauthHandler() = default;
void LockScreenReauthHandler::HandleInitialize(const base::Value::List& value) {
AllowJavascript();
OnReauthDialogReadyForTesting();
LoadAuthenticatorParam();
}
void LockScreenReauthHandler::HandleAuthenticatorLoaded(
const base::Value::List& value) {
VLOG(1) << "Authenticator finished loading";
authenticator_state_ = AuthenticatorState::LOADED;
if (waiting_caller_) {
std::move(waiting_caller_).Run();
}
// Recreate the client cert usage observer, in order to track only the certs
// used during the current sign-in attempt.
extension_provided_client_cert_usage_observer_ =
std::make_unique<LoginClientCertUsageObserver>();
}
void LockScreenReauthHandler::LoadAuthenticatorParam() {
if (authenticator_state_ == AuthenticatorState::LOADING) {
VLOG(1) << "Skip loading the Authenticator as it's already being loaded ";
return;
}
authenticator_state_ = AuthenticatorState::LOADING;
login::GaiaContext context;
context.email = email_;
context.gaia_id = user_manager::UserManager::Get()
->GetPrimaryUser()
->GetAccountId()
.GetGaiaId();
user_manager::KnownUser known_user(g_browser_process->local_state());
if (!context.email.empty()) {
context.gaps_cookie = known_user.GetGAPSCookie(
AccountId::FromUserEmail(gaia::CanonicalizeEmail(context.email)));
}
LoadGaia(context);
}
void LockScreenReauthHandler::LoadGaia(const login::GaiaContext& context) {
LOG_ASSERT(Profile::FromWebUI(web_ui()) ==
ProfileHelper::Get()->GetLockScreenProfile());
// Start a new session with SigninPartitionManager, generating a unique
// StoragePartition.
login::SigninPartitionManager* signin_partition_manager =
login::SigninPartitionManager::Factory::GetForBrowserContext(
Profile::FromWebUI(web_ui()));
// TODO(http://crbug/1348126): we should also close signin session after the
// flow is finished.
signin_partition_manager->StartSigninSession(
web_ui()->GetWebContents(),
base::BindOnce(&LockScreenReauthHandler::LoadGaiaWithPartition,
weak_factory_.GetWeakPtr(), context));
}
void LockScreenReauthHandler::LoadGaiaWithPartition(
const login::GaiaContext& context,
const std::string& partition_name) {
auto callback = base::BindOnce(
&LockScreenReauthHandler::OnSetCookieForLoadGaiaWithPartition,
weak_factory_.GetWeakPtr(), context, partition_name);
if (context.gaps_cookie.empty()) {
std::move(callback).Run(net::CookieAccessResult());
return;
}
// When the network service is enabled the webRequest API doesn't allow
// modification of the cookie header. So manually write the GAPS cookie into
// the CookieManager.
login::SigninPartitionManager* signin_partition_manager =
login::SigninPartitionManager::Factory::GetForBrowserContext(
Profile::FromWebUI(web_ui()));
login::SetCookieForPartition(context, signin_partition_manager,
std::move(callback));
}
void LockScreenReauthHandler::OnSetCookieForLoadGaiaWithPartition(
const login::GaiaContext& context,
const std::string& partition_name,
net::CookieAccessResult result) {
base::Value::Dict params;
params.Set("webviewPartitionName", partition_name);
signin_partition_name_ = partition_name;
const GaiaUrls& gaia_urls = *GaiaUrls::GetInstance();
params.Set("gaiaUrl", gaia_urls.gaia_url().spec());
params.Set("clientId", gaia_urls.oauth2_chrome_client_id());
bool do_saml_redirect = ShouldDoSamlRedirect(context.email);
params.Set("doSamlRedirect", do_saml_redirect);
// Path without the leading slash, as expected by authenticator.js.
const std::string default_gaia_path =
gaia_urls.embedded_setup_chromeos_url().path().substr(1);
params.Set("fallbackGaiaPath", default_gaia_path);
if (do_saml_redirect) {
params.Set("gaiaPath",
gaia_urls.saml_redirect_chromeos_url().path().substr(1));
} else if (!context.email.empty()) {
params.Set("gaiaPath",
gaia_urls.embedded_reauth_chromeos_url().path().substr(1));
} else {
params.Set("gaiaPath", default_gaia_path);
}
const std::string domain = enterprise_util::GetDomainFromEmail(context.email);
if (!domain.empty()) {
params.Set("enterpriseEnrollmentDomain", domain);
} else {
// TODO(b/332481266): add proper error handling.
LOG(ERROR) << "Couldn't get domain for account.";
}
params.Set("enableGaiaActionButtons", !do_saml_redirect);
const std::string sso_profile(GetSSOProfile());
if (!sso_profile.empty()) {
params.Set("ssoProfile", sso_profile);
}
const std::string app_locale = g_browser_process->GetApplicationLocale();
DCHECK(!app_locale.empty());
params.Set("hl", app_locale);
params.Set("email", context.email);
params.Set("gaiaId", context.gaia_id);
params.Set("extractSamlPasswordAttributes",
login::ExtractSamlPasswordAttributesEnabled());
params.Set("clientVersion", version_info::GetVersionNumber());
params.Set("readOnlyEmail", true);
PrefService* local_state = g_browser_process->local_state();
if (local_state->IsManagedPreference(
prefs::kUrlParameterToAutofillSAMLUsername)) {
params.Set(
"urlParameterToAutofillSAMLUsername",
local_state->GetString(prefs::kUrlParameterToAutofillSAMLUsername));
}
CallJavascript("loadAuthenticator", params);
if (features::IsNewLockScreenReauthLayoutEnabled()) {
UpdateOrientationAndWidth();
}
}
void LockScreenReauthHandler::UpdateOrientationAndWidth() {
gfx::Size display = display::Screen::GetScreen()->GetPrimaryDisplay().size();
bool is_horizontal = display.width() >= display.height();
CallJavascript("setOrientation", is_horizontal);
const LockScreenStartReauthDialog* lock_screen_online_reauth_dialog =
LockScreenStartReauthDialog::GetInstance();
int width = lock_screen_online_reauth_dialog->GetDialogWidth();
CallJavascript("setWidth", width);
}
void LockScreenReauthHandler::CallJavascript(const std::string& function,
base::ValueView params) {
CallJavascriptFunction(std::string(kMainElement) + function, params);
}
void LockScreenReauthHandler::HandleCompleteAuthentication(
const base::Value::List& params) {
CHECK_EQ(params.size(), 7u);
std::string gaia_id, email, password;
bool using_saml;
gaia_id = params[0].GetString();
email = params[1].GetString();
password = params[2].GetString();
auto scraped_saml_passwords =
::login::ConvertToStringList(params[3].GetList());
using_saml = params[4].GetBool();
const auto services = ::login::ConvertToStringList(params[5].GetList());
const auto& password_attributes = params[6].GetDict();
if (gaia::CanonicalizeEmail(email) != gaia::CanonicalizeEmail(email_)) {
// The authenticated user email doesn't match the current user's email.
CallJavascriptFunction(std::string(kMainElement) + "resetAuthenticator");
return;
}
DCHECK(!email.empty());
DCHECK(!gaia_id.empty());
// Retrieve ChallengeResponseKey from client certificates.
std::optional<ChallengeResponseKey> challenge_response_key;
if (using_saml && extension_provided_client_cert_usage_observer_ &&
extension_provided_client_cert_usage_observer_->ClientCertsWereUsed()) {
auto challenge_response_key_or_error = login::ExtractClientCertificates(
*extension_provided_client_cert_usage_observer_);
if (!challenge_response_key_or_error.has_value()) {
NOTREACHED_IN_MIGRATION();
return;
}
challenge_response_key = challenge_response_key_or_error.value();
}
// Build UserContext.
user_context_ = login::BuildUserContextForGaiaSignIn(
login::GetUsertypeFromServicesString(services),
AccountId::FromUserEmailGaiaId(gaia::CanonicalizeEmail(email), gaia_id),
using_saml, false /* using_saml_api */, password,
SamlPasswordAttributes::FromJs(password_attributes),
/*sync_trusted_vault_keys=*/std::nullopt, challenge_response_key);
// Create GaiaCookiesRetriever.
login::SigninPartitionManager* signin_partition_manager =
login::SigninPartitionManager::Factory::GetForBrowserContext(
Profile::FromWebUI(web_ui()));
gaia_cookie_retriever_ = std::make_unique<GaiaCookieRetriever>(
signin_partition_name_, signin_partition_manager,
base::BindOnce(&LockScreenReauthHandler::OnCookieWaitTimeout,
weak_factory_.GetWeakPtr()));
// Create the callback that will be invoked once cookies are received.
const bool needs_saml_confirm_password = password.empty();
GaiaCookieRetriever::OnCookieRetrievedCallback finish_auth_callback =
base::BindOnce(&LockScreenReauthHandler::FinishAuthentication,
weak_factory_.GetWeakPtr(), needs_saml_confirm_password,
std::move(scraped_saml_passwords),
std::move(user_context_));
// Request cookies and proceed with authentication.
gaia_cookie_retriever_->RetrieveCookies(std::move(finish_auth_callback));
}
void LockScreenReauthHandler::FinishAuthentication(
bool needs_saml_confirm_password,
::login::StringList scraped_saml_passwords,
std::unique_ptr<UserContext> user_context,
login::GaiaCookiesData gaia_cookies) {
gaia_cookies.TransferCookiesToUserContext(*user_context);
if (needs_saml_confirm_password) {
CHECK_NE(scraped_saml_passwords.size(), 1u);
SamlConfirmPassword(scraped_saml_passwords, std::move(user_context));
} else {
CheckCredentials(std::move(user_context));
}
}
void LockScreenReauthHandler::OnCookieWaitTimeout() {
NOTREACHED_IN_MIGRATION()
<< "Cookie has timed out while attempting to login in.";
LockScreenStartReauthDialog::Dismiss();
}
void LockScreenReauthHandler::OnReauthDialogReadyForTesting() {
LockScreenStartReauthDialog* lock_screen_online_reauth_dialog =
LockScreenStartReauthDialog::GetInstance();
lock_screen_online_reauth_dialog->OnReadyForTesting(); // IN-TEST
}
void LockScreenReauthHandler::CheckCredentials(
std::unique_ptr<UserContext> user_context) {
Profile* profile =
ProfileHelper::Get()->GetProfileByAccountId(user_context->GetAccountId());
if (!profile) {
LOG(ERROR) << "Invalid account id";
return;
}
auto password_changed_callback =
base::BindRepeating(&LockScreenReauthHandler::ShowPasswordChangedScreen,
weak_factory_.GetWeakPtr());
lock_screen_reauth_manager_ =
LockScreenReauthManagerFactory::GetForProfile(profile);
CHECK(lock_screen_reauth_manager_);
lock_screen_reauth_manager_->CheckCredentials(*user_context,
password_changed_callback);
}
void LockScreenReauthHandler::HandleUpdateUserPassword(
const base::Value::List& value) {
DCHECK(!value.empty());
std::string old_password = value[0].GetString();
lock_screen_reauth_manager_->UpdateUserPassword(old_password);
}
void LockScreenReauthHandler::ShowPasswordChangedScreen() {
CallJavascriptFunction(std::string(kMainElement) + "passwordChanged");
}
void LockScreenReauthHandler::ShowSamlConfirmPasswordScreen() {
CallJavascript("showSamlConfirmPassword",
static_cast<int>(scraped_saml_passwords_.size()));
}
void LockScreenReauthHandler::HandleOnPasswordTyped(
const base::Value::List& value) {
OnPasswordTyped(value[0].GetString());
}
void LockScreenReauthHandler::OnPasswordTyped(const std::string& password) {
if (scraped_saml_passwords_.empty() ||
base::Contains(scraped_saml_passwords_, password)) {
OnPasswordConfirmed(password);
return;
}
ShowSamlConfirmPasswordScreen();
}
void LockScreenReauthHandler::OnPasswordConfirmed(const std::string& password) {
Key key(password);
key.SetLabel(kCryptohomeGaiaKeyLabel);
user_context_->SetKey(key);
user_context_->SetPasswordKey(Key(password));
user_context_->SetSamlPassword(SamlPassword{password});
CheckCredentials(std::move(user_context_));
user_context_.reset();
scraped_saml_passwords_.clear();
}
void LockScreenReauthHandler::SamlConfirmPassword(
::login::StringList scraped_saml_passwords,
std::unique_ptr<UserContext> user_context) {
scraped_saml_passwords_ = scraped_saml_passwords;
user_context_ = std::move(user_context);
if (!features::IsCheckPasswordsAgainstCryptohomeHelperEnabled() ||
scraped_saml_passwords_.empty()) {
ShowSamlConfirmPasswordScreen();
return;
}
// TODO(crbug.com/40214270) Eliminate redundant cryptohome check.
check_passwords_against_cryptohome_helper_ =
std::make_unique<CheckPasswordsAgainstCryptohomeHelper>(
*user_context_.get(), scraped_saml_passwords_,
base::BindOnce(
&LockScreenReauthHandler::ShowSamlConfirmPasswordScreen,
weak_factory_.GetWeakPtr()),
base::BindOnce(&LockScreenReauthHandler::OnPasswordConfirmed,
weak_factory_.GetWeakPtr()));
}
void LockScreenReauthHandler::HandleWebviewLoadAborted(int error_code) {
if (error_code == net::ERR_BLOCKED_BY_ADMINISTRATOR) {
// Ignore this error to let the user see the error screen for blocked sites.
return;
}
if (error_code == net::ERR_INVALID_AUTH_CREDENTIALS) {
// Silently ignore this error - it is used as an intermediate state for
// committed interstitials (see https://crbug.com/1049349 for details).
return;
}
if (error_code == net::ERR_ABORTED) {
LOG(WARNING) << "Ignoring Gaia webview error: "
<< net::ErrorToShortString(error_code);
return;
}
LOG(ERROR) << "Gaia webview error: " << net::ErrorToShortString(error_code);
LockScreenStartReauthDialog* lock_screen_online_reauth_dialog =
LockScreenStartReauthDialog::GetInstance();
lock_screen_online_reauth_dialog->OnWebviewLoadAborted();
}
void LockScreenReauthHandler::HandleGetDeviceId(
const std::string& callback_id) {
if (!IsJavascriptAllowed()) {
return;
}
user_manager::KnownUser known_user{g_browser_process->local_state()};
ResolveJavascriptCallback(callback_id, GetDeviceId(known_user));
}
void LockScreenReauthHandler::ReloadGaia() {
CallJavascriptFunction(std::string(kMainElement) + "reloadAuthenticator");
}
void LockScreenReauthHandler::RegisterMessages() {
web_ui()->RegisterMessageCallback(
"initialize",
base::BindRepeating(&LockScreenReauthHandler::HandleInitialize,
weak_factory_.GetWeakPtr()));
web_ui()->RegisterMessageCallback(
"authenticatorLoaded",
base::BindRepeating(&LockScreenReauthHandler::HandleAuthenticatorLoaded,
weak_factory_.GetWeakPtr()));
web_ui()->RegisterMessageCallback(
"completeAuthentication",
base::BindRepeating(
&LockScreenReauthHandler::HandleCompleteAuthentication,
weak_factory_.GetWeakPtr()));
web_ui()->RegisterMessageCallback(
"updateUserPassword",
base::BindRepeating(&LockScreenReauthHandler::HandleUpdateUserPassword,
weak_factory_.GetWeakPtr()));
web_ui()->RegisterMessageCallback(
"onPasswordTyped",
base::BindRepeating(&LockScreenReauthHandler::HandleOnPasswordTyped,
weak_factory_.GetWeakPtr()));
web_ui()->RegisterHandlerCallback(
"webviewLoadAborted",
base::BindRepeating(&LockScreenReauthHandler::HandleWebviewLoadAborted,
weak_factory_.GetWeakPtr()));
web_ui()->RegisterHandlerCallback(
"getDeviceId",
base::BindRepeating(&LockScreenReauthHandler::HandleGetDeviceId,
weak_factory_.GetWeakPtr()));
}
bool LockScreenReauthHandler::IsAuthenticatorLoaded(
base::OnceClosure callback) {
if (authenticator_state_ == AuthenticatorState::LOADED) {
return true;
}
waiting_caller_ = std::move(callback);
return false;
}
} // namespace ash