Skip to content
WindowsDeep Dive Published Updated 7 min readViews unavailable

Windows Impersonation Tokens: Thread Context, Reversion, and Service Boundaries

Trace Windows thread impersonation from client-token acquisition through scoped access and reversion, with fail-closed handling for reusable service workers.

Windows impersonation lets a server thread perform a bounded operation using a client’s security context without changing the process identity. That distinction is useful for a service that must check a client’s access to a file or another protected object, but it creates a sharp lifetime boundary: the worker must stop using the client context before it returns to a pool or processes another request. This guide follows the named-pipe path and focuses on the thread-token lifecycle, not on building a pipe protocol or explaining every part of the Windows token model.

A thread context is not the service process identity

An access token carries security information used in access checks. A process has a primary token, while an impersonating thread can have a thread token that temporarily supplies a different context to security checks made by that thread. The process is not converted into the client, and other threads do not automatically inherit this thread’s context. Microsoft describes impersonation as a thread executing with security information different from the process that owns it (Client Impersonation).

That separation is not a reason to be casual. Code running on the impersonating thread may access files, registry keys, named objects, or other resources under the client’s authority. A reused worker that accidentally keeps the token can run the next request under the wrong identity. Keep the protected operation small, avoid calling unrelated callbacks while impersonating, and make the return-to-service-context point explicit.

Bind the token to the request that was actually read

For a named pipe, the server end calls ImpersonateNamedPipeClient. The API impersonates the security context of the last message read from that pipe; it is not a general operation that accepts an arbitrary client identifier. The server must check the return value. If impersonation fails, the client was not impersonated and later operations run in the calling process’s context. Microsoft explicitly says not to execute the client request on that failure path (ImpersonateNamedPipeClient).

Associate the pipe read, decoded request, and authorization decision in one request state machine. Validate message framing and application-level fields before performing any privileged action. If a pipe instance is reused, do not assume the token belongs to a request merely because the connection is still open: read the intended message and invoke impersonation at the point where that request’s identity is needed. Pipe ACLs answer who may connect; they do not authorize every operation in the application protocol.

The impersonation level also limits what the server may do. Microsoft distinguishes identification, local impersonation, and delegation to remote systems. On a local named-pipe connection, the client can specify the server’s impersonation level when it opens the pipe with CreateFile and SECURITY_SQOS_PRESENT. Do not apply that client-side control to remote pipes: Microsoft documents that the CreateFile impersonation-level flags are ignored for a remote named-pipe connection; the client’s impersonation level is instead determined by the levels enabled by the server, which are set by a flag on the server’s account in the directory service. A client-side request for a lower level therefore is not a remote-pipe safeguard; review the server’s delegation configuration and the token actually obtained. Do not assume that access to a local file implies the ability to authenticate to a remote server on the client’s behalf (Impersonating a Named Pipe Client, Impersonation Levels, CreateFile, SECURITY_IMPERSONATION_LEVEL).

Inspect the thread token without broadening its authority

After impersonation succeeds, OpenThreadToken can obtain a handle to the current thread’s token. Request only the information the authorization path needs. TOKEN_QUERY suffices for all GetTokenInformation classes except TokenSource, which requires TOKEN_QUERY_SOURCE. The OpenAsSelf parameter controls which security context is used for the access check on opening the token. It does not change the client token or elevate the request. An anonymous impersonation token cannot be opened this way, so treat that result as an authorization failure rather than silently switching to the service account (OpenThreadToken, GetTokenInformation).

For example, a file service can use the impersonating thread for one access check or one open operation. If a design instead needs a process created in the client’s context, that is a different operation: a server typically obtains the thread token and duplicates it as a primary token with DuplicateTokenEx, then follows the documented CreateProcessAsUser requirements, including its privilege and profile/environment considerations. Do not treat a thread impersonation token as if it were already a primary process token (DuplicateTokenEx, CreateProcessAsUser).

Restore the service context after operation failures and C++ exceptions

