blob: 03946f044fafdb5251e5a756a72646cc7d22f1dc [file] [log] [blame]
// 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.
#include "chrome/credential_provider/gaiacp/gaia_credential_provider.h"
#include <iomanip>
#include <map>
#include <utility>
#include "base/files/file_path.h"
#include "base/json/json_reader.h"
#include "base/stl_util.h"
#include "base/strings/string16.h"
#include "base/strings/stringprintf.h"
#include "base/values.h"
#include "chrome/common/chrome_version.h"
#include "chrome/credential_provider/common/gcp_strings.h"
#include "chrome/credential_provider/gaiacp/associated_user_validator.h"
#include "chrome/credential_provider/gaiacp/gaia_credential.h"
#include "chrome/credential_provider/gaiacp/gaia_credential_other_user.h"
#include "chrome/credential_provider/gaiacp/gaia_credential_provider_i.h"
#include "chrome/credential_provider/gaiacp/logging.h"
#include "chrome/credential_provider/gaiacp/mdm_utils.h"
#include "chrome/credential_provider/gaiacp/os_user_manager.h"
#include "chrome/credential_provider/gaiacp/reauth_credential.h"
#include "chrome/credential_provider/gaiacp/reauth_credential_anonymous.h"
#include "chrome/credential_provider/gaiacp/reg_utils.h"
namespace credential_provider {
#define W2CW(p) const_cast<wchar_t*>(p)
static const CREDENTIAL_PROVIDER_FIELD_DESCRIPTOR g_field_desc[] = {
{FID_DESCRIPTION, CPFT_LARGE_TEXT, W2CW(L"Description"), GUID_NULL},
{FID_CURRENT_PASSWORD_FIELD, CPFT_PASSWORD_TEXT, W2CW(L"Windows Password"),
GUID_NULL},
{FID_SUBMIT, CPFT_SUBMIT_BUTTON, W2CW(L"Submit button"), GUID_NULL},
{FID_PROVIDER_LOGO, CPFT_TILE_IMAGE, W2CW(L"Provider logo"),
CPFG_CREDENTIAL_PROVIDER_LOGO},
{FID_PROVIDER_LABEL, CPFT_LARGE_TEXT, W2CW(L"Provider label"),
CPFG_CREDENTIAL_PROVIDER_LABEL},
};
static_assert(base::size(g_field_desc) == FIELD_COUNT,
"g_field_desc does not match FIELDID enum");
namespace {
// Initializes an object that implements IReauthCredential.
HRESULT InitializeReauthCredential(CGaiaCredentialProvider* provider,
const base::string16& sid,
const base::string16& domain,
const base::string16& username,
const CComPtr<IGaiaCredential>& gaia_cred) {
CComPtr<IReauthCredential> reauth;
HRESULT hr = gaia_cred.QueryInterface(&reauth);
if (FAILED(hr)) {
LOG(ERROR) << "Could not get reauth credential interface hr=" << putHR(hr);
return hr;
}
hr = gaia_cred->Initialize(provider);
if (FAILED(hr)) {
LOG(ERROR) << "Could not initialize credential hr=" << putHR(hr);
return hr;
}
// Get the user's email address. If not found, proceed anyway. The net
// effect is that the user will need to enter their email address
// manually instead of it being pre-filled.
wchar_t email[64];
ULONG email_length = base::size(email);
hr = GetUserProperty(sid.c_str(), kUserEmail, email, &email_length);
if (FAILED(hr))
email[0] = 0;
hr = reauth->SetOSUserInfo(CComBSTR(W2COLE(sid.c_str())),
CComBSTR(W2COLE(domain.c_str())),
CComBSTR(W2COLE(username.c_str())));
if (FAILED(hr)) {
LOGFN(ERROR) << "cred->SetOSUserInfo hr=" << putHR(hr);
return hr;
}
if (email[0]) {
hr = reauth->SetEmailForReauth(CComBSTR(email));
if (FAILED(hr))
LOGFN(ERROR) << "reauth->SetEmailForReauth hr=" << putHR(hr);
}
return S_OK;
}
} // namespace
CGaiaCredentialProvider::CGaiaCredentialProvider() {}
CGaiaCredentialProvider::~CGaiaCredentialProvider() {}
HRESULT CGaiaCredentialProvider::FinalConstruct() {
LOGFN(INFO);
CleanupOlderVersions();
return S_OK;
}
void CGaiaCredentialProvider::FinalRelease() {
LOGFN(INFO);
ClearTransient();
// Unlock all the users that had their access locked due to invalid token
// handles.
AssociatedUserValidator::Get()->AllowSigninForUsersWithInvalidTokenHandles();
}
bool CGaiaCredentialProvider::ShouldCreateAnonymousReauthCredential(
bool other_user_credential_exists) {
// If user lockout is not enforced, no need to create anonymous reauth
// credential.
if (!AssociatedUserValidator::Get()->IsUserAccessBlockingEnforced(cpus_))
return false;
// TODO(crbug.com/935695): On domain joined machines, the "Other User" tile
// can appear along with normal user tiles and in this situation we may need
// to create both the other user tile and anonymous reauth credentials.
if (other_user_credential_exists)
return false;
// If the usage scneario is unlock workstation, then we will have two possible
// situations:
// 1. Other user tile only is visible: the condition above handles this case
// for all scenarios.
// 2. The user tile of the user that locked the workstation will appear. This
// user tile is forced by Windows, regardless of what permissions have been
// modified on the user to prevent them from signing in. In this case the
// user tile will be passed to SetUserArray. Since the user array is processed
// before we call this function, the user will already have their reauth
// credential created if needed, and it not, then no further processing should
// be needed because this should be the only user passsed in through
// SetUserArray.
if (cpus_ == CPUS_UNLOCK_WORKSTATION)
return false;
return true;
}
HRESULT CGaiaCredentialProvider::DestroyCredentials() {
LOGFN(INFO);
for (auto it = users_.begin(); it != users_.end(); ++it)
(*it)->Terminate();
users_.clear();
return S_OK;
}
void CGaiaCredentialProvider::ClearTransient() {
LOGFN(INFO);
// Reset event support.
advise_context_ = 0;
events_.Release();
new_user_sid_.Empty();
index_ = std::numeric_limits<size_t>::max();
}
void CGaiaCredentialProvider::CleanupOlderVersions() {
base::FilePath versions_directory = GetInstallDirectory();
if (!versions_directory.empty())
DeleteVersionsExcept(versions_directory, TEXT(CHROME_VERSION_STRING));
}
HRESULT CGaiaCredentialProvider::CreateAnonymousCredentialIfNeeded(
bool showing_other_user) {
CComPtr<IGaiaCredential> cred;
HRESULT hr = E_FAIL;
if (showing_other_user) {
hr = CComCreator<CComObject<COtherUserGaiaCredential>>::CreateInstance(
nullptr, IID_IGaiaCredential, reinterpret_cast<void**>(&cred));
} else if (CanNewUsersBeCreated(cpus_)) {
hr = CComCreator<CComObject<CGaiaCredential>>::CreateInstance(
nullptr, IID_IGaiaCredential, reinterpret_cast<void**>(&cred));
} else {
return S_OK;
}
if (SUCCEEDED(hr)) {
hr = cred->Initialize(this);
if (SUCCEEDED(hr)) {
users_.emplace_back(cred);
} else {
LOG(ERROR) << "Could not create credential hr=" << putHR(hr);
}
}
return hr;
}
HRESULT CGaiaCredentialProvider::CreateReauthCredentials(
ICredentialProviderUserArray* users,
std::set<base::string16>* reauth_sids) {
DCHECK(reauth_sids);
reauth_sids->clear();
std::map<base::string16, std::pair<base::string16, base::string16>>
sid_to_username;
OSUserManager* manager = OSUserManager::Get();
// Get the SIDs of all users being shown in the logon UI.
if (!users) {
LOGFN(ERROR) << "hr=" << putHR(E_INVALIDARG);
return E_INVALIDARG;
}
HRESULT hr = users->SetProviderFilter(Identity_LocalUserProvider);
if (FAILED(hr)) {
LOGFN(ERROR) << "users->SetProviderFilter hr=" << putHR(hr);
return hr;
}
DWORD count;
hr = users->GetCount(&count);
if (FAILED(hr)) {
LOGFN(ERROR) << "users->GetCount hr=" << putHR(hr);
return hr;
}
LOGFN(INFO) << "count=" << count;
for (DWORD i = 0; i < count; ++i) {
CComPtr<ICredentialProviderUser> user;
hr = users->GetAt(i, &user);
if (FAILED(hr)) {
LOGFN(ERROR) << "users->GetAt hr=" << putHR(hr);
return hr;
}
wchar_t* sid_buffer = nullptr;
hr = user->GetSid(&sid_buffer);
if (FAILED(hr)) {
LOGFN(ERROR) << "user->GetSid hr=" << putHR(hr);
continue;
}
base::string16 sid = sid_buffer;
::CoTaskMemFree(sid_buffer);
wchar_t username[kWindowsUsernameBufferLength];
wchar_t domain[kWindowsDomainBufferLength];
hr = manager->FindUserBySID(sid.c_str(), username, base::size(username),
domain, base::size(domain));
if (FAILED(hr)) {
LOGFN(ERROR) << "Can't get sid or username hr=" << putHR(hr);
continue;
}
// If the token handle is valid, no need to create a reauth credential.
// The user can just sign in using their password.
if (AssociatedUserValidator::Get()->IsTokenHandleValidForUser(sid))
continue;
CComPtr<IGaiaCredential> cred;
HRESULT hr = CComCreator<CComObject<CReauthCredential>>::CreateInstance(
nullptr, IID_IGaiaCredential, reinterpret_cast<void**>(&cred));
if (FAILED(hr)) {
LOG(ERROR) << "Could not create credential hr=" << putHR(hr);
return hr;
}
hr = InitializeReauthCredential(this, sid, domain, username, cred);
if (FAILED(hr)) {
LOG(ERROR) << "InitializeReauthCredential hr=" << putHR(hr);
return hr;
}
reauth_sids->insert(sid);
users_.emplace_back(cred);
}
return S_OK;
}
HRESULT CGaiaCredentialProvider::CreateAnonymousReauthCredentialsIfNeeded(
bool showing_other_user,
const std::set<base::string16>& reauth_sids) {
if (!ShouldCreateAnonymousReauthCredential(showing_other_user))
return S_OK;
std::set<base::string16> associated_sids =
AssociatedUserValidator::Get()->GetUpdatedAssociatedSids();
OSUserManager* manager = OSUserManager::Get();
for (auto& associated_sid : associated_sids) {
// Edge case: If the user locking the machine started sign in screen with a
// valid token handle, then their token handle gets invalidated and then the
// sign in screen goes to sleep. On the next call to SetUserArray here the
// user array will still contain the user that is now invalidated even
// though the user's permissions have been changed and they are no longer
// able to sign in with just their normal credentials. It appears that
// Windows does not update the user array after the first initial start up
// of LoginUI. So to cover that case here, if we see that we already
// created a reauth for the user, just skip it.
// TODO(crbug.com/935697).
if (reauth_sids.find(associated_sid) != reauth_sids.end())
continue;
if (AssociatedUserValidator::Get()->IsTokenHandleValidForUser(
associated_sid))
continue;
wchar_t username[kWindowsUsernameBufferLength];
wchar_t domain[kWindowsDomainBufferLength];
HRESULT hr = manager->FindUserBySID(associated_sid.c_str(), username,
base::size(username), domain,
base::size(domain));
if (FAILED(hr)) {
LOGFN(ERROR) << "FindUserBySID hr=" << putHR(hr);
continue;
}
CComPtr<IGaiaCredential> cred;
hr = CComCreator<CComObject<CReauthCredentialAnonymous>>::CreateInstance(
nullptr, IID_IGaiaCredential, reinterpret_cast<void**>(&cred));
if (FAILED(hr)) {
LOG(ERROR) << "Could not create credential hr=" << putHR(hr);
return hr;
}
hr = InitializeReauthCredential(this, associated_sid, domain, username,
cred);
if (FAILED(hr)) {
LOG(ERROR) << "InitializeReauthCredential hr=" << putHR(hr);
return hr;
}
users_.emplace(users_.begin(), cred);
}
return S_OK;
}
// Static.
bool CGaiaCredentialProvider::IsUsageScenarioSupported(
CREDENTIAL_PROVIDER_USAGE_SCENARIO cpus) {
return cpus == CPUS_LOGON || cpus == CPUS_UNLOCK_WORKSTATION;
}
// Static.
bool CGaiaCredentialProvider::CanNewUsersBeCreated(
CREDENTIAL_PROVIDER_USAGE_SCENARIO cpus) {
// When in an unlock usage, only the user that locked the computer will be
// allowed to sign in, so no new users can be added.
if (cpus == CPUS_UNLOCK_WORKSTATION)
return false;
// If MDM enrollment is required and multiple users is not supported, only
// allow a new associated user to be created if there does not yet exist an
// OS user created from a Google account.
if (MdmEnrollmentEnabled()) {
DWORD multi_user_supported = 0;
HRESULT hr = GetGlobalFlag(kRegMdmSupportsMultiUser, &multi_user_supported);
if (FAILED(hr) || multi_user_supported == 0) {
if (AssociatedUserValidator::Get()->GetAssociatedUsersCount() > 0)
return false;
}
}
return true;
}
// IGaiaCredentialProvider ////////////////////////////////////////////////////
HRESULT CGaiaCredentialProvider::GetUsageScenario(DWORD* cpus) {
DCHECK(cpus);
*cpus = static_cast<DWORD>(cpus_);
return S_OK;
}
HRESULT CGaiaCredentialProvider::OnUserAuthenticated(
IUnknown* credential,
BSTR /*username*/,
BSTR /*password*/,
BSTR sid,
BOOL fire_credentials_changed) {
DCHECK(!credential || sid);
// |credential| should be in the |users_|. Find its index.
index_ = std::numeric_limits<size_t>::max();
for (size_t i = 0; i < users_.size(); ++i) {
if (users_[i].IsEqualObject(credential)) {
index_ = i;
break;
}
}
if (index_ == std::numeric_limits<size_t>::max()) {
LOGFN(INFO) << "Could not find credential";
return E_INVALIDARG;
}
new_user_sid_ = sid;
// Tell winlogon.exe that credential info has changed. This provider will
// make the newly created user the default login credential with auto
// logon enabled. See GetCredentialCount() for more details.
HRESULT hr = S_OK;
if (events_ && fire_credentials_changed)
hr = events_->CredentialsChanged(advise_context_);
LOGFN(INFO) << "hr=" << putHR(hr) << " sid=" << new_user_sid_.m_str
<< " index=" << index_;
return hr;
}
// ICredentialProviderSetUserArray ////////////////////////////////////////////
HRESULT CGaiaCredentialProvider::SetUserArray(
ICredentialProviderUserArray* users) {
LOGFN(INFO);
if (!IsUsageScenarioSupported(cpus_))
return S_OK;
if (users_.size() > 0) {
LOG(ERROR) << "Users should be empty";
return E_UNEXPECTED;
}
CREDENTIAL_PROVIDER_ACCOUNT_OPTIONS options;
HRESULT hr = users->GetAccountOptions(&options);
bool showing_other_user =
SUCCEEDED(hr) && options != CPAO_EMPTY_CONNECTED && options != CPAO_NONE;
hr = CreateAnonymousCredentialIfNeeded(showing_other_user);
if (FAILED(hr))
LOG(ERROR) << "Could not create anonymous credential hr=" << putHR(hr);
std::set<base::string16> reauth_sids;
hr = CreateReauthCredentials(users, &reauth_sids);
if (FAILED(hr)) {
LOG(ERROR) << "CreateReauthCredentials hr=" << putHR(hr);
return hr;
}
hr =
CreateAnonymousReauthCredentialsIfNeeded(showing_other_user, reauth_sids);
if (FAILED(hr)) {
LOG(ERROR) << "CreateAnonymousReauthCredentialsIfNeeded hr=" << putHR(hr);
return hr;
}
return S_OK;
}
// ICredentialProvider ////////////////////////////////////////////////////////
HRESULT CGaiaCredentialProvider::SetUsageScenario(
CREDENTIAL_PROVIDER_USAGE_SCENARIO cpus,
DWORD flags) {
ClearTransient();
cpus_ = cpus;
cpus_flags_ = flags;
LOGFN(INFO) << " cpu=" << cpus << " flags=" << std::setbase(16) << flags;
return IsUsageScenarioSupported(cpus_) ? S_OK : E_NOTIMPL;
}
HRESULT CGaiaCredentialProvider::SetSerialization(
const CREDENTIAL_PROVIDER_CREDENTIAL_SERIALIZATION* pcpcs) {
DCHECK(pcpcs);
// NOTE: we only need to support this method if we want to support the
// CredUI or support remote login with gaia creds. We are likely to want
// to support both, but not for prototype.
if (pcpcs->clsidCredentialProvider == CLSID_GaiaCredentialProvider) {
// Unmarshall the serialized buffer and fill in partial fields as needed.
// Examine cpusflags for things like admin-only, cred-in-only, etc.
LOGFN(INFO) << " authpkg=" << pcpcs->ulAuthenticationPackage
<< " setsize=" << pcpcs->cbSerialization;
}
return S_OK;
}
HRESULT CGaiaCredentialProvider::Advise(ICredentialProviderEvents* pcpe,
UINT_PTR context) {
DCHECK(pcpe);
bool had_previous = events_.p != nullptr;
events_ = pcpe;
advise_context_ = context;
LOGFN(INFO) << " had=" << had_previous;
return S_OK;
}
HRESULT CGaiaCredentialProvider::UnAdvise() {
ClearTransient();
HRESULT hr = DestroyCredentials();
LOGFN(INFO) << "hr=" << putHR(hr);
// Delete the startup sentinel file if any still exists. It can still exist
// in 2 cases:
// 1. The UnAdvise should only occur after the user has logged in, so if
// they never selected any gaia credential and just used normal credentials
// this function will be called in that situation and it is guaranteed that
// the user has at least been able provide some input to winlogon.
// 2. When no usage scenario is supported, none of the credentials will be
// selected and thus the gcpw startup sentinel file will not be deleted.
// So in the case where the user is asked for CPUS_CRED_UI enough times,
// the sentinel file size will keep growing without being deleted and
// eventually GCPW will be disabled completely. In the unsupported usage
// scenario, FinalRelease will be called shortly after SetUsageScenario
// if the function returns E_NOTIMPL so try to catch potential crashes
// of the destruction of the provider when it is not used because
// crashes in this case will prevent the cred ui from coming up and not
// allow the user to access their desired resource.
DeleteStartupSentinel();
return S_OK;
}
HRESULT CGaiaCredentialProvider::GetFieldDescriptorCount(DWORD* count) {
*count = FIELD_COUNT;
return S_OK;
}
HRESULT CGaiaCredentialProvider::GetFieldDescriptorAt(
DWORD index,
CREDENTIAL_PROVIDER_FIELD_DESCRIPTOR** ppcpfd) {
*ppcpfd = nullptr;
HRESULT hr = E_INVALIDARG;
if (index < FIELD_COUNT) {
// Always return a CoTask copy of the structure as well as any strings
// pointed to by that structure.
*ppcpfd = reinterpret_cast<CREDENTIAL_PROVIDER_FIELD_DESCRIPTOR*>(
::CoTaskMemAlloc(sizeof(**ppcpfd)));
if (*ppcpfd) {
**ppcpfd = g_field_desc[index];
if ((*ppcpfd)->pszLabel) {
hr = ::SHStrDupW((*ppcpfd)->pszLabel, &(*ppcpfd)->pszLabel);
} else {
(*ppcpfd)->pszLabel = nullptr;
hr = S_OK;
}
}
if (FAILED(hr)) {
::CoTaskMemFree(*ppcpfd);
*ppcpfd = nullptr;
}
}
return hr;
}
HRESULT CGaiaCredentialProvider::GetCredentialCount(
DWORD* count,
DWORD* default_index,
BOOL* autologin_with_default) {
// NOTE: assumes SetUserArray() is called before this.
*count = users_.size();
*default_index = CREDENTIAL_PROVIDER_NO_DEFAULT;
*autologin_with_default = false;
// If a user was authenticated, winlogon was notified of credentials changes
// and is re-enumerating the credentials. Make sure autologin is enabled.
if (index_ < users_.size() && new_user_sid_.Length() > 0) {
*default_index = index_;
*autologin_with_default = true;
}
LOGFN(INFO) << " count=" << *count << " default=" << *default_index
<< " auto=" << *autologin_with_default;
return S_OK;
}
HRESULT CGaiaCredentialProvider::GetCredentialAt(
DWORD index,
ICredentialProviderCredential** ppcpc) {
HRESULT hr = E_INVALIDARG;
if (!ppcpc || index >= users_.size()) {
LOG(ERROR) << "hr=" << putHR(hr) << " index=" << index;
return hr;
}
*ppcpc = nullptr;
hr = users_[index]->QueryInterface(IID_ICredentialProviderCredential,
(void**)ppcpc);
LOGFN(INFO) << "hr=" << putHR(hr) << " index=" << index;
return hr;
}
} // namespace credential_provider