I have developed custom authentication package that I would like to use for interactive logon. It creates the access token in the LsaApUserLogon function.
When I call LsaUserLogon from an application I can enumerate new user sessions, but when I used it for logon (also I have created a custom credential provider) I can see in Windows Event log that I was successfully logged in and then logged out.
When I select my specific Credential and try to logon, it enters into LsaApLogonUser API of my Authentication Package. If I check log file, LsaApLogonUser return STATUS_SUCCESS. But Windows is not logged on. After leaving LsaAPLogonUser, LSA calls LsaApLogonTerminated API and back LogonUI.
When I prepared the TokenInformation I got LookupPrivilegeValueW failed for the SeInteractiveLogonRight. I don't know if this is important for logon.
LsaApLogonUser(...){
......
// NetUserGetInfo
// AllocateLocallyUniqueId (LogonId)
err = GetTokenInformationv2(pdi?pdi->DomainControllerName:NULL,wszDomain,wszUser,&LocalTokenInformation,LogonId);
err = g_pSec->CreateLogonSession(LogonId);
if(ProfileBuffer)
{
*ProfileBuffer=NULL;
*ProfileBufferLength=0;
}
(*TokenInformationType)=LsaTokenInformationV2;
(*TokenInformation)=LocalTokenInformation;
return STATUS_SUCCESS;
}
GetTokenInformationv2(...){
....
....
// Call LsaEnumerateAccountRights
// check LookupPrivilegeValueW // It failed for "SeInteractiveLogonRight"
//
return STATUS_SUCCESS;
}
Is ProfileBuffer important for logon? I don't know why LSA cannot logon.
The documentation does not say that the profile buffer can be set to NULL and it seems that it is indeed mandatory. The OP reports that allocating and returning a profile buffer (just a single byte was enough) resolved the problem.
The error when attempting to retrieve a LUID for SeInteractiveLogonRight was not relevant; the user's logon rights do not need to be included in the TOKEN_PRIVILEGES structure, so no LUID is needed, and as documented, the LookupPrivilegeValue function only accepts privileges:
The LookupPrivilegeValue function supports only the privileges specified in the Defined Privileges section of Winnt.h.
(Note that the relevant section of winnt.h only contains definitions for SeXxxPrivilege; the definitions for SeXxxLogonRight are in ntsecapi.h.)
Related
I've started to investigate how credential providers work, but due to poor documentation I got stuck in couple of places.
I have console application(sort of like ssh server) when I connect remotly to this application and authenticate(none is logged on), I want it to be able to forward those credentials to credential provider and I want CP to login user(create session).
I've also noticed that In documentation it says that SetSerialization method always gets called after SetusageScenario. But I've added logs and SetSerialization never gets called after SetusageScenario.
As I know SetSerialization is called when credential is provided by application. I can't understand what does it mean to provide credential through application? It seems like this is what I need, to forward credentials from application to credential provider, but How can I do that? Are there some functions that I could call from credential provider dll to provoke login?
Is there any way I can achieve this? I've read that when remote login is done, UpdateRemoteCredential is called, But As I understand(correct me if I'm wrong) in remote login it means RDP but my application is simple, it just listens to some port and gets username and password and then uses Logonuser function.
A program named MxLogon2 implements this scheme. A remote server with MxLogon2 installed can authenticate a USB key(But I want username/password) connected to the client side in a remote desktop session.
Any suggestion that would direct me to right way would be greatly appriciated.
P.S
I'm using pGina as my credential provider.
I wouldn't mind too much of SetSerialization in your case. This is used for pre-initializing tile.
One example would be, with RDP connection, you can store certificates of credentials on your system. This function is used to pre-inquire tiles with those credentials (not the only solution though).
Make it return E_NOTIMPL in your case.
The main purpose of the provider is to initialize Credentials and tiles :
Credentials initialization
is done through SetUsageScenario function (automatically called after Provider construction).
In there, you can customize the behavior depending on scenario you are in. Most basic one being CPUS_LOGON.
It shall look like this (this is only a preview)
HRESULT Provider::SetUsageScenario(
__in CREDENTIAL_PROVIDER_USAGE_SCENARIO cpus,
__in DWORD dwFlags
)
{
HRESULT hr;
switch(cpus)
{
case CPUS_UNLOCK_WORKSTATION:
case CPUS_LOGON:
hr = this->_InitCredential();
break;
default:
hr = E_INVALIDARG;
break;
}
return hr;
}
Tiles initialization
begins in GetCredentialCount function. It allows you to define how many tiles you gonna show (pdwCount out parameter), as well as the one to be used as the default tile (pdwDefault).
Auto-Logon is also handle in here (through pbAutoLogonWithDefault).
Tiles Enumeration is then beginning in GetCredentialAt function.
Passing credentials for authentication is done in the credential you're using through the GetSerialization function.
Examples of implementation can be found here.
I hardly insist on example as it easily divert depending on what you are trying to achieve. This is very flexible and you'd better understand the purpose of any function / parameter to fit your goals.
EDIT - cf comments :
#define IDCredential 0x1
Credential::Credential(Provider* parent)
{
//Do ssh connection or whatever
if(success)
{
parent->login = retrievedLogin;
parent->pwd = retrievedPwd;
parent->CredentialUsed = IDCredential;
parent->autoLogin = true;
parent->ProviderEvents->CredentialsChanged(AdviseContext);
}
}
HRESULT Provider::GetCredentialCount(
__out DWORD* pdwCount,
__out_range(<,*pdwCount) DWORD* pdwDefault,
__out BOOL* pbAutoLogonWithDefault
)
{
if(this->autoLogin && this->CredentialUsed)
{
*pbAutoLogonWithDefault = true; // --> Instant call to GetSerialization
*pdwDefault = this->CredentialUsed; //index of the tile
}
else
{
*pbAutoLogonWithDefault = false;
*pdwDefault = IDCredential;
}
return S_OK;
}
I have developed a custom credential provider for MFA and it is working fine. I am performing my additional authentication checks in GetSerialization() method by calling REST APIs. Now, in case of successful validation through REST API, I am allowing the user to login. But for the case of validation failure, I want to show error screen, like the one default provider shows in case of incorrect username or password (having a button for 'Ok'). I have understood that this is done in ReportResult() by default credential provider in case of logon failure. Can I call ReportResult() to display my custom error message with an 'Ok' button?
Regards,
EDIT: I am doing it like this:
if (SUCCEEDED(HRESULT_FROM_NT(ntsStatus)) && SUCCEEDED(HRESULT_FROM_NT(ntsSubstatus)))
{
SHStrDupW(L"Bad password", ppwszOptionalStatusText);
*pcpsiOptionalStatusIcon = CPSI_ERROR;
}
But the screen isn't stopping. It logs in the user. I need to detect the scenario when the windows credentials are correct but the API call fails, so I need to catch that status and display error
Have to try
*pcpgsr = CPGSR_NO_CREDENTIAL_NOT_FINISHED;
In my use case wih additional
*pcpsiOptionalStatusIcon = CPSI_WARNING;
it is enough to display status message.
ReportResult(
NTSTATUS ntsStatus,
NTSTATUS ntsSubstatus,
PWSTR* ppwszOptionalStatusText,
CREDENTIAL_PROVIDER_STATUS_ICON* pcpsiOptionalStatusIcon
)
You can set your own custom message by populating ppwszOptionalStatusText. If needed, icon can also be set pcpsiOptionalStatusIcon to one of CPSI_ERROR,CPSI_WARNING or CPSI_SUCCESS.
For eg., after checking on ntsStatus, you could customize like this
SHStrDupW(L"Bad password", ppwszOptionalStatusText);
*pcpsiOptionalStatusIcon = CPSI_ERROR;
Edit:
Inside GetSerialization(), once the API returns failure, set the CREDENTIAL_PROVIDER_GET_SERIALIZATION_RESPONSE to not finished. Like this
*pcpgsr = CPGSR_RETURN_NO_CREDENTIAL_FINISHED;
This should stop the logon to continue.
Edit 2:
As per #js.hrt comment below, to show the status screen from GetSerialization, by using *pcpgsr = CPGSR_NO_CREDENTIAL_NOT_FINISHED; SHStrDupW(L"Bad password", ppwszOptionalStatusText); *pcpsiOptionalStatusIcon = CPSI_ERROR; in GetSerialization() without using report result.
I am trying to read a credential from a smart card on one workstation (local), then send it to a different workstation(remote) and use it to logon to that workstation from within a credential provider.
I've been searching and, and researching for weeks now but have not found anyone showing or telling how to do this.
I have found several references to an excellent work at https://www.idrix.fr/Root/Samples/LsaSmartCardLogon2.cpp, which is working code for a smart card credential provider, but unfortunately requires that the credential provider have direct access to the smart card with the credential on it.
The actual code I am working with seems a little to long to include in the post so I thought I try asking the question using sudo code.
On the local workstation:
MySendCredentialsFromSmartCard(){
CryptAcquireContext(hProv, containerName, cardName, PROV_RSA_FULL, CRYPT_SILENT)
CryptGetUserKey(*hProv, AT_KEYEXCHANGE, &hCryptKey);
CryptGetKeyParam(hCryptKey, KP_CERTIFICATE, encodedCert, &size, 0);
MySendCertToRemote(encodedCert,size);
}
On Remote workstation running in custom credential provider:
HRESULT CSampleCredential::GetSerialization(
_Out_ CREDENTIAL_PROVIDER_GET_SERIALIZATION_RESPONSE *pcpgsr,
_Out_ CREDENTIAL_PROVIDER_CREDENTIAL_SERIALIZATION *pcpcs,
_Outptr_result_maybenull_ PWSTR *ppwszOptionalStatusText,
_Out_ CREDENTIAL_PROVIDER_STATUS_ICON *pcpsiOptionalStatusIcon){
MyReadCertFromRemote(&cert,&size);
CryptAcquireContext(&hCryptProv, NULL, NULL, PROV_RSA_FULL, 0);
CryptCreateHash(hCryptProv, CALG_SHA1, NULL, 0, &hHash);
CryptHashData(hHash, (BYTE *)cert, (DWORD)size, 0);
CERT_CREDENTIAL_INFO certInfo;
CryptGetHashParam(hHash, HP_HASHVAL, certInfo.rgbHashOfCert, &dwHashLen, 0);
CredMarshalCredential(CertCredential, &certInfo, marshalledCred);
/****************************for debug only*****************************/
LogonUser((*marshalledCred, NULL, NULL, LOGON32_LOGON_NEW_CREDENTIALS, LOGON32_PROVIDER_WINNT50, &userHandle);
//this reuturns 1 (success)
/**************************************************************************/
KERB_INTERACTIVE_UNLOCK_LOGON kiul;
CREDENTIAL_PROVIDER_USAGE_SCENARIO cpus
KerbInteractiveUnlockLogonInit(L"", (PWSTR)marshaledCred, L"", cpus, &kiul);//(note:I've tried this with the third param set to the card's pin)
KerbInteractiveUnlockLogonPack(kiul, &pcpcs->rgbSerialization, &pcpcs->cbSerialization);
RetrieveNegotiateAuthPackage(&ulAuthPackage);
pcpcs->ulAuthenticationPackage = ulAuthPackage;
pcpcs->clsidCredentialProvider = CLSID_OfProvider;
*pcpgsr = CPGSR_RETURN_CREDENTIAL_FINISHED;
}
LogonUI.exe displays "A specified logon session does not exist. It may already have been terminated." This seams to correspond to Windows error code 1312. It seems that most developers struggling with this error code are having trouble configuring IIS with an SSL cert. I have found no reference to this error in relation to a credential provider.
Various threads seem to indicate that LogonUser is mearly a wrapper around LSALogonUser and the returned values from ICredentialProviderCredential::GetSerialization will be passed to LSALogonUser. The fact that my call to LogonUser with the marshalled credential succeeds would seem to suggest that on I'm on the right track, but I haven't been able to verify that nor figure out what I'm missing.
I would like to try to avoid solutions that require storing the credential on the remote workstation as it is typically a domain admin credential.
Any ideas anybody? Any advise on how to proceed?
Thanks
I guess you can do it using a communication mechanism where you send your data from one workstation to another one and in the remote one you serialize your data in SetSerialization() instead of GetSerialization(), preparing all the fields and processing the logon.
From my understanding point of you, the Provider class (the one inherited by ICredentialProvider) has to wait for those credentials somehow and pass them to SetSerialization
I am having a hard time migrating a C++ CryptoAPI-based application that currently runs on Windows Server 2008 to Windows 8.1. The scenario is:
This application is eventually triggered by WatchDog.exe, which in its turn is triggered when the computer is started by Windows' Task Scheduler.
Task Scheduler uses the following rules to start the WatchDog.exe:
A Administrator User Account;
Run Whether user is logged on or not;
UNCHECKED: Do not store password. The task will only have access to
local resources;
Run with Highest Privileges;
Configure for Win 8.1;
Triggered at system startup.
The server sits there, nobody logged, until in a given scenario WatchDog.exe starts the application. Application log confirms that the owner of the process (GetUserName) is the very same user Task Scheduler used to trigger WatchDog.exe.
It turns out that this application works fine in Windows Server 2008, but in windows 8.1 a call to CryptAcquireContext fails with return code ERROR_FILE_NOT_FOUND (2L). The odd thing is that the application will NOT fail if, when started, the user is physically logged onthe machine, although it was not the user who started the application manually.
I took a look at the documentation and found:
"The profile of the user is not loaded and cannot be found. This
happens when the application impersonates a user, for example, the
IUSR_ComputerName account."
I had never heard of impersonification, so I made a research and found the APIs LogonUser, ImpersonateLoggedOnUser and RevertToSelf. I then updated the application in this way:
...
HANDLE hToken;
if (! LogonUser(L"admin", L".", L"XXXXXXXX", LOGON32_LOGON_BATCH, LOGON32_PROVIDER_DEFAULT, &hToken))
{
logger->log (_T("Error logging on."));
}
else
{
logger->log (PMLOG_LEVEL_TRACE, _T("Logged on."));
if (! ImpersonateLoggedOnUser(hToken))
{
logger->log (_T("Error impersonating."));
}
else
{
logger->log (_T("Impersonated."));
err = XXXXXXXXX(); // calls function which will execute CryptAcquireContext
if (! RevertToSelf())
{
logger->log (_T("Error reverting."));
}
else
{
logger->log (_T("Reverted."));
}
}
}
...
Excerpt with the call to CryptAcquireContext:
...
//---------------------------------------------------------------
// Get the handle to the default provider.
if(! CryptAcquireContext(&hCryptProv, cryptContainerName, MS_ENHANCED_PROV, PROV_RSA_FULL, 0))
{
DWORD e = GetLastError();
_stprintf_s (logMsg, 1000, _T("Error %ld acquiring cryptographic provider."), e);
cRSALogger->log (logMsg);
return ERR_CCRYPT_NO_KEY_CONTAINER;
}
cRSALogger->log (_T("Cryptographic provider acquired."));
...
As the result, I got the log:
...
[2015/01/08 20:53:25-TRACE] Logged on.
[2015/01/08 20:53:25-TRACE] Impersonated.
...
[2015/01/08 20:53:26-ERROR] Error 2 acquiring cryptographic provider.
...
[2015/01/08 20:53:26-TRACE] Reverted.
...
That seems to show that impersonation is working properly, but still I get Error 2 (ERROR_FILE_NOT_FOUND) on CryptAcquireContext.
Summary:
On Windows Server 2008, the very same application runs properly even
without the calls to LogonUser/Impersonate/Revert.
On Windows 8.1, the application, with or without the calls to
LogonUser/Impersonate/Revert, will only work properly if the user is logged on (which is not acceptable).
Any thoughts where I can run to in order to get this working on windows 8.1?
Thank in advance,
Dan
I'm running the following piece of code from a local service application. The purpose is to obtain the path to a user's profile before calling LoadUserProfile() to load that user's profile before calling CreateProcessAsUser() to run a user-mode process on behalf of that user.
Note that this question is not about LoadUserProfile(), or CreateProcessAsUser().
What happens is this. When the code below is run on Windows XP w/SP3 that is a part of the Active Directory domain, with a single user logged in via a local console (that user's session ID is used below) the NetUserGetInfo() API fails. Also note that it works fine in any other circumstance:
//'dwSessID' = session ID of the user to retrieve a user profile path for
LPTSTR pUserName = NULL;
DWORD dwcbSzUserName = 0;
if(!WTSQuerySessionInformation(WTS_CURRENT_SERVER_HANDLE, dwSessID, WTSUserName, &pUserName, &dwcbSzUserName))
{
//Error
return false;
}
USER_INFO_4* pUI4 = NULL;
DWORD dwNetStatus;
if((dwNetStatus = NetUserGetInfo(NULL, pUserName, 4, (BYTE**)&pUI4)) == NERR_Success)
{
PROFILEINFO pfi = {0};
pfi.dwSize = sizeof(pfi);
pfi.lpUserName = pUserName;
pfi.dwFlags = PI_NOUI;
pfi.lpProfilePath = pUI4->usri4_profile;
LoadUserProfile(hToken, &pfi);
//And so on
}
else
{
//On that specific machine I get here with 'dwNetStatus' = 2221,
//or NERR_UserNotFound, that according to MSDN is
//"The user name could not be found."
//Also note that GetLastError is not used for this API.
}
Can some suggest why can NetUserGetInfo() fail on that particular machine, and how to fix this code?
PS. I know that MSDN for NetUserGetInfo states that there might be issues with a ACL on Active Directory domain, but it doesn't specify how to set one...
If I read the documentation for NetUserGetInfo, for the information level of the data you code 4 . It's written Level 4 Return detailed information and additional attributes about the user account. This level is valid only on servers. As far as I understand it's not your case. Do you verify the value of pUserName returned by WTSQuerySessionInformation.
As JPBlanc stated NetUserGetInfo with level 4 is valid only on servers.
Another problem is that you retrieve the name of the logged on user, but not the domain the user belongs to.
Noticed you are calling NetUserGetInfo with pUserName the type of LPTSTR.
Sometimes it won't work (if you will compile your project to use ANSII strings by default).
Consider changing you string types to LPWSTR.