| // Copyright 2018 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. |
| |
| // Implementation of CGaiaCredentialBase class, used as the base for all |
| // credentials that need to show the gaia sign in page. |
| |
| #include "chrome/credential_provider/gaiacp/gaia_credential_base.h" |
| |
| #include <ntstatus.h> |
| |
| #include <algorithm> |
| #include <memory> |
| #include <utility> |
| |
| #include "base/bind.h" |
| #include "base/callback_helpers.h" |
| #include "base/command_line.h" |
| #include "base/files/file_path.h" |
| #include "base/files/file_util.h" |
| #include "base/json/json_reader.h" |
| #include "base/json/json_writer.h" |
| #include "base/path_service.h" |
| #include "base/stl_util.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/string_split.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/values.h" |
| #include "base/win/current_module.h" |
| #include "base/win/registry.h" |
| #include "base/win/scoped_com_initializer.h" |
| #include "base/win/scoped_handle.h" |
| #include "chrome/credential_provider/common/gcp_strings.h" |
| #include "chrome/credential_provider/gaiacp/associated_user_validator.h" |
| #include "chrome/credential_provider/gaiacp/auth_utils.h" |
| #include "chrome/credential_provider/gaiacp/gaia_credential_provider.h" |
| #include "chrome/credential_provider/gaiacp/gaia_credential_provider_i.h" |
| #include "chrome/credential_provider/gaiacp/gaia_resources.h" |
| #include "chrome/credential_provider/gaiacp/gcp_utils.h" |
| #include "chrome/credential_provider/gaiacp/grit/gaia_static_resources.h" |
| #include "chrome/credential_provider/gaiacp/internet_availability_checker.h" |
| #include "chrome/credential_provider/gaiacp/logging.h" |
| #include "chrome/credential_provider/gaiacp/mdm_utils.h" |
| #include "chrome/credential_provider/gaiacp/os_process_manager.h" |
| #include "chrome/credential_provider/gaiacp/os_user_manager.h" |
| #include "chrome/credential_provider/gaiacp/password_recovery_manager.h" |
| #include "chrome/credential_provider/gaiacp/reg_utils.h" |
| #include "chrome/credential_provider/gaiacp/scoped_lsa_policy.h" |
| #include "chrome/credential_provider/gaiacp/scoped_user_profile.h" |
| #include "chrome/credential_provider/gaiacp/win_http_url_fetcher.h" |
| #include "chrome/installer/launcher_support/chrome_launcher_support.h" |
| #include "content/public/common/content_switches.h" |
| #include "google_apis/gaia/gaia_auth_util.h" |
| #include "google_apis/gaia/gaia_switches.h" |
| #include "google_apis/gaia/gaia_urls.h" |
| #include "net/base/escape.h" |
| #include "net/base/registry_controlled_domains/registry_controlled_domain.h" |
| |
| namespace credential_provider { |
| |
| namespace { |
| |
| constexpr wchar_t kEmailDomainsKey[] = L"ed"; |
| constexpr char kGetAccessTokenBodyWithScopeFormat[] = |
| "client_id=%s&" |
| "client_secret=%s&" |
| "grant_type=refresh_token&" |
| "refresh_token=%s&" |
| "scope=%s"; |
| constexpr wchar_t kRegEnableADAssociation[] = L"enable_ad_association"; |
| // The access scopes should be separated by single space. |
| constexpr char kAccessScopes[] = |
| "https://www.googleapis.com/auth/admin.directory.user"; |
| constexpr int kHttpTimeout = 3000; // in milliseconds |
| |
| // Names of keys used to fetch the custom attributes from google admin sdk |
| // users directory api. |
| constexpr char kKeyCustomSchemas[] = "customSchemas"; |
| constexpr char kKeyEmployeeData[] = "employeeData"; |
| constexpr char kKeyAdUpn[] = "ad_upn"; |
| |
| base::string16 GetEmailDomains() { |
| std::vector<wchar_t> email_domains(16); |
| ULONG length = email_domains.size(); |
| HRESULT hr = GetGlobalFlag(kEmailDomainsKey, &email_domains[0], &length); |
| if (FAILED(hr)) { |
| if (hr == HRESULT_FROM_WIN32(ERROR_MORE_DATA)) { |
| email_domains.resize(length + 1); |
| length = email_domains.size(); |
| hr = GetGlobalFlag(kEmailDomainsKey, &email_domains[0], &length); |
| if (FAILED(hr)) |
| email_domains[0] = 0; |
| } |
| } |
| return base::string16(&email_domains[0]); |
| } |
| |
| // Use WinHttpUrlFetcher to communicate with the admin sdk and fetch the active |
| // directory UPN from the admin configured custom attributes. |
| HRESULT GetAdUpnFromCloudDirectory(const base::string16& email, |
| const std::string& access_token, |
| std::string* ad_upn, |
| BSTR* error_text) { |
| DCHECK(email.size() > 0); |
| DCHECK(access_token.size() > 0); |
| DCHECK(ad_upn); |
| DCHECK(error_text); |
| *error_text = nullptr; |
| |
| std::string escape_url_encoded_email = |
| net::EscapeUrlEncodedData(base::UTF16ToUTF8(email), true); |
| std::string get_cd_user_url = base::StringPrintf( |
| "https://www.googleapis.com/admin/directory/v1/users/" |
| "%s?projection=full&viewType=domain_public", |
| escape_url_encoded_email.c_str()); |
| LOGFN(INFO) << "Encoded URL : " << get_cd_user_url; |
| auto fetcher = WinHttpUrlFetcher::Create(GURL(get_cd_user_url)); |
| fetcher->SetRequestHeader("Accept", "application/json"); |
| fetcher->SetHttpRequestTimeout(kHttpTimeout); |
| |
| std::string access_token_header = |
| base::StringPrintf("Bearer %s", access_token.c_str()); |
| fetcher->SetRequestHeader("Authorization", access_token_header.c_str()); |
| std::vector<char> cd_user_response; |
| HRESULT hr = fetcher->Fetch(&cd_user_response); |
| std::string cd_user_response_json_string = |
| std::string(cd_user_response.begin(), cd_user_response.end()); |
| if (FAILED(hr)) { |
| LOGFN(INFO) << "fetcher->Fetch hr=" << putHR(hr); |
| *error_text = |
| CGaiaCredentialBase::AllocErrorString(IDS_INTERNAL_ERROR_BASE); |
| return hr; |
| } |
| |
| *ad_upn = SearchForKeyInStringDictUTF8( |
| cd_user_response_json_string, |
| {kKeyCustomSchemas, kKeyEmployeeData, kKeyAdUpn}); |
| return S_OK; |
| } |
| |
| // Request a downscoped access token using the refresh token provided in the |
| // input. |
| HRESULT RequestDownscopedAccessToken(const std::string& refresh_token, |
| std::string* access_token, |
| BSTR* error_text) { |
| DCHECK(refresh_token.size() > 0); |
| DCHECK(access_token); |
| DCHECK(error_text); |
| *error_text = nullptr; |
| |
| GaiaUrls* gaia_urls = GaiaUrls::GetInstance(); |
| std::string enc_client_id = |
| net::EscapeUrlEncodedData(gaia_urls->oauth2_chrome_client_id(), true); |
| std::string enc_client_secret = |
| net::EscapeUrlEncodedData(gaia_urls->oauth2_chrome_client_secret(), true); |
| std::string enc_refresh_token = |
| net::EscapeUrlEncodedData(refresh_token, true); |
| std::string get_access_token_body = base::StringPrintf( |
| kGetAccessTokenBodyWithScopeFormat, enc_client_id.c_str(), |
| enc_client_secret.c_str(), enc_refresh_token.c_str(), |
| net::EscapeUrlEncodedData(kAccessScopes, true).c_str()); |
| std::string get_oauth_token_url = |
| base::StringPrintf("%s", gaia_urls->oauth2_token_url().spec().c_str()); |
| |
| auto oauth_fetcher = WinHttpUrlFetcher::Create(GURL(get_oauth_token_url)); |
| oauth_fetcher->SetRequestBody(get_access_token_body.c_str()); |
| oauth_fetcher->SetRequestHeader("content-type", |
| "application/x-www-form-urlencoded"); |
| oauth_fetcher->SetHttpRequestTimeout(kHttpTimeout); |
| |
| std::vector<char> oauth_response; |
| HRESULT oauth_hr = oauth_fetcher->Fetch(&oauth_response); |
| if (FAILED(oauth_hr)) { |
| LOGFN(ERROR) << "oauth_fetcher.Fetch hr=" << putHR(oauth_hr); |
| *error_text = |
| CGaiaCredentialBase::AllocErrorString(IDS_INTERNAL_ERROR_BASE); |
| return oauth_hr; |
| } |
| |
| std::string oauth_response_json_string = |
| std::string(oauth_response.begin(), oauth_response.end()); |
| *access_token = SearchForKeyInStringDictUTF8(oauth_response_json_string, |
| {kKeyAccessToken}); |
| if (access_token->empty()) { |
| LOGFN(ERROR) << "Fetched access token with new scopes is empty."; |
| *error_text = |
| CGaiaCredentialBase::AllocErrorString(IDS_EMPTY_ACCESS_TOKEN_BASE); |
| return E_FAIL; |
| } |
| return S_OK; |
| } |
| |
| // Find an AD account associated with GCPW user if one exists. |
| // (1) Verifies if the gaia user has a corresponding mapping in Google |
| // Admin SDK Users Directory and contains the custom_schema that contains |
| // the ad_upn or local_user_name for the corresponding user. |
| // (2) If there is an entry in cloud directory, gcpw would search for the SID |
| // corresponding to that user entry on the device. |
| // (3) If a SID is found, then it would log the user onto the device using |
| // username extracted from Google Admin SDK Users Directory and password |
| // being the same as the gaia entity. |
| // (4) If there is no entry found in cloud directory, gcpw would fallback to |
| // attempting creation of a new user on the device. |
| // |
| // Below are the failure scenarios : |
| // (1) If an invalid upn is set in the custom attributes, the login would fail. |
| // (2) If an attempt to find SID from domain controller failed, then we fail |
| // the login. |
| // Note that if an empty upn is found in the custom attribute, then the login |
| // would try and attempt to create local user. |
| HRESULT FindAdUserSidIfAvailable(const std::string& refresh_token, |
| const base::string16& email, |
| wchar_t* sid, |
| const DWORD sid_length, |
| BSTR* error_text) { |
| DCHECK(sid); |
| DCHECK(error_text); |
| *error_text = nullptr; |
| |
| // Step 1: Get the downscoped access token with required admin sdk scopes. |
| std::string access_token; |
| HRESULT hr = |
| RequestDownscopedAccessToken(refresh_token, &access_token, error_text); |
| |
| if (FAILED(hr)) { |
| LOGFN(ERROR) << "RequestDownscopedAccessToken hr=" << putHR(hr); |
| return hr; |
| } |
| |
| // Step 2: Make a get call to admin sdk using the fetched access_token and |
| // retrieve the ad_upn. |
| std::string ad_upn; |
| hr = GetAdUpnFromCloudDirectory(email, access_token, &ad_upn, error_text); |
| if (FAILED(hr)) { |
| LOGFN(ERROR) << "GetAdUpnFromCloudDirectory hr=" << putHR(hr); |
| return hr; |
| } |
| |
| base::string16 ad_domain; |
| base::string16 ad_user; |
| if (ad_upn.empty()) { |
| LOGFN(INFO) << "Found empty ad_upn in cloud directory. Fall back to " |
| "creating local account"; |
| return S_FALSE; |
| } |
| |
| // The format for ad_upn custom attribute is domainName/userName. |
| const base::char16 kSlashDelimiter[] = STRING16_LITERAL("/"); |
| std::vector<base::string16> tokens = |
| base::SplitString(base::UTF8ToUTF16(ad_upn), kSlashDelimiter, |
| base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY); |
| |
| // Values fetched from custom attribute shouldn't be empty. |
| if (tokens.size() != 2) { |
| LOGFN(ERROR) << "Found unparseable ad_upn in cloud directory : " << ad_upn; |
| *error_text = |
| CGaiaCredentialBase::AllocErrorString(IDS_INVALID_AD_UPN_BASE); |
| return E_FAIL; |
| } |
| |
| ad_domain = tokens.at(0); |
| ad_user = tokens.at(1); |
| |
| OSUserManager* os_user_manager = OSUserManager::Get(); |
| DCHECK(os_user_manager); |
| base::string16 existing_sid = base::string16(); |
| |
| LOGFN(INFO) << "Get user sid for user " << ad_user << " and domain name " |
| << ad_domain; |
| hr = os_user_manager->GetUserSID(ad_domain.c_str(), ad_user.c_str(), |
| &existing_sid); |
| LOGFN(INFO) << "GetUserSID result=" << hr; |
| |
| if (existing_sid.length() > 0) { |
| LOGFN(INFO) << "Found existing SID = " << existing_sid; |
| wcscpy_s(sid, sid_length, existing_sid.c_str()); |
| return S_OK; |
| } else { |
| LOGFN(ERROR) << "No existing sid found with UPN : " << ad_upn; |
| *error_text = |
| CGaiaCredentialBase::AllocErrorString(IDS_INVALID_AD_UPN_BASE); |
| return E_FAIL; |
| } |
| } |
| |
| // Tries to find a user associated to the gaia_id stored in |result| under the |
| // key |kKeyId|. If one exists, then this function will fill out |gaia_id|, |
| // |username|, |domain| and |sid| with the user's information. If not this |
| // function will try to generate a new username derived from the email and fill |
| // out only |gaia_id| and |username|. |domain| will always be the local domain |
| // since only local users can be created. |sid| will be empty until the user is |
| // created later on. |is_consumer_account| will be set to true if the email used |
| // to sign in is gmail or googlemail. |
| HRESULT MakeUsernameForAccount(const base::Value& result, |
| base::string16* gaia_id, |
| wchar_t* username, |
| DWORD username_length, |
| wchar_t* domain, |
| DWORD domain_length, |
| wchar_t* sid, |
| DWORD sid_length, |
| bool* is_consumer_account, |
| BSTR* error_text) { |
| DCHECK(gaia_id); |
| DCHECK(username); |
| DCHECK(domain); |
| DCHECK(sid); |
| DCHECK(is_consumer_account); |
| DCHECK(error_text); |
| |
| // Determine if the email is a consumer domain (gmail.com or googlemail.com). |
| base::string16 email = GetDictString(result, kKeyEmail); |
| std::transform(email.begin(), email.end(), email.begin(), ::tolower); |
| base::string16::size_type consumer_domain_pos = email.find(L"@gmail.com"); |
| if (consumer_domain_pos == base::string16::npos) |
| consumer_domain_pos = email.find(L"@googlemail.com"); |
| |
| *is_consumer_account = consumer_domain_pos != base::string16::npos; |
| |
| *gaia_id = GetDictString(result, kKeyId); |
| |
| // First try to detect if this gaia account has been used to create an OS |
| // user already. If so, return the OS username of that user. |
| HRESULT hr = GetSidFromId(*gaia_id, sid, sid_length); |
| |
| bool has_existing_user_sid = false; |
| // Check if the machine is domain joined and get the domain name if domain |
| // joined. |
| if (SUCCEEDED(hr)) { |
| // This makes sure that we don't invoke the network calls on every login |
| // attempt and instead fallback to the SID to gaia id mapping created by |
| // GCPW. |
| LOGFN(INFO) << "Found existing SID created in GCPW registry entry = " |
| << sid; |
| has_existing_user_sid = true; |
| } else if (CGaiaCredentialBase::IsAdToGoogleAssociationEnabled() && |
| OSUserManager::Get()->IsDeviceDomainJoined()) { |
| LOGFN(INFO) << "No existing SID found in the GCPW registry."; |
| |
| std::string refresh_token = GetDictStringUTF8(result, kKeyRefreshToken); |
| hr = FindAdUserSidIfAvailable(refresh_token, email, sid, sid_length, |
| error_text); |
| if (FAILED(hr)) { |
| LOGFN(ERROR) << "Failed finding AD user sid for GCPW user. hr=" |
| << putHR(hr); |
| return hr; |
| } else if (hr == S_OK) { |
| has_existing_user_sid = true; |
| } |
| } else { |
| LOGFN(INFO) << "Falling back to creation of new user"; |
| } |
| |
| if (has_existing_user_sid) { |
| HRESULT hr = OSUserManager::Get()->FindUserBySID( |
| sid, username, username_length, domain, domain_length); |
| if (SUCCEEDED(hr)) |
| return hr; |
| } |
| |
| LOGFN(INFO) << "No existing user found associated to gaia id:" << *gaia_id; |
| wcscpy_s(domain, domain_length, OSUserManager::GetLocalDomain().c_str()); |
| username[0] = 0; |
| sid[0] = 0; |
| |
| // Create a username based on the email address. Usernames are more |
| // restrictive than emails, so some transformations are needed. This tries |
| // to preserve the email as much as possible in the username while respecting |
| // Windows username rules. See remarks in |
| // https://docs.microsoft.com/en-us/windows/desktop/api/lmaccess/ns-lmaccess-_user_info_0 |
| base::string16 os_username = email; |
| |
| // If the email is a consumer domain, strip it. |
| if (consumer_domain_pos != base::string16::npos) { |
| os_username.resize(consumer_domain_pos); |
| } else { |
| // Strip off well known TLDs. |
| std::string username_utf8 = |
| gaia::SanitizeEmail(base::UTF16ToUTF8(os_username)); |
| |
| size_t tld_length = |
| net::registry_controlled_domains::GetCanonicalHostRegistryLength( |
| gaia::ExtractDomainName(username_utf8), |
| net::registry_controlled_domains::EXCLUDE_UNKNOWN_REGISTRIES, |
| net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES); |
| |
| // If an TLD is found strip it off, plus 1 to remove the separating dot too. |
| if (tld_length > 0) { |
| username_utf8.resize(username_utf8.length() - tld_length - 1); |
| os_username = base::UTF8ToUTF16(username_utf8); |
| } |
| } |
| |
| // If the username is longer than 20 characters, truncate. |
| if (os_username.size() > kWindowsUsernameBufferLength - 1) |
| os_username.resize(kWindowsUsernameBufferLength - 1); |
| |
| // Replace invalid characters. While @ is not strictly invalid according to |
| // MSDN docs, it causes trouble. |
| for (auto& c : os_username) { |
| if (wcschr(L"@\\[]:|<>+=;?*", c) != nullptr || c < 32) |
| c = L'_'; |
| } |
| |
| wcscpy_s(username, username_length, os_username.c_str()); |
| |
| return S_OK; |
| } |
| |
| // Waits for the login UI to complete and returns the result of the operation. |
| // This function returns S_OK on success, E_UNEXPECTED on failure, and E_ABORT |
| // if the user aborted or timed out (or was killed during cleanup). |
| HRESULT WaitForLoginUIAndGetResult( |
| CGaiaCredentialBase::UIProcessInfo* uiprocinfo, |
| std::string* json_result, |
| DWORD* exit_code, |
| BSTR* status_text) { |
| LOGFN(INFO); |
| DCHECK(uiprocinfo); |
| DCHECK(json_result); |
| DCHECK(exit_code); |
| DCHECK(status_text); |
| |
| // Buffer used to accumulate output from UI. |
| const int kBufferSize = 4096; |
| std::vector<char> output_buffer(kBufferSize, '\0'); |
| base::ScopedClosureRunner zero_buffer_on_exit( |
| base::BindOnce(base::IgnoreResult(&SecurelyClearBuffer), |
| &output_buffer[0], kBufferSize)); |
| |
| HRESULT hr = WaitForProcess(uiprocinfo->procinfo.process_handle(), |
| uiprocinfo->parent_handles, exit_code, |
| &output_buffer[0], kBufferSize); |
| // output_buffer contains sensitive information like the password. Don't log |
| // it. |
| LOGFN(INFO) << "exit_code=" << *exit_code; |
| |
| // Killed internally in the GLS or killed externally by selecting |
| // another credential while GLS is running. |
| if (*exit_code == kUiecAbort || *exit_code == kUiecKilled) { |
| LOGFN(ERROR) << "Aborted hr=" << putHR(hr); |
| return E_ABORT; |
| } else if (*exit_code != kUiecSuccess) { |
| LOGFN(ERROR) << "Error hr=" << putHR(hr); |
| *status_text = |
| CGaiaCredentialBase::AllocErrorString(IDS_INVALID_UI_RESPONSE_BASE); |
| return E_FAIL; |
| } |
| |
| *json_result = std::string(&output_buffer[0]); |
| return S_OK; |
| } |
| |
| // This function validates the response from GLS and makes sure it contained |
| // all the fields required to proceed with logon. This does not necessarily |
| // guarantee that the logon will succeed, only that GLS response seems correct. |
| HRESULT ValidateResult(const base::Value& result, BSTR* status_text) { |
| DCHECK(status_text); |
| |
| // Check the exit_code to see if any errors were detected by the GLS. |
| base::Optional<int> exit_code = result.FindIntKey(kKeyExitCode); |
| if (exit_code.value() != kUiecSuccess) { |
| switch (exit_code.value()) { |
| case kUiecAbort: |
| // This case represents a user abort and no error message is shown. |
| return E_ABORT; |
| case kUiecTimeout: |
| case kUiecKilled: |
| NOTREACHED() << "Internal codes, not returned by GLS"; |
| break; |
| case kUiecEMailMissmatch: |
| *status_text = |
| CGaiaCredentialBase::AllocErrorString(IDS_EMAIL_MISMATCH_BASE); |
| break; |
| case kUiecInvalidEmailDomain: |
| *status_text = CGaiaCredentialBase::AllocErrorString( |
| IDS_INVALID_EMAIL_DOMAIN_BASE); |
| break; |
| case kUiecMissingSigninData: |
| *status_text = |
| CGaiaCredentialBase::AllocErrorString(IDS_INVALID_UI_RESPONSE_BASE); |
| break; |
| } |
| return E_FAIL; |
| } |
| |
| // Check that the webui returned all expected values. |
| |
| bool has_error = false; |
| std::string email = GetDictStringUTF8(result, kKeyEmail); |
| if (email.empty()) { |
| LOGFN(ERROR) << "Email is empty"; |
| has_error = true; |
| } |
| |
| std::string fullname = GetDictStringUTF8(result, kKeyFullname); |
| if (fullname.empty()) { |
| LOGFN(ERROR) << "Full name is empty"; |
| has_error = true; |
| } |
| |
| std::string id = GetDictStringUTF8(result, kKeyId); |
| if (id.empty()) { |
| LOGFN(ERROR) << "Id is empty"; |
| has_error = true; |
| } |
| |
| std::string mdm_id_token = GetDictStringUTF8(result, kKeyMdmIdToken); |
| if (mdm_id_token.empty()) { |
| LOGFN(ERROR) << "mdm id token is empty"; |
| has_error = true; |
| } |
| |
| std::string access_token = GetDictStringUTF8(result, kKeyAccessToken); |
| if (access_token.empty()) { |
| LOGFN(ERROR) << "access token is empty"; |
| has_error = true; |
| } |
| |
| std::string password = GetDictStringUTF8(result, kKeyPassword); |
| if (password.empty()) { |
| LOGFN(ERROR) << "Password is empty"; |
| has_error = true; |
| } else { |
| SecurelyClearString(password); |
| } |
| |
| std::string refresh_token = GetDictStringUTF8(result, kKeyRefreshToken); |
| if (refresh_token.empty()) { |
| LOGFN(ERROR) << "refresh token is empty"; |
| has_error = true; |
| } |
| |
| std::string token_handle = GetDictStringUTF8(result, kKeyTokenHandle); |
| if (token_handle.empty()) { |
| LOGFN(ERROR) << "Token handle is empty"; |
| has_error = true; |
| } |
| |
| if (has_error) { |
| *status_text = |
| CGaiaCredentialBase::AllocErrorString(IDS_INVALID_UI_RESPONSE_BASE); |
| return E_UNEXPECTED; |
| } |
| |
| return S_OK; |
| } |
| |
| // Creates a new windows OS user with the given |base_username|, |fullname| and |
| // |password| on the local machine. Returns the SID of the new user. |
| // If a user with |base_username| already exists, the function will try to |
| // generate a new indexed username up to |max_attempts| before failing. |
| // The actual username used for the new user will be filled in |final_username| |
| // if successful. |
| HRESULT CreateNewUser(OSUserManager* manager, |
| const wchar_t* base_username, |
| const wchar_t* password, |
| const wchar_t* fullname, |
| const wchar_t* comment, |
| bool add_to_users_group, |
| int max_attempts, |
| BSTR* final_username, |
| BSTR* sid) { |
| DCHECK(manager); |
| DCHECK(base_username); |
| DCHECK(password); |
| DCHECK(fullname); |
| DCHECK(comment); |
| DCHECK(final_username); |
| DCHECK(sid); |
| wchar_t new_username[kWindowsUsernameBufferLength]; |
| errno_t err = wcscpy_s(new_username, base::size(new_username), base_username); |
| if (err != 0) { |
| LOGFN(ERROR) << "wcscpy_s errno=" << err; |
| return E_FAIL; |
| } |
| |
| // Keep trying to create the user account until an unused username can be |
| // found or |max_attempts| has been reached. |
| for (int i = 0; i < max_attempts; ++i) { |
| CComBSTR new_sid; |
| DWORD error; |
| HRESULT hr = manager->AddUser(new_username, password, fullname, comment, |
| add_to_users_group, &new_sid, &error); |
| if (hr == HRESULT_FROM_WIN32(NERR_UserExists)) { |
| base::string16 next_username = base_username; |
| base::string16 next_username_suffix = |
| base::NumberToString16(i + kInitialDuplicateUsernameIndex); |
| // Create a new user name that fits in |kWindowsUsernameBufferLength| |
| if (next_username.size() + next_username_suffix.size() > |
| (kWindowsUsernameBufferLength - 1)) { |
| next_username = |
| next_username.substr(0, (kWindowsUsernameBufferLength - 1) - |
| next_username_suffix.size()) + |
| next_username_suffix; |
| } else { |
| next_username += next_username_suffix; |
| } |
| LOGFN(INFO) << "Username '" << new_username |
| << "' already exists. Trying '" << next_username << "'"; |
| |
| errno_t err = wcscpy_s(new_username, base::size(new_username), |
| next_username.c_str()); |
| if (err != 0) { |
| LOGFN(ERROR) << "wcscpy_s errno=" << err; |
| return E_FAIL; |
| } |
| |
| continue; |
| } else if (FAILED(hr)) { |
| LOGFN(ERROR) << "manager->AddUser hr=" << putHR(hr); |
| return hr; |
| } |
| |
| *sid = ::SysAllocString(new_sid); |
| *final_username = ::SysAllocString(new_username); |
| return S_OK; |
| } |
| |
| return HRESULT_FROM_WIN32(NERR_UserExists); |
| } |
| |
| } // namespace |
| |
| CGaiaCredentialBase::UIProcessInfo::UIProcessInfo() {} |
| |
| CGaiaCredentialBase::UIProcessInfo::~UIProcessInfo() {} |
| |
| // static |
| bool CGaiaCredentialBase::IsAdToGoogleAssociationEnabled() { |
| DWORD enable_ad_association = 0; |
| return GetGlobalFlagOrDefault(kRegEnableADAssociation, enable_ad_association); |
| } |
| |
| // static |
| HRESULT CGaiaCredentialBase::OnDllRegisterServer() { |
| OSUserManager* manager = OSUserManager::Get(); |
| |
| auto policy = ScopedLsaPolicy::Create(POLICY_ALL_ACCESS); |
| |
| if (!policy) { |
| HRESULT hr = HRESULT_FROM_WIN32(::GetLastError()); |
| LOGFN(ERROR) << "ScopedLsaPolicy::Create hr=" << putHR(hr); |
| return hr; |
| } |
| |
| PSID sid = nullptr; |
| |
| // Try to get existing username and password and then log on the user, if any |
| // step fails, assume that a new user needs to be created. |
| wchar_t gaia_username[kWindowsUsernameBufferLength]; |
| HRESULT hr = policy->RetrievePrivateData(kLsaKeyGaiaUsername, gaia_username, |
| base::size(gaia_username)); |
| |
| if (SUCCEEDED(hr)) { |
| LOGFN(INFO) << "Expecting gaia user '" << gaia_username << "' to exist."; |
| wchar_t password[32]; |
| HRESULT hr = policy->RetrievePrivateData(kLsaKeyGaiaPassword, password, |
| base::size(password)); |
| if (SUCCEEDED(hr)) { |
| base::string16 local_domain = OSUserManager::GetLocalDomain(); |
| base::win::ScopedHandle token; |
| hr = OSUserManager::Get()->CreateLogonToken( |
| local_domain.c_str(), gaia_username, password, |
| /*interactive=*/false, &token); |
| if (SUCCEEDED(hr)) { |
| hr = OSUserManager::Get()->GetUserSID(local_domain.c_str(), |
| gaia_username, &sid); |
| if (FAILED(hr)) { |
| LOGFN(ERROR) << "GetUserSID(sid from existing user '" << gaia_username |
| << "') hr=" << putHR(hr); |
| sid = nullptr; |
| } |
| } |
| } |
| } |
| |
| if (sid == nullptr) { |
| // No valid existing user found, reset to default name and start generating |
| // from there. |
| errno_t err = wcscpy_s(gaia_username, base::size(gaia_username), |
| kDefaultGaiaAccountName); |
| if (err != 0) { |
| LOGFN(ERROR) << "wcscpy_s errno=" << err; |
| return E_FAIL; |
| } |
| |
| // Generate a random password for the new gaia account. |
| wchar_t password[32]; |
| HRESULT hr = |
| manager->GenerateRandomPassword(password, base::size(password)); |
| if (FAILED(hr)) { |
| LOGFN(ERROR) << "GenerateRandomPassword hr=" << putHR(hr); |
| return hr; |
| } |
| |
| CComBSTR sid_string; |
| CComBSTR gaia_username; |
| // Keep trying to create the special Gaia account used to run the UI until |
| // an unused username can be found or kMaxUsernameAttempts has been reached. |
| hr = |
| CreateNewUser(manager, kDefaultGaiaAccountName, password, |
| GetStringResource(IDS_GAIA_ACCOUNT_FULLNAME_BASE).c_str(), |
| GetStringResource(IDS_GAIA_ACCOUNT_COMMENT_BASE).c_str(), |
| /*add_to_users_group=*/false, kMaxUsernameAttempts, |
| &gaia_username, &sid_string); |
| |
| if (FAILED(hr)) { |
| LOGFN(ERROR) << "CreateNewUser hr=" << putHR(hr); |
| return hr; |
| } |
| |
| if (!::ConvertStringSidToSid(sid_string, &sid)) { |
| hr = HRESULT_FROM_WIN32(::GetLastError()); |
| LOGFN(ERROR) << "ConvertStringSidToSid hr=" << putHR(hr); |
| return hr; |
| } |
| |
| // Save the password in a machine secret area. |
| hr = policy->StorePrivateData(kLsaKeyGaiaPassword, password); |
| if (FAILED(hr)) { |
| LOGFN(ERROR) << "Failed to store gaia user password in LSA hr=" |
| << putHR(hr); |
| return hr; |
| } |
| |
| // Save the gaia username in a machine secret area. |
| hr = policy->StorePrivateData(kLsaKeyGaiaUsername, gaia_username); |
| if (FAILED(hr)) { |
| LOGFN(ERROR) << "Failed to store gaia user name in LSA hr=" << putHR(hr); |
| return hr; |
| } |
| } |
| |
| if (!sid) { |
| LOGFN(ERROR) << "No valid username could be found for the gaia user."; |
| return HRESULT_FROM_WIN32(NERR_UserExists); |
| } |
| |
| // Add "logon as batch" right. |
| hr = policy->AddAccountRights(sid, SE_BATCH_LOGON_NAME); |
| ::LocalFree(sid); |
| if (FAILED(hr)) { |
| LOGFN(ERROR) << "policy.AddAccountRights hr=" << putHR(hr); |
| return hr; |
| } |
| return S_OK; |
| } |
| |
| // static |
| HRESULT CGaiaCredentialBase::OnDllUnregisterServer() { |
| auto policy = ScopedLsaPolicy::Create(POLICY_ALL_ACCESS); |
| if (policy) { |
| wchar_t password[32]; |
| |
| HRESULT hr = policy->RetrievePrivateData(kLsaKeyGaiaPassword, password, |
| base::size(password)); |
| if (FAILED(hr)) |
| LOGFN(ERROR) << "policy.RetrievePrivateData hr=" << putHR(hr); |
| |
| hr = policy->RemovePrivateData(kLsaKeyGaiaPassword); |
| if (FAILED(hr)) |
| LOGFN(ERROR) << "policy.RemovePrivateData hr=" << putHR(hr); |
| |
| OSUserManager* manager = OSUserManager::Get(); |
| PSID sid; |
| |
| wchar_t gaia_username[kWindowsUsernameBufferLength]; |
| hr = policy->RetrievePrivateData(kLsaKeyGaiaUsername, gaia_username, |
| base::size(gaia_username)); |
| |
| if (SUCCEEDED(hr)) { |
| hr = policy->RemovePrivateData(kLsaKeyGaiaUsername); |
| base::string16 local_domain = OSUserManager::GetLocalDomain(); |
| |
| hr = manager->GetUserSID(local_domain.c_str(), gaia_username, &sid); |
| if (FAILED(hr)) { |
| LOGFN(ERROR) << "manager.GetUserSID hr=" << putHR(hr); |
| sid = nullptr; |
| } |
| |
| hr = manager->RemoveUser(gaia_username, password); |
| if (FAILED(hr)) |
| LOGFN(ERROR) << "manager->RemoveUser hr=" << putHR(hr); |
| |
| // Remove the account from LSA after the OS account is deleted. |
| if (sid != nullptr) { |
| hr = policy->RemoveAccount(sid); |
| ::LocalFree(sid); |
| if (FAILED(hr)) |
| LOGFN(ERROR) << "policy.RemoveAccount hr=" << putHR(hr); |
| } |
| } else { |
| LOGFN(ERROR) << "Get gaia username failed hr=" << putHR(hr); |
| } |
| |
| } else { |
| LOGFN(ERROR) << "ScopedLsaPolicy::Create failed"; |
| } |
| |
| return S_OK; |
| } |
| |
| CGaiaCredentialBase::CGaiaCredentialBase() {} |
| |
| CGaiaCredentialBase::~CGaiaCredentialBase() {} |
| |
| bool CGaiaCredentialBase::AreCredentialsValid() const { |
| return CanAttemptWindowsLogon() && |
| IsWindowsPasswordValidForStoredUser(password_) == S_OK; |
| } |
| |
| bool CGaiaCredentialBase::CanAttemptWindowsLogon() const { |
| return username_.Length() > 0 && password_.Length() > 0; |
| } |
| |
| HRESULT CGaiaCredentialBase::IsWindowsPasswordValidForStoredUser( |
| BSTR password) const { |
| if (username_.Length() == 0 || user_sid_.Length() == 0) |
| return S_FALSE; |
| |
| if (::SysStringLen(password) == 0) |
| return S_FALSE; |
| OSUserManager* manager = OSUserManager::Get(); |
| return manager->IsWindowsPasswordValid(domain_, username_, password); |
| } |
| |
| HRESULT CGaiaCredentialBase::GetStringValueImpl(DWORD field_id, |
| wchar_t** value) { |
| HRESULT hr = E_INVALIDARG; |
| switch (field_id) { |
| case FID_DESCRIPTION: { |
| base::string16 description( |
| GetStringResource(IDS_AUTH_FID_DESCRIPTION_BASE)); |
| hr = ::SHStrDupW(description.c_str(), value); |
| break; |
| } |
| case FID_PROVIDER_LABEL: { |
| base::string16 label(GetStringResource(IDS_AUTH_FID_PROVIDER_LABEL_BASE)); |
| hr = ::SHStrDupW(label.c_str(), value); |
| break; |
| } |
| case FID_CURRENT_PASSWORD_FIELD: { |
| hr = ::SHStrDupW(current_windows_password_.Length() > 0 |
| ? current_windows_password_ |
| : L"", |
| value); |
| break; |
| } |
| case FID_FORGOT_PASSWORD_LINK: { |
| base::string16 forgot_password( |
| GetStringResource(IDS_FORGOT_PASSWORD_LINK_BASE)); |
| hr = ::SHStrDupW(forgot_password.c_str(), value); |
| break; |
| } |
| default: |
| break; |
| } |
| |
| return hr; |
| } |
| |
| HRESULT CGaiaCredentialBase::GetBitmapValueImpl(DWORD field_id, |
| HBITMAP* phbmp) { |
| HRESULT hr = E_INVALIDARG; |
| switch (field_id) { |
| case FID_PROVIDER_LOGO: |
| *phbmp = ::LoadBitmap(CURRENT_MODULE(), |
| MAKEINTRESOURCE(IDB_GOOGLE_LOGO_SMALL)); |
| if (*phbmp) |
| hr = S_OK; |
| break; |
| default: |
| break; |
| } |
| |
| return hr; |
| } |
| |
| void CGaiaCredentialBase::ResetInternalState() { |
| LOGFN(INFO); |
| username_.Empty(); |
| domain_.Empty(); |
| wait_for_report_result_ = false; |
| |
| SecurelyClearBuffer((BSTR)password_, password_.ByteLength()); |
| password_.Empty(); |
| |
| current_windows_password_.Empty(); |
| |
| SecurelyClearDictionaryValue(&authentication_results_); |
| needs_windows_password_ = false; |
| request_force_password_change_ = false; |
| result_status_ = STATUS_SUCCESS; |
| |
| TerminateLogonProcess(); |
| |
| if (events_) { |
| wchar_t* default_status_text = nullptr; |
| GetStringValue(FID_DESCRIPTION, &default_status_text); |
| events_->SetFieldString(this, FID_DESCRIPTION, default_status_text); |
| events_->SetFieldState(this, FID_FORGOT_PASSWORD_LINK, CPFS_HIDDEN); |
| events_->SetFieldState(this, FID_CURRENT_PASSWORD_FIELD, CPFS_HIDDEN); |
| events_->SetFieldString(this, FID_CURRENT_PASSWORD_FIELD, |
| current_windows_password_); |
| events_->SetFieldSubmitButton(this, FID_SUBMIT, FID_DESCRIPTION); |
| UpdateSubmitButtonInteractiveState(); |
| } |
| |
| token_update_locker_.reset(); |
| } |
| |
| HRESULT CGaiaCredentialBase::GetBaseGlsCommandline( |
| base::CommandLine* command_line) { |
| DCHECK(command_line); |
| |
| base::FilePath gls_path = |
| chrome_launcher_support::GetChromePathForInstallationLevel( |
| chrome_launcher_support::SYSTEM_LEVEL_INSTALLATION, false); |
| |
| constexpr wchar_t kGlsPath[] = L"gls_path"; |
| |
| wchar_t custom_gls_path_value[MAX_PATH]; |
| ULONG path_len = base::size(custom_gls_path_value); |
| HRESULT hr = GetGlobalFlag(kGlsPath, custom_gls_path_value, &path_len); |
| if (SUCCEEDED(hr)) { |
| base::FilePath custom_gls_path(custom_gls_path_value); |
| if (base::PathExists(custom_gls_path)) { |
| gls_path = custom_gls_path; |
| } else { |
| LOGFN(ERROR) << "Specified gls path ('" << custom_gls_path.value() |
| << "') does not exist, using default gls path."; |
| } |
| } |
| |
| if (gls_path.empty()) { |
| LOGFN(ERROR) << "No path to chrome.exe could be found."; |
| return HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND); |
| } |
| |
| command_line->SetProgram(gls_path); |
| |
| LOGFN(INFO) << "App exe: " << command_line->GetProgram().value(); |
| |
| command_line->AppendSwitch(kGcpwSigninSwitch); |
| |
| // Registry specified endpoint. |
| wchar_t endpoint_url_setting[256]; |
| ULONG endpoint_url_length = base::size(endpoint_url_setting); |
| if (SUCCEEDED(GetGlobalFlag(L"ep_url", endpoint_url_setting, |
| &endpoint_url_length)) && |
| endpoint_url_setting[0]) { |
| GURL endpoint_url(endpoint_url_setting); |
| if (endpoint_url.is_valid()) { |
| command_line->AppendSwitchASCII(switches::kGaiaUrl, |
| endpoint_url.GetWithEmptyPath().spec()); |
| command_line->AppendSwitchASCII(kGcpwEndpointPathSwitch, |
| endpoint_url.path().substr(1)); |
| } |
| } |
| |
| // Get the language selected by the LanguageSelector and pass it onto Chrome. |
| // The language will depend on if it is currently a SYSTEM logon (initial |
| // logon) or a lock screen logon (from a user). If the user who locked the |
| // screen has a specific language, that will be the one used for the UI |
| // language. |
| command_line->AppendSwitchNative("lang", GetSelectedLanguage()); |
| |
| return S_OK; |
| } |
| |
| HRESULT CGaiaCredentialBase::GetUserGlsCommandline( |
| base::CommandLine* command_line) { |
| return S_OK; |
| } |
| |
| HRESULT CGaiaCredentialBase::GetGlsCommandline( |
| base::CommandLine* command_line) { |
| DCHECK(command_line); |
| HRESULT hr = GetBaseGlsCommandline(command_line); |
| if (FAILED(hr)) { |
| LOGFN(ERROR) << "GetBaseGlsCommandline hr=" << putHR(hr); |
| return hr; |
| } |
| |
| // If email domains are specified, pass them to the GLS. |
| base::string16 email_domains = GetEmailDomains(); |
| if (email_domains[0]) |
| command_line->AppendSwitchNative(kEmailDomainsSwitch, email_domains); |
| |
| hr = GetUserGlsCommandline(command_line); |
| if (FAILED(hr)) { |
| LOGFN(ERROR) << "GetBaseGlsCommandline hr=" << putHR(hr); |
| return hr; |
| } |
| |
| LOGFN(INFO) << "Command line: " << command_line->GetCommandLineString(); |
| return S_OK; |
| } |
| |
| void CGaiaCredentialBase::DisplayErrorInUI(LONG status, |
| LONG substatus, |
| BSTR status_text) { |
| if (status != STATUS_SUCCESS) { |
| if (events_) |
| events_->SetFieldString(this, FID_DESCRIPTION, status_text); |
| } |
| } |
| |
| HRESULT CGaiaCredentialBase::HandleAutologon( |
| CREDENTIAL_PROVIDER_GET_SERIALIZATION_RESPONSE* cpgsr, |
| CREDENTIAL_PROVIDER_CREDENTIAL_SERIALIZATION* cpcs) { |
| USES_CONVERSION; |
| LOGFN(INFO) << "user-sid=" << get_sid().m_str; |
| DCHECK(cpgsr); |
| DCHECK(cpcs); |
| |
| if (!CanAttemptWindowsLogon()) |
| return S_FALSE; |
| |
| bool password_updated = false; |
| // If a password update is needed, check if the user entered their old |
| // Windows password and it is valid. If it is, try to change the password |
| // using the old password. If it isn't, return S_FALSE to state that the |
| // login is not complete. |
| if (needs_windows_password_) { |
| OSUserManager* manager = OSUserManager::Get(); |
| if (request_force_password_change_) { |
| HRESULT hr = manager->SetUserPassword(domain_, username_, password_); |
| if (FAILED(hr)) { |
| LOGFN(ERROR) << "SetUserPassword hr=" << putHR(hr); |
| if (events_) { |
| events_->SetFieldString( |
| this, FID_DESCRIPTION, |
| GetStringResource(IDS_FORCED_PASSWORD_CHANGE_FAILURE_BASE) |
| .c_str()); |
| } |
| return S_FALSE; |
| } |
| password_updated = true; |
| } else { |
| HRESULT hr = |
| IsWindowsPasswordValidForStoredUser(current_windows_password_); |
| if (hr == S_OK) { |
| hr = manager->ChangeUserPassword(domain_, username_, |
| current_windows_password_, password_); |
| if (FAILED(hr)) { |
| if (hr != HRESULT_FROM_WIN32(ERROR_ACCESS_DENIED)) { |
| LOGFN(ERROR) << "ChangeUserPassword hr=" << putHR(hr); |
| return hr; |
| } |
| LOGFN(ERROR) << "Access was denied to ChangeUserPassword."; |
| password_ = current_windows_password_; |
| } else { |
| password_updated = true; |
| } |
| } else { |
| if (current_windows_password_.Length() && events_) { |
| UINT pasword_message_id = IDS_INVALID_PASSWORD_BASE; |
| if (hr == HRESULT_FROM_WIN32(ERROR_ACCOUNT_LOCKED_OUT)) { |
| pasword_message_id = IDS_ACCOUNT_LOCKED_BASE; |
| LOGFN(ERROR) << "Account is locked."; |
| } |
| |
| DisplayPasswordField(pasword_message_id); |
| } |
| return S_FALSE; |
| } |
| } |
| } |
| |
| // Password was changed successfully, remove the old password information |
| // so that a new password can be saved. |
| if (password_updated) { |
| HRESULT hr = PasswordRecoveryManager::Get()->ClearUserRecoveryPassword( |
| OLE2CW(get_sid())); |
| if (FAILED(hr)) |
| LOGFN(ERROR) << "ClearUserRecoveryPassword hr=" << putHR(hr); |
| } |
| |
| // The OS user has already been created, so return all the information |
| // needed to log them in. |
| DWORD cpus = 0; |
| provider()->GetUsageScenario(&cpus); |
| HRESULT hr = BuildCredPackAuthenticationBuffer( |
| domain_, get_username(), get_password(), |
| static_cast<CREDENTIAL_PROVIDER_USAGE_SCENARIO>(cpus), cpcs); |
| if (FAILED(hr)) { |
| LOGFN(ERROR) << "BuildCredPackAuthenticationBuffer hr=" << putHR(hr); |
| return hr; |
| } |
| |
| // Prevent update of token handle validity until after sign in has completed |
| // so that a race condition doesn't end up locking out a user while they are |
| // in the process of signing in. The lock must occur before restoring access |
| // to the user below to prevent a race condition where the user would have |
| // their access restored but then the token handle update thread is |
| // immediately executed which causes the user to be locked again afterwards. |
| PreventDenyAccessUpdate(); |
| |
| // Restore user's access so that they can sign in. |
| hr = AssociatedUserValidator::Get()->RestoreUserAccess(OLE2W(get_sid())); |
| if (FAILED(hr) && hr != HRESULT_FROM_NT(STATUS_OBJECT_NAME_NOT_FOUND)) { |
| LOGFN(ERROR) << "RestoreUserAccess hr=" << putHR(hr); |
| ::CoTaskMemFree(cpcs->rgbSerialization); |
| cpcs->rgbSerialization = nullptr; |
| return hr; |
| } |
| |
| cpcs->clsidCredentialProvider = CLSID_GaiaCredentialProvider; |
| *cpgsr = CPGSR_RETURN_CREDENTIAL_FINISHED; |
| |
| return S_OK; |
| } |
| |
| // static |
| void CGaiaCredentialBase::TellOmahaDidRun() { |
| #if defined(GOOGLE_CHROME_BUILD) |
| // Tell omaha that product was used. Best effort only. |
| // |
| // This code always runs as LocalSystem, which means that HKCU maps to |
| // HKU\.Default. This is OK because omaha reads the "dr" value from subkeys |
| // of HKEY_USERS. |
| base::win::RegKey key; |
| LONG sts = key.Create(HKEY_CURRENT_USER, kRegUpdaterClientStateAppPath, |
| KEY_SET_VALUE | KEY_WOW64_32KEY); |
| if (sts != ERROR_SUCCESS) { |
| LOGFN(INFO) << "Unable to open omaha key sts=" << sts; |
| } else { |
| sts = key.WriteValue(L"dr", L"1"); |
| if (sts != ERROR_SUCCESS) |
| LOGFN(INFO) << "Unable to write omaha dr value sts=" << sts; |
| } |
| #endif // defined(GOOGLE_CHROME_BUILD) |
| } |
| |
| void CGaiaCredentialBase::PreventDenyAccessUpdate() { |
| if (!token_update_locker_) { |
| token_update_locker_.reset( |
| new AssociatedUserValidator::ScopedBlockDenyAccessUpdate( |
| AssociatedUserValidator::Get())); |
| } |
| } |
| |
| // static |
| BSTR CGaiaCredentialBase::AllocErrorString(UINT id) { |
| CComBSTR str(GetStringResource(id).c_str()); |
| return str.Detach(); |
| } |
| |
| // static |
| HRESULT CGaiaCredentialBase::GetInstallDirectory(base::FilePath* path) { |
| DCHECK(path); |
| |
| if (!base::PathService::Get(base::FILE_MODULE, path)) { |
| HRESULT hr = HRESULT_FROM_WIN32(::GetLastError()); |
| LOGFN(ERROR) << "Get(FILE_MODULE) hr=" << putHR(hr); |
| return hr; |
| } |
| |
| *path = path->DirName(); |
| return S_OK; |
| } |
| |
| // ICredentialProviderCredential ////////////////////////////////////////////// |
| |
| HRESULT CGaiaCredentialBase::Advise(ICredentialProviderCredentialEvents* cpce) { |
| LOGFN(INFO); |
| events_ = cpce; |
| return S_OK; |
| } |
| |
| HRESULT CGaiaCredentialBase::UnAdvise(void) { |
| LOGFN(INFO); |
| events_.Release(); |
| |
| return S_OK; |
| } |
| |
| HRESULT CGaiaCredentialBase::SetSelected(BOOL* auto_login) { |
| *auto_login = CanAttemptWindowsLogon(); |
| LOGFN(INFO) << "auto-login=" << *auto_login; |
| |
| // After this point the user is able to interact with the winlogon and thus |
| // can avoid potential crash loops so the startup sentinel can be deleted. |
| DeleteStartupSentinel(); |
| return S_OK; |
| } |
| |
| HRESULT CGaiaCredentialBase::SetDeselected(void) { |
| LOGFN(INFO); |
| |
| // This check is trying to handle the scenario when GetSerialization finishes |
| // with cpgsr set as CPGSR_RETURN_CREDENTIAL_FINISHED which indicates that |
| // the windows autologon is ready to go. In this case ideally ReportResult |
| // should be invoked by the windows login UI process prior to SetDeselected. |
| // But for OtherUserCredential scenario, SetDeselected is being invoked |
| // prior to ReportResult which is leading to clearing of the internalstate |
| // prior to saving the account user info in ReportResult. |
| if (!wait_for_report_result_) { |
| // Cancel logon so that the next time this credential is clicked everything |
| // has to be re-entered by the user. This prevents a Windows password |
| // entered into the password field by the user from being persisted too |
| // long. The behaviour is similar to that of the normal windows password |
| // text box. Whenever a different user is selected and then the original |
| // credential is selected again, the password is cleared. |
| ResetInternalState(); |
| } |
| return S_OK; |
| } |
| |
| HRESULT CGaiaCredentialBase::GetFieldState( |
| DWORD field_id, |
| CREDENTIAL_PROVIDER_FIELD_STATE* pcpfs, |
| CREDENTIAL_PROVIDER_FIELD_INTERACTIVE_STATE* pcpfis) { |
| HRESULT hr = E_INVALIDARG; |
| switch (field_id) { |
| case FID_DESCRIPTION: |
| case FID_SUBMIT: |
| *pcpfs = CPFS_DISPLAY_IN_SELECTED_TILE; |
| *pcpfis = CPFIS_NONE; |
| hr = S_OK; |
| break; |
| case FID_PROVIDER_LOGO: |
| *pcpfs = ::IsWindows8OrGreater() ? CPFS_HIDDEN : CPFS_DISPLAY_IN_BOTH; |
| *pcpfis = CPFIS_NONE; |
| hr = S_OK; |
| break; |
| case FID_PROVIDER_LABEL: |
| *pcpfs = ::IsWindows8OrGreater() ? CPFS_HIDDEN |
| : CPFS_DISPLAY_IN_DESELECTED_TILE; |
| *pcpfis = CPFIS_NONE; |
| hr = S_OK; |
| break; |
| case FID_CURRENT_PASSWORD_FIELD: |
| *pcpfs = CPFS_HIDDEN; |
| *pcpfis = CPFIS_NONE; |
| hr = S_OK; |
| break; |
| case FID_FORGOT_PASSWORD_LINK: |
| *pcpfs = CPFS_HIDDEN; |
| *pcpfis = CPFIS_NONE; |
| hr = S_OK; |
| break; |
| default: |
| break; |
| } |
| LOGFN(INFO) << "hr=" << putHR(hr) << " field=" << field_id |
| << " state=" << *pcpfs << " inter-state=" << *pcpfis; |
| return hr; |
| } |
| |
| HRESULT CGaiaCredentialBase::GetStringValue(DWORD field_id, wchar_t** value) { |
| return GetStringValueImpl(field_id, value); |
| } |
| |
| HRESULT CGaiaCredentialBase::GetBitmapValue(DWORD field_id, HBITMAP* phbmp) { |
| return GetBitmapValueImpl(field_id, phbmp); |
| } |
| |
| HRESULT CGaiaCredentialBase::GetCheckboxValue(DWORD field_id, |
| BOOL* pbChecked, |
| wchar_t** ppszLabel) { |
| // No checkboxes. |
| return E_NOTIMPL; |
| } |
| |
| HRESULT CGaiaCredentialBase::GetSubmitButtonValue(DWORD field_id, |
| DWORD* adjacent_to) { |
| HRESULT hr = E_INVALIDARG; |
| switch (field_id) { |
| case FID_SUBMIT: |
| *adjacent_to = FID_DESCRIPTION; |
| hr = S_OK; |
| break; |
| default: |
| break; |
| } |
| |
| return hr; |
| } |
| |
| HRESULT CGaiaCredentialBase::GetComboBoxValueCount(DWORD field_id, |
| DWORD* pcItems, |
| DWORD* pdwSelectedItem) { |
| // No comboboxes. |
| return E_NOTIMPL; |
| } |
| |
| HRESULT CGaiaCredentialBase::GetComboBoxValueAt(DWORD field_id, |
| DWORD dwItem, |
| wchar_t** ppszItem) { |
| // No comboboxes. |
| return E_NOTIMPL; |
| } |
| |
| HRESULT CGaiaCredentialBase::SetStringValue(DWORD field_id, |
| const wchar_t* psz) { |
| USES_CONVERSION; |
| |
| HRESULT hr = E_INVALIDARG; |
| switch (field_id) { |
| case FID_CURRENT_PASSWORD_FIELD: |
| if (needs_windows_password_) { |
| current_windows_password_ = W2COLE(psz); |
| UpdateSubmitButtonInteractiveState(); |
| } |
| hr = S_OK; |
| break; |
| } |
| return hr; |
| } |
| |
| HRESULT CGaiaCredentialBase::SetCheckboxValue(DWORD field_id, BOOL bChecked) { |
| // No checkboxes. |
| return E_NOTIMPL; |
| } |
| |
| HRESULT CGaiaCredentialBase::SetComboBoxSelectedValue(DWORD field_id, |
| DWORD dwSelectedItem) { |
| // No comboboxes. |
| return E_NOTIMPL; |
| } |
| |
| HRESULT CGaiaCredentialBase::CommandLinkClicked(DWORD dwFieldID) { |
| if (dwFieldID == FID_FORGOT_PASSWORD_LINK && needs_windows_password_) { |
| request_force_password_change_ = !request_force_password_change_; |
| DisplayPasswordField(IDS_PASSWORD_UPDATE_NEEDED_BASE); |
| UpdateSubmitButtonInteractiveState(); |
| return S_OK; |
| } |
| |
| return E_INVALIDARG; |
| } |
| |
| HRESULT CGaiaCredentialBase::GetSerialization( |
| CREDENTIAL_PROVIDER_GET_SERIALIZATION_RESPONSE* cpgsr, |
| CREDENTIAL_PROVIDER_CREDENTIAL_SERIALIZATION* cpcs, |
| wchar_t** status_text, |
| CREDENTIAL_PROVIDER_STATUS_ICON* status_icon) { |
| USES_CONVERSION; |
| LOGFN(INFO); |
| DCHECK(status_text); |
| DCHECK(status_icon); |
| |
| *status_text = nullptr; |
| *status_icon = CPSI_NONE; |
| memset(cpcs, 0, sizeof(*cpcs)); |
| |
| // This may be a long running function so disable user input while processing. |
| if (events_) { |
| events_->SetFieldInteractiveState(this, FID_SUBMIT, CPFIS_DISABLED); |
| events_->SetFieldInteractiveState(this, FID_CURRENT_PASSWORD_FIELD, |
| CPFIS_DISABLED); |
| } |
| |
| HRESULT hr = HandleAutologon(cpgsr, cpcs); |
| |
| bool submit_button_enabled = false; |
| // Don't clear the state of the credential on error. The error can occur |
| // because the user is locked out or entered an incorrect old password when |
| // trying to update their password. In these situations it may still be |
| // possible to sign in with the information that is currently available if |
| // the problem can be fixed externally so keep all the information for now. |
| if (FAILED(hr)) { |
| LOGFN(ERROR) << "HandleAutologon hr=" << putHR(hr); |
| *status_icon = CPSI_ERROR; |
| *cpgsr = CPGSR_RETURN_NO_CREDENTIAL_FINISHED; |
| } else if (hr == S_FALSE) { |
| // If HandleAutologon returns S_FALSE, then there was not enough information |
| // to log the user on or they need to update their password and gave an |
| // invalid old password. Display the Gaia sign in page if there is not |
| // sufficient Gaia credentials or just return |
| // CPGSR_NO_CREDENTIAL_NOT_FINISHED to wait for the user to try a new |
| // password. |
| |
| // Logon process is still running or windows password needs to be entered, |
| // return that serialization is not finished so that a second logon stub |
| // isn't started. |
| if (logon_ui_process_ != INVALID_HANDLE_VALUE || needs_windows_password_) { |
| *cpgsr = CPGSR_NO_CREDENTIAL_NOT_FINISHED; |
| |
| // Warn that password needs update. |
| if (needs_windows_password_) |
| *status_icon = CPSI_WARNING; |
| |
| hr = S_OK; |
| } else { |
| LOGFN(INFO) << "HandleAutologon hr=" << putHR(hr); |
| TellOmahaDidRun(); |
| |
| // If there is no internet connection, just abort right away. |
| if (!InternetAvailabilityChecker::Get()->HasInternetConnection()) { |
| BSTR error_message = AllocErrorString(IDS_NO_NETWORK_BASE); |
| ::SHStrDupW(OLE2CW(error_message), status_text); |
| ::SysFreeString(error_message); |
| |
| *status_icon = CPSI_NONE; |
| *cpgsr = CPGSR_NO_CREDENTIAL_FINISHED; |
| LOGFN(INFO) << "No internet connection"; |
| submit_button_enabled = UpdateSubmitButtonInteractiveState(); |
| |
| hr = S_OK; |
| } else { |
| // The account creation is async so we are not done yet. |
| *cpgsr = CPGSR_NO_CREDENTIAL_NOT_FINISHED; |
| |
| // The expectation is that the UI will eventually return the username, |
| // password, and auth to this CGaiaCredentialBase object, so that |
| // OnUserAuthenticated() can be called, followed by |
| // provider_->OnUserAuthenticated(). |
| hr = CreateAndRunLogonStub(); |
| } |
| } |
| } else { |
| *status_icon = CPSI_SUCCESS; |
| } |
| |
| // Logon is not complete, re-enable UI as needed. |
| if (*cpgsr != CPGSR_NO_CREDENTIAL_FINISHED && |
| *cpgsr != CPGSR_RETURN_CREDENTIAL_FINISHED && |
| *cpgsr != CPGSR_RETURN_NO_CREDENTIAL_FINISHED) { |
| if (events_) { |
| events_->SetFieldInteractiveState( |
| this, FID_CURRENT_PASSWORD_FIELD, |
| needs_windows_password_ ? CPFIS_FOCUSED : CPFIS_NONE); |
| } |
| submit_button_enabled = UpdateSubmitButtonInteractiveState(); |
| } |
| |
| // If user interaction is enabled that means we are not trying to do final |
| // sign in of the account so we can re-enable token updates. |
| if (submit_button_enabled) |
| token_update_locker_.reset(); |
| |
| // If cpgsr is CPGSR_RETURN_CREDENTIAL_FINISHED and the status is S_OK, then |
| // report result would be invoked. So we shouldn't be resetting the internal |
| // state prior to report result getting triggered. |
| if (*cpgsr == CPGSR_RETURN_CREDENTIAL_FINISHED && hr == S_OK) { |
| wait_for_report_result_ = true; |
| } |
| |
| // Otherwise, keep the ui disabled forever now. ReportResult will eventually |
| // be called on success or failure and the reset of the state of the |
| // credential will be done there. |
| return hr; |
| } |
| |
| HRESULT CGaiaCredentialBase::CreateAndRunLogonStub() { |
| LOGFN(INFO); |
| |
| base::CommandLine command_line(base::CommandLine::NO_PROGRAM); |
| HRESULT hr = GetGlsCommandline(&command_line); |
| if (FAILED(hr)) { |
| LOGFN(ERROR) << "GetGlsCommandline hr=" << putHR(hr); |
| return hr; |
| } |
| |
| // The process should start on the interactive window station (since it |
| // needs to show a UI) but on its own desktop so that it cannot interact |
| // with winlogon on user windows. |
| std::unique_ptr<UIProcessInfo> uiprocinfo(new UIProcessInfo); |
| PSID logon_sid; |
| hr = CreateGaiaLogonToken(&uiprocinfo->logon_token, &logon_sid); |
| if (FAILED(hr)) { |
| LOGFN(ERROR) << "CreateGaiaLogonToken hr=" << putHR(hr); |
| return hr; |
| } |
| |
| OSProcessManager* process_manager = OSProcessManager::Get(); |
| hr = process_manager->SetupPermissionsForLogonSid(logon_sid); |
| LocalFree(logon_sid); |
| if (FAILED(hr)) { |
| LOGFN(ERROR) << "SetupPermissionsForLogonSid hr=" << putHR(hr); |
| return hr; |
| } |
| |
| hr = ForkGaiaLogonStub(process_manager, command_line, uiprocinfo.get()); |
| if (FAILED(hr)) { |
| LOGFN(ERROR) << "ForkGaiaLogonStub hr=" << putHR(hr); |
| return hr; |
| } |
| |
| // Save the handle to the logon UI process so that it can be killed should |
| // the credential be Unadvise()d. |
| DCHECK_EQ(logon_ui_process_, INVALID_HANDLE_VALUE); |
| logon_ui_process_ = uiprocinfo->procinfo.process_handle(); |
| |
| uiprocinfo->credential = this; |
| |
| // Background thread takes ownership of |uiprocinfo|. |
| unsigned int wait_thread_id; |
| UIProcessInfo* puiprocinfo = uiprocinfo.release(); |
| uintptr_t wait_thread = _beginthreadex(nullptr, 0, WaitForLoginUI, |
| puiprocinfo, 0, &wait_thread_id); |
| if (wait_thread != 0) { |
| LOGFN(INFO) << "Started wait thread id=" << wait_thread_id; |
| ::CloseHandle(reinterpret_cast<HANDLE>(wait_thread)); |
| } else { |
| HRESULT hr = HRESULT_FROM_WIN32(::GetLastError()); |
| LOGFN(ERROR) << "Unable to start wait thread hr=" << putHR(hr); |
| ::TerminateProcess(puiprocinfo->procinfo.process_handle(), kUiecKilled); |
| delete puiprocinfo; |
| return hr; |
| } |
| |
| // This function returns success, which means that GetSerialization() will |
| // return success. CGaiaCredentialBase is now committed to telling |
| // CGaiaCredentialProvider whether the serialization eventually succeeds or |
| // fails, so that CGaiaCredentialProvider can in turn inform winlogon about |
| // what happened. |
| LOGFN(INFO) << "cleaning up"; |
| return S_OK; |
| } |
| |
| // static |
| HRESULT CGaiaCredentialBase::CreateGaiaLogonToken( |
| base::win::ScopedHandle* token, |
| PSID* sid) { |
| DCHECK(token); |
| DCHECK(sid); |
| |
| auto policy = ScopedLsaPolicy::Create(POLICY_ALL_ACCESS); |
| if (!policy) { |
| LOGFN(ERROR) << "LsaOpenPolicy failed"; |
| return E_UNEXPECTED; |
| } |
| |
| wchar_t gaia_username[kWindowsUsernameBufferLength]; |
| HRESULT hr = policy->RetrievePrivateData(kLsaKeyGaiaUsername, gaia_username, |
| base::size(gaia_username)); |
| |
| if (FAILED(hr)) { |
| LOGFN(ERROR) << "Retrieve gaia username hr=" << putHR(hr); |
| return hr; |
| } |
| wchar_t password[32]; |
| hr = policy->RetrievePrivateData(kLsaKeyGaiaPassword, password, |
| base::size(password)); |
| if (FAILED(hr)) { |
| LOGFN(ERROR) << "Retrieve password for gaia user '" << gaia_username |
| << "' hr=" << putHR(hr); |
| return hr; |
| } |
| |
| base::string16 local_domain = OSUserManager::GetLocalDomain(); |
| hr = OSUserManager::Get()->CreateLogonToken(local_domain.c_str(), |
| gaia_username, password, |
| /*interactive=*/false, token); |
| if (FAILED(hr)) { |
| LOGFN(ERROR) << "CreateLogonToken hr=" << putHR(hr); |
| return hr; |
| } |
| |
| hr = OSProcessManager::Get()->GetTokenLogonSID(*token, sid); |
| if (FAILED(hr)) { |
| LOGFN(ERROR) << "GetTokenLogonSID hr=" << putHR(hr); |
| token->Close(); |
| return hr; |
| } |
| |
| wchar_t* sid_string; |
| if (::ConvertSidToStringSid(*sid, &sid_string)) { |
| LOGFN(INFO) << "logon-sid=" << sid_string; |
| LocalFree(sid_string); |
| } else { |
| LOGFN(ERROR) << "logon-sid=<can't get string>"; |
| } |
| |
| return S_OK; |
| } |
| |
| // static |
| HRESULT CGaiaCredentialBase::ForkGaiaLogonStub( |
| OSProcessManager* process_manager, |
| const base::CommandLine& command_line, |
| UIProcessInfo* uiprocinfo) { |
| LOGFN(INFO); |
| DCHECK(process_manager); |
| DCHECK(uiprocinfo); |
| |
| ScopedStartupInfo startupinfo(kDesktopFullName); |
| |
| // Only create a stdout pipe for the logon stub process. On some machines |
| // Chrome will not startup properly when also given a stderror pipe due |
| // to access restrictions. For the purposes of the credential provider |
| // only the output of stdout matters. |
| HRESULT hr = |
| InitializeStdHandles(CommDirection::kChildToParentOnly, kStdOutput, |
| &startupinfo, &uiprocinfo->parent_handles); |
| if (FAILED(hr)) { |
| LOGFN(ERROR) << "InitializeStdHandles hr=" << putHR(hr); |
| return hr; |
| } |
| |
| // The process is created suspended so that we can adjust its environment |
| // before it starts. Also, it must not run before it is added to the job |
| // object. |
| hr = process_manager->CreateProcessWithToken( |
| uiprocinfo->logon_token, command_line, startupinfo.GetInfo(), |
| &uiprocinfo->procinfo); |
| if (FAILED(hr)) { |
| LOGFN(ERROR) << "process_manager->CreateProcessWithToken hr=" << putHR(hr); |
| return hr; |
| } |
| |
| LOGFN(INFO) << "pid=" << uiprocinfo->procinfo.process_id() |
| << " tid=" << uiprocinfo->procinfo.thread_id(); |
| |
| // Don't create a job here with UI restrictions, since win10 does not allow |
| // nested jobs unless all jobs don't specify UI restrictions. Since chrome |
| // will set a job with UI restrictions for renderer/gpu/etc processes, setting |
| // one here causes chrome to fail. |
| |
| // Environment is fully set up for UI, so let it go. |
| if (::ResumeThread(uiprocinfo->procinfo.thread_handle()) == |
| static_cast<DWORD>(-1)) { |
| HRESULT hr = HRESULT_FROM_WIN32(::GetLastError()); |
| LOGFN(ERROR) << "ResumeThread hr=" << putHR(hr); |
| ::TerminateProcess(uiprocinfo->procinfo.process_handle(), kUiecKilled); |
| return hr; |
| } |
| |
| // Don't close the desktop until after the process has started and acquired |
| // a handle to it. Otherwise, the desktop will be destroyed and the process |
| // will fail to start. |
| // |
| // WaitForInputIdle() return immediately with an error if the process |
| // created is a console app. In production this will not be the case, |
| // however in tests this may happen. However, tests are not concerned with |
| // the destruction of the desktop since one is not created. |
| DWORD ret = ::WaitForInputIdle(uiprocinfo->procinfo.process_handle(), 10000); |
| if (ret != 0) |
| LOGFN(INFO) << "WaitForInputIdle, ret=" << ret; |
| |
| return S_OK; |
| } |
| |
| HRESULT CGaiaCredentialBase::ForkSaveAccountInfoStub(const base::Value& dict, |
| BSTR* status_text) { |
| LOGFN(INFO); |
| DCHECK(status_text); |
| |
| ScopedStartupInfo startupinfo; |
| StdParentHandles parent_handles; |
| HRESULT hr = |
| InitializeStdHandles(CommDirection::kParentToChildOnly, kAllStdHandles, |
| &startupinfo, &parent_handles); |
| if (FAILED(hr)) { |
| LOGFN(ERROR) << "InitializeStdHandles hr=" << putHR(hr); |
| *status_text = AllocErrorString(IDS_INTERNAL_ERROR_BASE); |
| return hr; |
| } |
| |
| base::CommandLine command_line(base::CommandLine::NO_PROGRAM); |
| hr = GetCommandLineForEntrypoint(CURRENT_MODULE(), L"SaveAccountInfo", |
| &command_line); |
| if (hr == S_FALSE) { |
| // This happens in tests. It means this code is running inside the |
| // unittest exe and not the credential provider dll. Just ignore saving |
| // the account info. |
| LOGFN(INFO) << "Not running SAIS"; |
| return S_OK; |
| } else if (FAILED(hr)) { |
| LOGFN(ERROR) << "GetCommandLineForEntryPoint hr=" << putHR(hr); |
| *status_text = AllocErrorString(IDS_INTERNAL_ERROR_BASE); |
| return hr; |
| } |
| |
| // Mark this process as a child process so that it doesn't try to |
| // start a crashpad handler process. Only the main entry point |
| // into the dll should start the handler process. |
| command_line.AppendSwitchASCII(switches::kProcessType, |
| "gcpw-save-account-info"); |
| |
| base::win::ScopedProcessInformation procinfo; |
| hr = OSProcessManager::Get()->CreateRunningProcess( |
| command_line, startupinfo.GetInfo(), &procinfo); |
| if (FAILED(hr)) { |
| LOGFN(ERROR) << "OSProcessManager::CreateRunningProcess hr=" << putHR(hr); |
| *status_text = AllocErrorString(IDS_INTERNAL_ERROR_BASE); |
| return hr; |
| } |
| |
| // Write account info to stdin of child process. This buffer is read by |
| // SaveAccountInfoW() in dllmain.cpp. If this fails, chrome won't pick up |
| // the credentials from the credential provider and will need to sign in |
| // manually. TODO(crbug.com/902911): Figure out how to handle this. |
| std::string json; |
| if (base::JSONWriter::Write(dict, &json)) { |
| DWORD written; |
| if (!::WriteFile(parent_handles.hstdin_write.Get(), json.c_str(), |
| json.length() + 1, &written, /*lpOverlapped=*/nullptr)) { |
| HRESULT hrWrite = HRESULT_FROM_WIN32(::GetLastError()); |
| LOGFN(ERROR) << "WriteFile hr=" << putHR(hrWrite); |
| } |
| SecurelyClearString(json); |
| } else { |
| LOGFN(ERROR) << "base::JSONWriter::Write failed"; |
| } |
| |
| return S_OK; |
| } |
| |
| // static |
| unsigned __stdcall CGaiaCredentialBase::WaitForLoginUI(void* param) { |
| USES_CONVERSION; |
| DCHECK(param); |
| std::unique_ptr<UIProcessInfo> uiprocinfo( |
| reinterpret_cast<UIProcessInfo*>(param)); |
| |
| // Make sure COM is initialized in this thread. This thread must be |
| // initialized as an MTA or the call to enroll with MDM causes a crash in COM. |
| base::win::ScopedCOMInitializer com_initializer( |
| base::win::ScopedCOMInitializer::kMTA); |
| if (!com_initializer.Succeeded()) { |
| HRESULT hr = HRESULT_FROM_WIN32(::GetLastError()); |
| LOGFN(ERROR) << "ScopedCOMInitializer failed hr=" << putHR(hr); |
| return hr; |
| } |
| |
| CComBSTR status_text; |
| DWORD exit_code; |
| std::string json_result; |
| HRESULT hr = WaitForLoginUIAndGetResult(uiprocinfo.get(), &json_result, |
| &exit_code, &status_text); |
| if (SUCCEEDED(hr)) { |
| // Notify that the new user is created. |
| // TODO(rogerta): Docs say this should not be called on a background |
| // thread, but on the thread that received the |
| // CGaiaCredentialBase::Advise() call. Seems to work for now though, but I |
| // suspect there could be a problem if this call races with a call to |
| // CGaiaCredentialBase::Unadvise(). |
| base::string16 json_result16 = base::UTF8ToUTF16(json_result); |
| CComBSTR result_string(W2COLE(json_result16.c_str())); |
| SecurelyClearString(json_result16); |
| |
| hr = uiprocinfo->credential->OnUserAuthenticated(result_string, |
| &status_text); |
| SecurelyClearBuffer((BSTR)result_string, result_string.ByteLength()); |
| } |
| |
| SecurelyClearString(json_result); |
| |
| // If the process was killed by the credential in Terminate(), don't process |
| // the error message since it is possible that the credential and/or the |
| // provider no longer exists. |
| if (FAILED(hr)) { |
| if (hr != E_ABORT) |
| LOGFN(ERROR) << "WaitForLoginUIAndGetResult hr=" << putHR(hr); |
| |
| // If hr is E_ABORT, this is a user initiated cancel. Don't consider this |
| // an error. |
| LONG sts = hr == E_ABORT ? STATUS_SUCCESS : HRESULT_CODE(hr); |
| |
| // Either WaitForLoginUIAndGetResult did not fail or there should be an |
| // error message to display. |
| DCHECK(sts == STATUS_SUCCESS || status_text != nullptr); |
| hr = uiprocinfo->credential->ReportError(sts, STATUS_SUCCESS, status_text); |
| if (FAILED(hr)) |
| LOGFN(ERROR) << "uiprocinfo->credential->ReportError hr=" << putHR(hr); |
| } |
| |
| LOGFN(INFO) << "done"; |
| return 0; |
| } |
| |
| // static |
| HRESULT CGaiaCredentialBase::SaveAccountInfo(const base::Value& properties) { |
| LOGFN(INFO); |
| |
| base::string16 sid = GetDictString(properties, kKeySID); |
| if (sid.empty()) { |
| LOGFN(ERROR) << "SID is empty"; |
| return E_INVALIDARG; |
| } |
| |
| base::string16 username = GetDictString(properties, kKeyUsername); |
| if (username.empty()) { |
| LOGFN(ERROR) << "Username is empty"; |
| return E_INVALIDARG; |
| } |
| |
| base::string16 password = GetDictString(properties, kKeyPassword); |
| if (password.empty()) { |
| LOGFN(ERROR) << "Password is empty"; |
| return E_INVALIDARG; |
| } |
| |
| // TODO(crbug.com/976744): Use the down scoped kKeyMdmAccessToken instead |
| // of login scoped token. |
| std::string access_token = GetDictStringUTF8(properties, kKeyAccessToken); |
| if (!access_token.empty()) { |
| // Update the password recovery information if possible. |
| HRESULT hr = PasswordRecoveryManager::Get()->StoreWindowsPasswordIfNeeded( |
| sid, access_token, password); |
| if (FAILED(hr) && hr != E_NOTIMPL) |
| LOGFN(ERROR) << "StoreWindowsPasswordIfNeeded hr=" << putHR(hr); |
| } else { |
| LOGFN(ERROR) << "Access token is empty. Cannot save Windows password."; |
| } |
| |
| base::string16 domain = GetDictString(properties, kKeyDomain); |
| |
| // Load the user's profile so that their registry hive is available. |
| auto profile = ScopedUserProfile::Create(sid, domain, username, password); |
| |
| SecurelyClearString(password); |
| |
| if (!profile) { |
| LOGFN(ERROR) << "Could not load user profile"; |
| return E_UNEXPECTED; |
| } |
| |
| HRESULT hr = profile->SaveAccountInfo(properties); |
| if (FAILED(hr)) |
| LOGFN(ERROR) << "profile.SaveAccountInfo failed (cont) hr=" << putHR(hr); |
| |
| return hr; |
| } |
| |
| HRESULT CGaiaCredentialBase::ReportResult( |
| NTSTATUS status, |
| NTSTATUS substatus, |
| wchar_t** ppszOptionalStatusText, |
| CREDENTIAL_PROVIDER_STATUS_ICON* pcpsiOptionalStatusIcon) { |
| LOGFN(INFO) << "status=" << putHR(status) |
| << " substatus=" << putHR(substatus); |
| |
| if (status == STATUS_SUCCESS && authentication_results_) { |
| // Update the sid, domain, username and password in |
| // |authentication_results_| with the real Windows information for the user |
| // so that the SaveAccountInfo process can correctly sign in to the user |
| // account. |
| authentication_results_->SetKey( |
| kKeySID, base::Value(base::UTF16ToUTF8((BSTR)user_sid_))); |
| authentication_results_->SetKey( |
| kKeyDomain, base::Value(base::UTF16ToUTF8((BSTR)domain_))); |
| authentication_results_->SetKey( |
| kKeyUsername, base::Value(base::UTF16ToUTF8((BSTR)username_))); |
| authentication_results_->SetKey( |
| kKeyPassword, base::Value(base::UTF16ToUTF8((BSTR)password_))); |
| |
| // At this point the user and password stored in authentication_results_ |
| // should match what is stored in username_ and password_ so the |
| // SaveAccountInfo process can be forked. |
| CComBSTR status_text; |
| HRESULT hr = |
| ForkSaveAccountInfoStub(*authentication_results_, &status_text); |
| if (FAILED(hr)) |
| LOGFN(ERROR) << "ForkSaveAccountInfoStub hr=" << putHR(hr); |
| } |
| |
| *ppszOptionalStatusText = nullptr; |
| *pcpsiOptionalStatusIcon = CPSI_NONE; |
| ResetInternalState(); |
| return S_OK; |
| } |
| |
| HRESULT CGaiaCredentialBase::GetUserSid(wchar_t** sid) { |
| *sid = nullptr; |
| return S_FALSE; |
| } |
| |
| HRESULT CGaiaCredentialBase::Initialize(IGaiaCredentialProvider* provider) { |
| LOGFN(INFO); |
| DCHECK(provider); |
| |
| provider_ = provider; |
| return S_OK; |
| } |
| |
| HRESULT CGaiaCredentialBase::Terminate() { |
| LOGFN(INFO); |
| SetDeselected(); |
| provider_.Release(); |
| return S_OK; |
| } |
| |
| void CGaiaCredentialBase::TerminateLogonProcess() { |
| // Terminate login UI process if started. This is best effort since it may |
| // have already terminated. |
| if (logon_ui_process_ != INVALID_HANDLE_VALUE) { |
| LOGFN(INFO) << "Attempting to kill logon UI process"; |
| ::TerminateProcess(logon_ui_process_, kUiecKilled); |
| logon_ui_process_ = INVALID_HANDLE_VALUE; |
| } |
| } |
| |
| HRESULT CGaiaCredentialBase::ValidateOrCreateUser(const base::Value& result, |
| BSTR* domain, |
| BSTR* username, |
| BSTR* sid, |
| BSTR* error_text) { |
| LOGFN(INFO); |
| DCHECK(domain); |
| DCHECK(username); |
| DCHECK(sid); |
| DCHECK(error_text); |
| DCHECK(sid); |
| |
| *error_text = nullptr; |
| |
| wchar_t found_username[kWindowsUsernameBufferLength]; |
| wchar_t found_domain[kWindowsDomainBufferLength]; |
| wchar_t found_sid[kWindowsSidBufferLength]; |
| bool is_consumer_account = false; |
| base::string16 gaia_id; |
| HRESULT hr = MakeUsernameForAccount( |
| result, &gaia_id, found_username, base::size(found_username), |
| found_domain, base::size(found_domain), found_sid, base::size(found_sid), |
| &is_consumer_account, error_text); |
| |
| if (FAILED(hr)) { |
| LOGFN(ERROR) << "MakeUsernameForAccount hr=" << putHR(hr); |
| return hr; |
| } |
| |
| // Disallow consumer accounts when mdm enrollment is enabled and the global |
| // flag to allow consumer accounts is not set. |
| if (MdmEnrollmentEnabled() && is_consumer_account) { |
| DWORD allow_consumer_accounts = 0; |
| if (FAILED(GetGlobalFlag(kRegMdmAllowConsumerAccounts, |
| &allow_consumer_accounts)) || |
| allow_consumer_accounts == 0) { |
| LOGFN(ERROR) << "Consumer accounts are not allowed mdm_aca=" |
| << allow_consumer_accounts; |
| *error_text = AllocErrorString(IDS_INVALID_EMAIL_DOMAIN_BASE); |
| return E_FAIL; |
| } |
| } |
| |
| // If an existing user associated to the gaia id was found, make sure that it |
| // is valid for this credential. |
| if (found_sid[0]) { |
| hr = ValidateExistingUser(found_username, found_domain, found_sid, |
| error_text); |
| |
| if (FAILED(hr)) { |
| LOGFN(ERROR) << "ValidateExistingUser hr=" << putHR(hr); |
| return hr; |
| } |
| |
| *username = ::SysAllocString(found_username); |
| *domain = ::SysAllocString(found_domain); |
| *sid = ::SysAllocString(found_sid); |
| |
| return S_OK; |
| } |
| |
| DWORD cpus = 0; |
| provider()->GetUsageScenario(&cpus); |
| |
| // New users creation is not allowed during work station unlock. This code |
| // prevents users from being created when the "Other User" tile appears on the |
| // lock screen through certain system policy settings. In this situation only |
| // the user who locked the computer is allowed to sign in. |
| if (cpus == CPUS_UNLOCK_WORKSTATION) { |
| *error_text = AllocErrorString(IDS_INVALID_UNLOCK_WORKSTATION_USER_BASE); |
| return HRESULT_FROM_WIN32(ERROR_LOGON_TYPE_NOT_GRANTED); |
| // This code prevents users from being created when the "Other User" tile |
| // appears on the sign in scenario and only 1 user is allowed to use this |
| // system. |
| } else if (!CGaiaCredentialProvider::CanNewUsersBeCreated( |
| static_cast<CREDENTIAL_PROVIDER_USAGE_SCENARIO>(cpus))) { |
| *error_text = AllocErrorString(IDS_ADD_USER_DISALLOWED_BASE); |
| return HRESULT_FROM_WIN32(ERROR_LOGON_TYPE_NOT_GRANTED); |
| } |
| |
| base::string16 local_password = GetDictString(result, kKeyPassword); |
| base::string16 local_fullname = GetDictString(result, kKeyFullname); |
| base::string16 comment(GetStringResource(IDS_USER_ACCOUNT_COMMENT_BASE)); |
| hr = CreateNewUser( |
| OSUserManager::Get(), found_username, local_password.c_str(), |
| local_fullname.c_str(), comment.c_str(), |
| /*add_to_users_group=*/true, kMaxUsernameAttempts, username, sid); |
| SecurelyClearString(local_password); |
| |
| // May return user exists if this is the anonymous credential and the maximum |
| // attempts to generate a new username has been reached. |
| if (hr == HRESULT_FROM_WIN32(NERR_UserExists)) { |
| LOGFN(ERROR) << "Could not find a new username based on desired username '" |
| << found_domain << "\\" << found_username |
| << "'. Maximum attempts reached."; |
| *error_text = AllocErrorString(IDS_INTERNAL_ERROR_BASE); |
| return hr; |
| } else if (FAILED(hr)) { |
| LOGFN(ERROR) << "Failed to create user '" << found_domain << "\\" |
| << found_username << "'. hr=" << putHR(hr); |
| *error_text = AllocErrorString(IDS_INTERNAL_ERROR_BASE); |
| return hr; |
| } |
| |
| *domain = ::SysAllocString(found_domain); |
| |
| return hr; |
| } |
| |
| HRESULT CGaiaCredentialBase::OnUserAuthenticated(BSTR authentication_info, |
| BSTR* status_text) { |
| USES_CONVERSION; |
| DCHECK(status_text); |
| |
| // Logon UI process is no longer needed and should already be finished by now |
| // so clear the handle so that calls to HandleAutoLogon do not block further |
| // processing thinking that there is still a logon process active. |
| logon_ui_process_ = INVALID_HANDLE_VALUE; |
| |
| // Convert the string to a base::Dictionary and add the calculated username |
| // to it to be passed to the SaveAccountInfo process. |
| std::string json_string; |
| base::UTF16ToUTF8(OLE2CW(authentication_info), |
| ::SysStringLen(authentication_info), &json_string); |
| |
| base::Optional<base::Value> properties = |
| base::JSONReader::Read(json_string, base::JSON_ALLOW_TRAILING_COMMAS); |
| |
| SecurelyClearString(json_string); |
| json_string.clear(); |
| |
| if (!properties || !properties->is_dict()) { |
| LOGFN(ERROR) << "base::JSONReader::Read failed to translate to JSON"; |
| *status_text = AllocErrorString(IDS_INVALID_UI_RESPONSE_BASE); |
| return E_FAIL; |
| } |
| |
| { |
| base::ScopedClosureRunner zero_dict_on_exit(base::BindOnce( |
| &SecurelyClearDictionaryValue, base::Unretained(&properties))); |
| |
| HRESULT hr = ValidateResult(*properties, status_text); |
| if (FAILED(hr)) { |
| LOGFN(ERROR) << "ValidateResult hr=" << putHR(hr); |
| return hr; |
| } |
| |
| // The value in |dict| is now known to contain everything that is needed |
| // from the GLS. Try to validate the user that wants to sign in and then |
| // add additional information into |dict| as needed. |
| hr = ValidateOrCreateUser(*properties, &domain_, &username_, &user_sid_, |
| status_text); |
| if (FAILED(hr)) { |
| LOGFN(ERROR) << "ValidateOrCreateUser hr=" << putHR(hr); |
| return hr; |
| } |
| |
| base::IgnoreResult(zero_dict_on_exit.Release()); |
| authentication_results_ = std::move(properties); |
| } |
| |
| base::string16 local_password = |
| GetDictString(*authentication_results_, kKeyPassword); |
| password_ = ::SysAllocString(local_password.c_str()); |
| SecurelyClearString(local_password); |
| |
| // Disable the submit button. Either the signon will succeed with the given |
| // credentials or a password update will be needed and that flow will handle |
| // re-enabling the submit button in HandleAutoLogon. |
| if (events_) |
| events_->SetFieldInteractiveState(this, FID_SUBMIT, CPFIS_DISABLED); |
| |
| // Check if the credentials are valid for the user. If they aren't show the |
| // password update prompt and continue without authenticating on the provider. |
| if (!AreCredentialsValid()) { |
| // Change UI into a mode where it expects to have the old password entered. |
| base::string16 old_windows_password; |
| needs_windows_password_ = true; |
| |
| // Pre-fill the old password if possible so that the sign in will proceed to |
| // automatically update the password. |
| if (SUCCEEDED(RecoverWindowsPasswordIfPossible(&old_windows_password))) { |
| current_windows_password_ = |
| ::SysAllocString(old_windows_password.c_str()); |
| SecurelyClearString(old_windows_password); |
| } else { |
| // Fall-through to continue with auto sign in and try the recovered |
| // password. |
| DisplayPasswordField(IDS_PASSWORD_UPDATE_NEEDED_BASE); |
| return S_FALSE; |
| } |
| } |
| |
| result_status_ = STATUS_SUCCESS; |
| |
| // Prevent update of token handle validity until after sign in has completed |
| // so the list of credentials doesn't suddenly change between now and when the |
| // attempt to auto login occurs. |
| PreventDenyAccessUpdate(); |
| |
| // When this function returns, winlogon will be told to logon to the newly |
| // created account. This is important, as the save account info process |
| // can't actually save the info until the user's profile is created, which |
| // happens on first logon. |
| return provider_->OnUserAuthenticated(static_cast<IGaiaCredential*>(this), |
| username_, password_, user_sid_, TRUE); |
| } |
| |
| HRESULT CGaiaCredentialBase::ReportError(LONG status, |
| LONG substatus, |
| BSTR status_text) { |
| USES_CONVERSION; |
| LOGFN(INFO); |
| |
| // Provider may be unset if the GLS process ended as a result of a kill |
| // request coming from Terminate() which would release the |provider_| |
| // reference. |
| if (!provider_) |
| return S_OK; |
| |
| result_status_ = status; |
| |
| // If the user cancelled out of the logon, the process may be already |
| // terminated, but if the handle to the process is still valid the |
| // credential provider will not start a new GLS process when requested so |
| // try to terminate the logon process now and clear the handle. |
| TerminateLogonProcess(); |
| UpdateSubmitButtonInteractiveState(); |
| |
| DisplayErrorInUI(status, STATUS_SUCCESS, status_text); |
| |
| return provider_->OnUserAuthenticated(nullptr, CComBSTR(), CComBSTR(), |
| CComBSTR(), FALSE); |
| } |
| |
| bool CGaiaCredentialBase::UpdateSubmitButtonInteractiveState() { |
| bool should_enable = |
| logon_ui_process_ == INVALID_HANDLE_VALUE && |
| ((!needs_windows_password_ || current_windows_password_.Length()) || |
| (needs_windows_password_ && request_force_password_change_)); |
| if (events_) { |
| events_->SetFieldInteractiveState( |
| this, FID_SUBMIT, should_enable ? CPFIS_NONE : CPFIS_DISABLED); |
| } |
| |
| return should_enable; |
| } |
| |
| void CGaiaCredentialBase::DisplayPasswordField(int password_message) { |
| needs_windows_password_ = true; |
| if (events_) { |
| if (request_force_password_change_) { |
| events_->SetFieldState(this, FID_CURRENT_PASSWORD_FIELD, CPFS_HIDDEN); |
| events_->SetFieldString( |
| this, FID_DESCRIPTION, |
| GetStringResource(IDS_CONFIRM_FORCED_PASSWORD_CHANGE_BASE).c_str()); |
| events_->SetFieldString( |
| this, FID_FORGOT_PASSWORD_LINK, |
| GetStringResource(IDS_ENTER_PASSWORD_LINK_BASE).c_str()); |
| events_->SetFieldSubmitButton(this, FID_SUBMIT, FID_DESCRIPTION); |
| } else { |
| events_->SetFieldString(this, FID_DESCRIPTION, |
| GetStringResource(password_message).c_str()); |
| events_->SetFieldState(this, FID_CURRENT_PASSWORD_FIELD, |
| CPFS_DISPLAY_IN_SELECTED_TILE); |
| // Request force password change wouldn't work on a domain joined |
| // machine as it requires domain admin role privileges to communicate |
| // with the domain controller whereas GCPW only has SYSTEM privilege. |
| if (!OSUserManager::Get()->IsUserDomainJoined(get_sid().m_str)) { |
| events_->SetFieldState(this, FID_FORGOT_PASSWORD_LINK, |
| CPFS_DISPLAY_IN_SELECTED_TILE); |
| events_->SetFieldString( |
| this, FID_FORGOT_PASSWORD_LINK, |
| GetStringResource(IDS_FORGOT_PASSWORD_LINK_BASE).c_str()); |
| } |
| events_->SetFieldInteractiveState(this, FID_CURRENT_PASSWORD_FIELD, |
| CPFIS_FOCUSED); |
| events_->SetFieldSubmitButton(this, FID_SUBMIT, |
| FID_CURRENT_PASSWORD_FIELD); |
| } |
| } |
| } |
| |
| HRESULT CGaiaCredentialBase::ValidateExistingUser( |
| const base::string16& username, |
| const base::string16& domain, |
| const base::string16& sid, |
| BSTR* error_text) { |
| return S_OK; |
| } |
| |
| HRESULT CGaiaCredentialBase::RecoverWindowsPasswordIfPossible( |
| base::string16* recovered_password) { |
| DCHECK(recovered_password); |
| |
| if (!authentication_results_) { |
| LOGFN(ERROR) << "No authentication results found during sign in"; |
| return E_FAIL; |
| } |
| |
| const std::string* access_token = |
| authentication_results_->FindStringKey(kKeyAccessToken); |
| if (!access_token) { |
| LOGFN(ERROR) << "No access token found in authentication results"; |
| return E_FAIL; |
| } |
| |
| return PasswordRecoveryManager::Get()->RecoverWindowsPasswordIfPossible( |
| OLE2CW(get_sid()), *access_token, recovered_password); |
| } |
| |
| } // namespace credential_provider |