0

I need to impersonate a user in a Java application server, and execute an http request to an ASP application in IIS with the permissions of that user. I am trying to adapt the WindowsNegotiateScheme class of the Apache HttpClient for this purpose. This uses JNA to directly access the Windows SSPI authentication functions, hence the problem is probably not specific to Java. As I need a "protocol transition", I do not have the password of the user to impersonate available, just the name. But using the S4U features, this should be possible.

All SSPI function calls either return 0 (success) or 590610 (SEC_I_CONTINUE_NEEDED), never an error code, and the code runs, but the http request arrives at the ASP side with the user running the application server, no matter if I run it locally on my workstation under my user id, or in the application server (which happens to be the machine also running the IIS/ASP application), which runs as local system.

The relevant part of the code is below:

public Header authenticate(Credentials credentials, HttpRequest request, HttpContext context) throws AuthenticationException {
    final String response;
    if (clientCred == null) { // first time
        boolean impersonate = ...; // logic not relevant here

        try {
            final String username = impersonate ? credentials.getUserPrincipal().getName() : CurrentWindowsCredentials.getCurrentUsername();
            final Sspi.TimeStamp lifetime = new Sspi.TimeStamp();
            this.clientCred = new Sspi.CredHandle();
            int credUse = impersonate ? (Sspi.SECPKG_CRED_OUTBOUND | Sspi.SECPKG_CRED_INBOUND) : Sspi.SECPKG_CRED_OUTBOUND;

            int rc = Secur32.INSTANCE.AcquireCredentialsHandle(username,
                    scheme, credUse, null, null, null, null,
                    clientCred, lifetime);
            if (WinError.SEC_E_OK != rc) {
                throw new Win32Exception(rc);
            }

            if(impersonate) {
                impersonationContext = getLocalContext(clientCred);
                rc = Secur32.INSTANCE.ImpersonateSecurityContext(impersonationContext);
            } else {
                impersonationContext = null;
            }

            String targetSpn = getServicePrincipalName(context);
            response = getToken(null, null, targetSpn);
        } catch (RuntimeException ex) {
            ...
        }
    } else {
        ... // second round
    }
    ... // build header form response and return it
}

Method getLocalContext() is meant to get a context to the service itself for the impersonating user. It looks as follows:

private static Sspi.CtxtHandle getLocalContext(final Sspi.CredHandle initialCredentials) {
    final IntByReference attr = new IntByReference();

    String localSpn = "HOST/" + Kernel32Util.getComputerName().toLowerCase();

    Sspi.CtxtHandle clientContext = null;
    Sspi.CtxtHandle serverContext = null;

    Sspi.SecBufferDesc clientToServerToken;
    Sspi.SecBufferDesc serverToClientToken = null;
    while(true) {
        // get clientContext and clientToServerToken:
        Sspi.CtxtHandle outClientContext = (clientContext != null) ? clientContext : new Sspi.CtxtHandle();
        clientToServerToken = new Sspi.SecBufferDesc(Sspi.SECBUFFER_TOKEN, Sspi.MAX_TOKEN_SIZE);
        int rc = Secur32.INSTANCE.InitializeSecurityContext(initialCredentials,
                clientContext, localSpn, Sspi.ISC_REQ_DELEGATE | Sspi.ISC_REQ_MUTUAL_AUTH, 0,
                Sspi.SECURITY_NATIVE_DREP, serverToClientToken, 0, outClientContext, clientToServerToken,
                attr, null);
        clientContext = outClientContext; // make them same for next round
        switch (rc) {
            case WinError.SEC_I_CONTINUE_NEEDED:
                break;
            case WinError.SEC_E_OK:
                return serverContext != null ? serverContext : clientContext;
            default:
                freeContexts(clientContext, serverContext);
                throw new Win32Exception(rc);
        }

        // get serverContext and serverToClientToken:
        Sspi.CtxtHandle outServerContext = (serverContext != null) ? serverContext : new Sspi.CtxtHandle();
        serverToClientToken = new Sspi.SecBufferDesc(Sspi.SECBUFFER_TOKEN, Sspi.MAX_TOKEN_SIZE);
        rc = Secur32.INSTANCE.AcceptSecurityContext(initialCredentials,
                serverContext, clientToServerToken, Sspi.ISC_REQ_DELEGATE | Sspi.ISC_REQ_MUTUAL_AUTH,
                Sspi.SECURITY_NATIVE_DREP, outServerContext, serverToClientToken, attr, null);
        serverContext = outServerContext; // make them same for next round
        switch (rc) {
            case WinError.SEC_I_CONTINUE_NEEDED:
                break;
            case WinError.SEC_E_OK:
                return serverContext;
            default:
                freeContexts(clientContext, serverContext);
                throw new Win32Exception(rc);
        }
    }
}