The following fragment demonstrates the ordering for a synchronous request handler and C++ exceptions. ClientOperation represents application-owned authorization and work; the token handle it receives is borrowed for that call, must not be closed or retained, and is closed by the handler before reversion. The operation must not queue work that continues after the handler returns. With MSVC’s recommended /EHsc model, catch (…) catches C++ exceptions, not asynchronous structured exceptions such as access violations. This example deliberately does not recover from SEH: treat such faults as process-fatal and do not add an outer recovery handler that resumes work while the thread may still be impersonating. Microsoft documents separate SEH cleanup constructs; any design that must handle SEH needs its own reviewed structured-cleanup strategy, and enabling broad SEH recovery can be unsafe. The fail-closed hook must not return if the process cannot restore its own context.

#include <windows.h>

using ClientOperation = bool (*)(HANDLE token, void* context, DWORD* errorCode);

// Application-owned fail-stop path. It must terminate this service process.
[[noreturn]] void FailClosedServiceProcess(DWORD revertError);

bool HandleOnePipeRequest(HANDLE pipe, ClientOperation operation, void* context) {
    if (pipe == INVALID_HANDLE_VALUE || operation == nullptr) {
        SetLastError(ERROR_INVALID_PARAMETER);
        return false;
    }

    // Never process the request as the service if this call fails.
    if (!ImpersonateNamedPipeClient(pipe)) {
        return false;
    }

    HANDLE clientToken = nullptr;
    DWORD operationError = ERROR_SUCCESS;
    bool operationOk = false;

    if (!OpenThreadToken(GetCurrentThread(), TOKEN_QUERY, TRUE, &clientToken)) {
        operationError = GetLastError();
    } else {
        try {
            operationOk = operation(clientToken, context, &operationError);
        } catch (...) { // C++ exceptions under /EHsc; this is not an SEH recovery boundary.
            operationOk = false;
            operationError = ERROR_GEN_FAILURE;
        }

        if (!CloseHandle(clientToken)) {
            const DWORD closeError = GetLastError();
            operationOk = false;
            if (operationError == ERROR_SUCCESS) {
                operationError = closeError;
            }
        }
    }

    // Reversion is required even when token inspection or the operation failed.
    if (!RevertToSelf()) {
        FailClosedServiceProcess(GetLastError());
    }

    if (!operationOk) {
        SetLastError(operationError == ERROR_SUCCESS ? ERROR_GEN_FAILURE : operationError);
    }
    return operationOk;
}

This is an ordering pattern, not a complete named-pipe server. A production callback still needs a request-specific authorization policy, bounded input, and safe handle ownership. In asynchronous code, keep the thread and its impersonation scope together; do not return a worker to the pool while an operation that depends on its context is still outstanding. If work must continue on a different thread, define and review an explicit token-transfer design rather than expecting a thread token to follow the work.

Reversion failure is a process-level security fault

Call RevertToSelf after the protected operation on every normal and error path. Microsoft documents a critical failure behavior: if reversion fails, the application continues running in the client’s context and should shut down the process. Therefore, logging an error and returning the worker to the pool is not a safe fallback. A watchdog or service supervisor can help restart a failed service, but it does not make continued execution under an unknown client token acceptable (RevertToSelf).

The named-pipe pattern is not interchangeable with RPC impersonation. Microsoft documents separate RpcRevertToSelf and RpcRevertToSelfEx cleanup APIs for RPC impersonation. Keep the acquisition and reversion functions paired to the mechanism actually used.

Test the worker boundary, not only a successful file open

Exercise denied pipe access, malformed requests, failed impersonation, token-open failure, callback failure, exceptions, client disconnects, and successful reversion. Reuse the same worker for a second request under a different identity and verify that the second operation cannot observe the first client’s token. For a service pool, test cancellation and shutdown while a request is in progress; do not let a timeout path bypass cleanup.

Record a request correlation ID, the authorization result, and sanitized error codes, not token contents or secrets. A test may inspect a token’s user SID or groups with the documented token-information APIs, but identity inspection is not a replacement for the application’s resource-specific authorization decision. The example has not been executed on Windows; it illustrates documented API ordering and leaves the product-specific policy, fail-stop implementation, and native integration tests to the service.

Related:

Sources:

Comments