Open giuliohome opened 3 years ago
I think I have to call ImpersonateLoggedOnUser with the with sso.user.accessToken
in input.
Will try (if I find an example for node.js: this package export appears to be a good fit, I'm going to install it) and close the issue, if it works. Thanks
I'm getting
0|node.js express sso | TypeError: 'handle' should be an External returned from logonUser()
0|node.js express sso | at F:\Apps\ng\angular-sso-example\back\src\server.ts:62:3
for this code
const { impersonateLoggedOnUser, revertToSelf} = require('F:\\Apps\\ng\\angular-sso-example\\back\\build\\Release\\users.node');
const accessToken = req?.session?.sso?.user?.accessToken;
impersonateLoggedOnUser(accessToken);
The two libraries have different ways to treat the handles. I think I have to deserialize the handle from this utility in such a way that is expected from here...
edit
Ehm.... this is what you use to retrieve a HANDLE from a string, I'll try to modify the other lib users.cc accordingly...
Now the access token is correctly transformed into a napi value and viceversa (code below), but still ImpersonateLoggedOnUser throws invalid handle.
std::string p2s(void *ptr) {
std::stringstream s;
s << "0x" << std::setfill('0') << std::setw(sizeof(ULONG_PTR) * 2) << std::hex
<< ptr;
std::string result = s.str();
return result;
}
Value impersonateLoggedOnUser(CallbackInfo const& info) {
auto env = info.Env();
// return info[0];
HANDLE token =
s2p(info[0].As<Napi::String>().Utf8Value());
Value ret_handle = External<void>::New(env, token, [](Env env, HANDLE handle) {
CloseHandle(handle);
});
auto handle = get_handle(env,
ret_handle
);
if (!ImpersonateLoggedOnUser(handle)) {
throw createWindowsError(env, GetLastError(), "ImpersonateLoggedOnUser");
}
//std::string str = p2s(handle);
//return Napi::String::New(env, str);
return ret_handle;
}
I have recompiled the library to have access to the private server context handle. So now I can run again all the sequence
const getServerHandle = (req : any) => req?.session?.sso?.serverContextHandle
const input = getServerHandle(req);
sspi.ImpersonateSecurityContext(input);
const new_access_token = sspi.OpenThreadToken();
impersonateLoggedOnUser(new_access_token);
and in this case I'm getting a different error: 'Access is denied.'
asked on SO, but it has been closed.
In conclusion, the results of my tests of reusing serverContextHandle and OpenThreadToken are
Unfortunately also MS SQL windows authentication fails because it sees the process owner user and not the impersonated SSPI token. Sharing this last commit for future reference and review.
I doubt that other debug details are needed. All this boils down to saying that Kerberos ticket is only valid to authenticate Alice to Bob but can't be used for Bob to impersonate Alice (typically for windows authentication on ms sql server). I only see 2 possible answers: either 1) yes, it's by design (hopefully that is true IMO) or 2) no, one could be able to use Kerberos ticket to impersonateLoggedOnUser
or to connect via windows auth to a SQL Server and the like... In either case it can be answered, in principle, without further details about my debug environment. My conclusion written here.
Solved with this code!
In the context of SSPI Kerberos access token (typically from single sign on), the access denied from impersonateLoggedOnUser
is solved in C++ by setting DWORD flags = MAXIMUM_ALLOWED;
in OpenThreadToken
. At that point ImpersonateLoggedOnUser
will accept the returned token (without errors like access denied) and Kerberos single sign on impersonation can be achieved via CreateProcessAsUser
. However the impersonation is not possible with an elevated user, I think... (and for sure refresh group policies with 'allow logon')
Will post a PR
#include "../../misc.h"
#include <fstream>
namespace myAddon {
// Proof of concept as Kerberos SSPI impersonated user
void testImpersponation(HANDLE userToken) {
// Create and open a text file
std::ofstream MyFile("test_SSPI.bat");
// Write to the file
MyFile << "whoami > whoami.txt";
// Close the file
MyFile.close(); // check if file owner is the impersonated user
STARTUPINFO si = { sizeof(STARTUPINFO) };
PROCESS_INFORMATION pi = {0};
wchar_t wszCommand[]=L"cmd.exe /C test_SSPI.bat";
/* Unicode version of CreateProcess modifies its command parameter... Ansi doesn't.
Apparently this is not classed as a bug ???? */
if(!CreateProcessAsUser(userToken,NULL,wszCommand,NULL,NULL,FALSE,CREATE_NEW_CONSOLE,NULL,NULL,&si,&pi))
{
//CloseHandle(hToken);
fprintf(stderr,"CreateProcess returned error %d\n",GetLastError());
return;
}
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread)
}
void e_ImpersonateSecurityContext(const Napi::CallbackInfo &info) {
Napi::Env env = info.Env();
if (info.Length() < 1) {
throw Napi::Error::New(
env,
"ImpersonateSecurityContext: Wrong number of arguments. "
"ImpersonateSecurityContext(serverContextHandle: string)");
}
Napi::String serverContextHandleString = info[0].As<Napi::String>();
CtxtHandle serverContextHandle =
SecHandleUtil::deserialize(serverContextHandleString.Utf8Value());
SECURITY_STATUS secStatus = ImpersonateSecurityContext(&serverContextHandle);
if (secStatus != SEC_E_OK) {
throw Napi::Error::New(env,
"Cannot ImpersonateSecurityContext: secStatus = " +
plf::error_msg(secStatus));
}
HANDLE userToken;
DWORD flags = MAXIMUM_ALLOWED; // TOKEN_QUERY | TOKEN_QUERY_SOURCE;
BOOL status = OpenThreadToken(GetCurrentThread(), flags, TRUE, &userToken);
if (status == FALSE) {
throw Napi::Error::New(env, "OpenThreadToken: error. " + plf::error_msg());
}
HANDLE duplicatedToken;
BOOL statusDupl = DuplicateTokenEx(userToken, MAXIMUM_ALLOWED, NULL, SecurityImpersonation, TokenPrimary, &duplicatedToken);
if (statusDupl == FALSE) {
throw Napi::Error::New(env, "DuplicateTokenEx: error. " + plf::error_msg());
}
if (!ImpersonateLoggedOnUser(duplicatedToken)) {
throw Napi::Error::New(env, "C++ ImpersonateLoggedOnUser: error. " + plf::error_msg());
}
testImpersponation(duplicatedToken);
RevertToSelf();
CloseHandle(duplicatedToken);
}
} // namespace myAddon
The above proof of concept and impersonation test has been moved now to the other library as written below.
now the impersonation test has been moved to the other library native-users-node PR by passing the server handle to the session: it looks good to me!
Only a support/help question. Let say I have a user authenticated via kerberos SSO. Now my node backend is running under system user and it has no access to a certain network folder. The authenticated user instead has access to such network folder, so I want to impersonate the user; question: how to do that? Should I use the access token from sso? I can't find an example, a tutorial or more instructions. I've tried to look at the source code here. The point is that I see that SSO.ts is doing
sspi.ImpersonateSecurityContext(this.serverContextHandle);
but to do that it is using a serverContextHandle that is kept private(*)! I would be tempted to fork and modify the code at that point (here, conceptually, I should be able to open the shared folder as the impersonated user, correct?), but it seems complex and before doing that, I would rather gather a better overall understanding. Also because I see alsosspi.OpenThreadToken()
immediately after the impersonation: is that needed for the impersonation (maybe not, I guess the user is already impersonated here, correct?) or just to save the access token? I guess it is for the latter goal, but, again, as I said before, I miss the usage of this access token.(*) Well, it is passed via contructor from the auth.ts that in turn is gathering the
serverSecurityContext.contextHandle
, basically fromsspi.AcceptSecurityContext(input)
where the input is more or less the Kerberos authorization token... ok, but I believe I'm not supposed to repeat all that procedure (starting from Kerberos token and passing it to AcceptSecurityContext) again in my usage code: that would mean that saving the access token is useless, so I'm not considering this option.