I found that it runs two complete rounds (InitializeSecurityContext, AcceptSecurityContext, InitializeSecurityContext, AcceptSecurityContext) before returning.

And for reference, the getToken method is just copied from the original WinHttpClient WindowsNegotiateScheme class:

String getToken(
        final CtxtHandle continueCtx,
        final SecBufferDesc continueToken,
        final String targetName) {
    final IntByReference attr = new IntByReference();
    final SecBufferDesc token = new SecBufferDesc(
            Sspi.SECBUFFER_TOKEN, Sspi.MAX_TOKEN_SIZE);

    sspiContext = new CtxtHandle();
    final int rc = Secur32.INSTANCE.InitializeSecurityContext(clientCred,
            continueCtx, targetName, Sspi.ISC_REQ_DELEGATE | Sspi.ISC_REQ_MUTUAL_AUTH, 0,
            Sspi.SECURITY_NATIVE_DREP, continueToken, 0, sspiContext, token,
            attr, null);
    switch (rc) {
        case WinError.SEC_I_CONTINUE_NEEDED:
            continueNeeded = true;
            break;
        case WinError.SEC_E_OK:
            dispose(); // Don't keep the context
            continueNeeded = false;
            break;
        default:
            dispose();
            throw new Win32Exception(rc);
    }
    return Base64.encodeBase64String(token.getBytes());
}

I checked in the debugger that the username used for the impersonation is in fact something like DOMAINNAME\johndoe, and I realized it does not even matter if I use a nonexistig user, the behavior is just the same: AcquireCredentialsHandle returns 0 (= SEC_E_OK). This tells me that either this method does not check the user name against the domain controller, but only the syntax of its arguments, but then why does the initial call to InitializeSecurityContext not complain if it first argument are credentials for a nonexistent user? Or the method somehow ignores its first argument at all, but why?

Assuming I have a context for the user to impersonate already from the AcquireCredentialsHandle call and omitting the call to getLocalContext and ImpersonateSecurityContext did not work either.

I found there is not much documentation on pure SSPI usage for impersonation. Most samples I found (like this) rely on a .net class WindowsIdentity, which is not available in the pure SSPI interface. And the source code of that class does not look as if it just consists of a few SSPI calls, but a lot of other .net infrastructure is referenced.

I thought that maybe the Windows OS impersonation does not collaborate properly with Java, but according to this question, the Windows impersonation is per thread, and according to this question, Java uses the OS threads.

How can I get the impersonation working, or do you at least have some hints what to try?

FrankPl
  • 563
  • 2
  • 18

1 Answers1

1

Your code will never do what you have described. Your snippets only do credential delegation (unconstrained delegation) where the forwarded TGT is embedded in the security context. Since you want protocol transition (S4U2Self), you are required to call LsaLogonUser with KERB_S4U_LOGON.

Read this blog post. It is going to be a lot of pain in C. Never understood why this is so easy with GSS-API, but such a pain on Windows.

Good luck.

Michael-O
  • 17,130
  • 6
  • 51
  • 108
  • Thank you for pointing to this blog. It seems to be really difficult to find working examples for S4U that purely rely on the Win32 API (SSPI). – FrankPl Jan 31 '18 at 19:16
  • Your answer lead me to new Google search terms, and I found an implementation in C# using SSPI at http://www.pinvoke.net/default.aspx/secur32.LsaLogonUser – FrankPl Feb 01 '18 at 10:19
  • @FrankPl This P/Invoke looks good to me. I would try C# first and see wether this works. Though, in pure C# there is `WindowsIdentity` which will S4U for you. No C boilerplate code. – Michael-O Feb 01 '18 at 10:54