jlguenego / node-expose-sspi

Expose Microsoft Windows SSPI to Node for SSO authentication.
ISC License
128 stars 19 forks source link

[Usage Question] impersonation for kerberos authenticated users #117

Open giuliohome opened 3 years ago

giuliohome commented 3 years ago

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 also sspi.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 from sspi.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.

giuliohome commented 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

giuliohome commented 3 years ago

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);
giuliohome commented 3 years ago

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...

giuliohome commented 3 years ago

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;
}
giuliohome commented 3 years ago

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

  1. impersonateLoggedOnUser : 'Access is denied.'
  2. sqlite connection: 'SQLITE_CANTOPEN: unable to open database file'
  3. ms sql with windows auth: "[Microsoft][SQL Server Native Client 11.0][SQL Server]Login failed for user 'domain\owner'."
giuliohome commented 3 years ago

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.

giuliohome commented 3 years ago

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.

giuliohome commented 3 years ago

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.

giuliohome commented 3 years ago

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!