Handling Windows Impersonation Tokens Correctly — Borrowing Privileges per Thread and Reverting Safely
· Updated: · Go Komura · Windows, Security, Access Token, Impersonation, Win32, .NET, C#, Operations, Legacy Asset Reuse
Revision history (1 updates, last updated Sep 1, 2026)
A log of the changes made to this article. Where a pre-update version was archived, it stays readable at a permanent DOI link.
- Retranslated as a full translation of the Japanese original. The previous English version was an abridgement that carried only part of the source, so sections, tables, Mermaid diagrams, figure captions and FAQ entries were missing. All of them have been restored to match the Japanese original, and the technical claims are the same as in the Japanese version. Read the version before this update (DOI: 10.5281/zenodo.21614667)
- First published
Cite this article(DOI: 10.5281/zenodo.21614666)
This article is archived on Zenodo. Below are both the DOI that always resolves to the latest version and the DOI pinned to the version you are reading.
Go Komura (2026). Handling Windows Impersonation Tokens Correctly — Borrowing Privileges per Thread and Reverting Safely. KomuraSoft LLC. https://doi.org/10.5281/zenodo.21614666 https://comcomponent.com/en/blog/2026/06/09/002-windows-impersonation-token/
- DOI (latest version)
- 10.5281/zenodo.21614666
- DOI (this version)
- 10.5281/zenodo.22220527
1. What to Understand First
When you build Windows applications or Windows services, you run into the requirement “run just this operation as a different user.”
For example:
- From a Windows service, access a file server with the end user’s own privileges
- In an administration application, check only what a specific user can see
- In Named Pipes, RPC, COM, IIS, or ASP.NET Core, perform part of the work with the calling user’s privileges
- Because of existing assets, switch the Windows account used for each unit of work
This is where impersonation, access tokens, and impersonation tokens come in.
But there is one thing to emphasize right at the start.
Windows impersonation is not magic that makes you an administrator. It is a mechanism that switches the security context used for access checks, mainly on a per-thread basis.
Implement it without grasping this distinction and you get problems like these:
- You thought you were impersonating, yet file access returns
Access denied - Local files are readable, but only network shares fail
- Somewhere inside
Task.Runorasync, you are quietly back to the original user - Logging and downstream work keep running while still impersonated, blurring the privilege boundary
- Primary tokens and impersonation tokens get confused, and launching a process fails
- You get stuck on “the user is in Administrators, so why can’t they write?”
This article lays out how to handle Windows impersonation tokens safely in practice.
It is not about attack techniques or privilege theft. It is about handling privilege boundaries correctly in Windows applications, Windows services, and .NET applications.
The code in this article is published on GitHub as a complete buildable sample set: a library, a demo you run on Windows, and unit tests for argument validation and guard behavior.
windows-impersonation-token - komurasoft-blog-samples (GitHub)
In the diagram a solid line marks a relation that always holds and a dashed line marks a conditional one (the conditions are given per relation on the detail page). The full list of relations (18 in total, with evidence and certainty) and the definitions of the main concepts are collected on the knowledge map detail page (in Japanese). Data: JSON-LD / Turtle
2. What Is an Access Token?
Windows uses an access token to represent the security context of a user or a process.
An access token roughly carries the following information. These terms come up again and again in later sections, so here is a one-line meaning for each.
| Item | Meaning |
|---|---|
| The user’s SID | SID stands for Security Identifier, the identifier that uniquely denotes a user or a group. Access checks use this value, not the display name |
| Group membership | The set of SIDs of the groups the user belongs to. Whether the user is in Administrators shows up here as well |
| Privileges | Rights granted outside the ACL of any individual object, such as backup operations or shutdown. Each one carries an enabled or disabled state |
| Default owner | The SID that becomes the owner of objects newly created with this token |
| Default DACL | DACL stands for Discretionary Access Control List, the list that spells out who is allowed to do what. It is attached by default to newly created objects |
| Restricting SIDs | The list of SIDs used by a restricted token. An additional access check runs against this list too, so membership in a group does not guarantee access |
| Integrity level | A hierarchy of Low, Medium, High, and so on. A lower integrity level cannot write to objects at a higher level. Section 17 covers this |
| Elevation state | Whether the token is elevated, in an environment where UAC is enabled. Section 17 covers this |
| Impersonation level | A value only impersonation tokens carry. It determines whether the server can merely identify the client, actually access objects as the client, or delegate to a remote system. Section 7 covers this |
| Token type | Whether this is a primary token or an impersonation token. Section 6 covers this |
Keep in mind that what drives an access check is not the name but the SID, the groups, and the integrity level. With that in hand, the later discussion of “the name is right yet you still get Access denied” is much easier to follow.
Files, registry keys, services, named pipes, processes, threads, events, mutexes, and many other Windows objects carry a security descriptor.
When a thread tries to open a protected object, Windows compares the information in the token against the ACL of the target object.
The thread doing the work
↓
Which security context is the access made in?
↓
Look at the token's user, groups, and privileges
↓
Compare them against the target object's ACL
↓
Decide allow / deny
Understanding this question, “which security context does the access use,” is the way into understanding impersonation tokens.
3. A Process Has a Primary Token
Each Windows process normally has a primary access token.
For example, when a user launches an application from the desktop, the process gets a primary token representing that user’s security context.
For a Windows service, it gets the primary token of the account the service runs as.
Picture it like this.
MyService.exe
Primary Token: DOMAIN\svc-app
If a thread inside this service is not impersonating anything, the process’s primary token is used when it accesses files or the registry.
In other words, the default looks like this.
Thread A
Impersonation Token: none
↓
Access checks use the Process's Primary Token
Open C:\Data\foo.txt in this state and the question is whether DOMAIN\svc-app has the right to do so.
4. Impersonation Tokens Attach to Threads
Once impersonation starts, an impersonation token is attached to the thread. This is the crucial point: impersonation is fundamentally not “the whole process becomes a different user.” It is more accurate to think of it as that thread undergoing access checks in a different security context.
MyService.exe
Primary Token: DOMAIN\svc-app
Thread A
Impersonation Token: DOMAIN\alice
Thread B
Impersonation Token: none
Here, when Thread A opens a file, the access check runs with the privileges of DOMAIN\alice.
Thread B, on the other hand, is not impersonating, so its access check runs with the privileges of DOMAIN\svc-app.
Miss this distinction and you get confusion like the following.
// Thread A: you think you are impersonating
StartImpersonation(token);
// but the work is handed off to another thread
Task.Run(() =>
{
File.ReadAllText(path);
});
// and you revert right away
RevertToSelf();
In this case, there is no guarantee that the thread actually reading the file runs in the impersonation state you expected.
Impersonation has to be handled with the relationship between scope, threads, and asynchronous work made explicit.
5. “Impersonation” Is Not Privilege Escalation
The word impersonation sounds a little dramatic, but what matters in practice is not to confuse it with privilege escalation.
What impersonation gets you is basically this.
Do the work with the server process's privileges
↓
For part of that work only, run access checks with the client user's privileges
For example, if you want to use a file server’s ACLs directly as your access control, a server application that always reads files as the service account cannot reflect the per-user ACLs.
So you impersonate the calling user for just part of the request handling and do the file access there.
HTTP / RPC / Named Pipe request
User: DOMAIN\alice
↓
Server application
Process: DOMAIN\svc-app
↓
Impersonate DOMAIN\alice for the file access alone
↓
The file server's ACL allows or denies it
This is useful when you want to rely on the existing Windows ACLs rather than on authorization logic of your own.
Convenient as it is, impersonation makes privilege boundaries hard to see if you get the design wrong.
- Which operation runs as whom
- Where impersonation starts
- Where it is reliably reverted
- Which log entries are written under which user’s privileges
- Whether it reverts on exceptions too
- Whether impersonation is still in effect at the end of the asynchronous work
Making all of this explicit in code is what matters.
6. Keep Primary Tokens and Impersonation Tokens Separate in Your Head
Among Windows tokens, the pair that gets confused most is the primary token and the impersonation token.
Roughly, think of them this way.
| Token | Main use | Typical examples |
|---|---|---|
| Primary token | Represents the security context of a process | Launching a process, CreateProcessAsUser |
| Impersonation token | Lets a thread run in a different security context | ImpersonateLoggedOnUser, SetThreadToken, Named Pipe client impersonation |
The key point is that launching a process generally requires a primary token.
Holding an impersonation token does not mean you can use it as is to start a process as another user.
The typical flow looks like this.
Impersonate the client
↓
Get the impersonation token with OpenThreadToken
↓
Create a primary token with DuplicateTokenEx
↓
Pass it to CreateProcessAsUser or similar
Conversely, if what you want is “just this thread should do file access as a different user,” that is impersonation-token territory, not process launching.
Mix the two up and you end up chasing errors such as Access denied or The parameter is incorrect even though the API arguments look correct.
7. Understand Impersonation Levels
An impersonation token carries an impersonation level.
There are four levels in common use.
| Impersonation level | Rough meaning |
|---|---|
| Anonymous | The server cannot obtain identifying information about the client |
| Identification | The server can identify the client but cannot use that identity to access objects with the client’s privileges |
| Impersonation | The server can act with the client’s privileges on the local system |
| Delegation | The server can also delegate the client’s privileges to remote systems |
The one that trips people up in practice is the difference between Identification and Impersonation.
Identification, as the name says, is the level for finding out who the other party is.
It is not enough for opening files with that user’s privileges.
That leads to situations like this.
WindowsIdentity.GetCurrent().Name looks like the user you expected
↓
but the file access still returns Access denied
In that case you need to check the impersonation level, not just the name.
In .NET, WindowsIdentity.ImpersonationLevel gives you the clue.
using System.Security.Principal;
WindowsIdentity identity = WindowsIdentity.GetCurrent();
Console.WriteLine(identity.Name);
Console.WriteLine(identity.ImpersonationLevel);
Access across the network calls for even more care.
In a setup like “the web server impersonates the user and then accesses another file server or DB server as that user,” you can run into what is known as the double-hop problem.
Impersonating in application code does not necessarily solve that. You need a design that takes in Kerberos, SPNs, delegation, constrained delegation, service accounts, and the authentication method of the target.
8. The Basic Shape of Impersonation
Conceptually, handling impersonation with Win32 APIs looks like this.
1. Obtain the token to impersonate with
2. Impersonate the current thread with that token
3. Run only the work that needs it
4. Always revert to the original security context
5. Close the token handle
In code, that always means try / finally.
if (!ImpersonateLoggedOnUser(tokenHandle))
{
throw new Win32Exception(Marshal.GetLastWin32Error());
}
try
{
// only this part runs as the impersonated user
DoWorkAsImpersonatedUser();
}
finally
{
if (!RevertToSelf())
{
// failing to revert is dangerous, so at the very least do not keep going
throw new Win32Exception(Marshal.GetLastWin32Error());
}
}
Laid out over time, it looks like this.
sequenceDiagram
participant App as Caller
participant Th as Worker thread
participant Win as Windows
participant FS as File
App->>Th: Request the work
Th->>Win: Attach the token with ImpersonateLoggedOnUser
Note over Th: From here access checks run<br/>as the user alice
Th->>FS: Open the file
FS-->>Th: The ACL is evaluated with the privileges of alice
Th->>Win: Detach the token with RevertToSelf
Note over Th: From here it is back to the<br/>service account svc-app
Th-->>App: Return the result
Impersonation is in effect only between ImpersonateLoggedOnUser and RevertToSelf. Any I/O performed outside that stretch is evaluated against the process’s account, not the impersonated user. The asynchronous pitfall covered in section 14 is easiest to grasp as the problem of the real I/O slipping outside that stretch.
What matters is not starting the impersonation but reliably reverting it. Forget to revert and the rest of the work on that thread keeps running as the impersonated user.
In applications that use a thread pool in particular, what you thought was one unit of work can affect another request or another operation.
So think of impersonation not as “start it and revert it” but as something to confine to a small scope.
9. In .NET, Use WindowsIdentity.RunImpersonated
In .NET, using WindowsIdentity.RunImpersonated where you can makes the impersonation scope easy to express in code.
If you have a SafeAccessTokenHandle, you can write it like this.
using Microsoft.Win32.SafeHandles;
using System.Security.Principal;
static string ReadFileAsUser(SafeAccessTokenHandle token, string path)
{
return WindowsIdentity.RunImpersonated(token, () =>
{
return File.ReadAllText(path);
});
}
The good thing about this shape is that the impersonated region is closed inside the lambda.
RunImpersonated(token, () =>
{
// impersonated only here
});
// from here out you are back in the original context
When you want to impersonate for a specific operation only, such as file access, registry access, or a call into an existing library, this shape is both readable and safe.
For asynchronous work, use RunImpersonatedAsync.
using Microsoft.Win32.SafeHandles;
using System.Security.Principal;
static Task WriteFileAsUserAsync(
SafeAccessTokenHandle token,
string path,
string text,
CancellationToken cancellationToken)
{
return WindowsIdentity.RunImpersonatedAsync(token, async () =>
{
await File.WriteAllTextAsync(path, text, cancellationToken);
});
}
What you want to avoid is launching a fire-and-forget task from inside the impersonation scope.
// bad example
WindowsIdentity.RunImpersonated(token, () =>
{
_ = Task.Run(() =>
{
File.WriteAllText(path, text);
});
});
With this code it becomes unclear when, and in which execution context, the code that actually writes the file runs.
Asynchronous work that has to be impersonated should be awaited inside RunImpersonatedAsync, so that you leave the scope only after the work has completed.
10. Obtaining a Token with LogonUser
LogonUser is the canonical API for obtaining a token from another user’s credentials, and it is an API to treat with care.
LogonUser takes a user name, a domain, and a password.
In other words, your application ends up handling credentials.
In practice, watch for the following.
- Do not put passwords in code or configuration files in clear text
- Prefer OS authentication, service accounts, delegation, or existing Windows authentication where you can
- Keep secrets in a proper secret store or in your operations platform
- Always close token handles
- Do not write secrets other than the user name to the log
- Minimize the region you impersonate
A minimal example follows, but whatever you do, do not copy it verbatim and write a string literal into password. Credentials written into source code survive in the repository history, in build outputs, and in anything decompiled from them. Putting them in a configuration file in clear text amounts to the same thing. Section 21.6 returns to this.
So where do you put the password? Here are the options.
| Storage method | Where it fits | Watch out for |
|---|---|---|
| Do not hold a password at all | The first choice. Get by with Windows authentication, service accounts, and delegation | It has to be decided at design time. Removing it later is painful |
| Prompt for it interactively at run time | Operations tools an administrator runs at their own machine | Not usable for unattended runs or services. Release the string you read as soon as you are done with it |
| Windows Credential Manager | Repeated use from the same account on the same PC | It is a per-user vault. To use it from a service account, you have to register it under that account |
| DPAPI | Protecting a value in a configuration file within a single PC | Mind the ProtectedData scope. CurrentUser means only the user who encrypted it, LocalMachine means other users on that PC can decrypt it too |
| A secret management platform such as Key Vault | Multiple servers, CI, or cloud integration | You need to authenticate to the platform itself. How the retrieved secret is handled in memory is a separate question |
For the concrete code when using DPAPI, see the separate article Storing Secrets in Windows Apps - Avoiding Plaintext Configuration with DPAPI.
Whichever method you choose, the common step is to ask whether the application really needs to receive a password at all before you pick a storage method.
Here is the minimal example.
using Microsoft.Win32.SafeHandles;
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Security.Principal;
internal static class NativeMethods
{
private const int LOGON32_LOGON_INTERACTIVE = 2;
private const int LOGON32_PROVIDER_DEFAULT = 0;
[DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
internal static extern bool LogonUser(
string lpszUsername,
string? lpszDomain,
string lpszPassword,
int dwLogonType,
int dwLogonProvider,
out SafeAccessTokenHandle phToken);
public static SafeAccessTokenHandle Logon(
string userName,
string? domain,
string password)
{
bool ok = LogonUser(
userName,
domain,
password,
LOGON32_LOGON_INTERACTIVE,
LOGON32_PROVIDER_DEFAULT,
out SafeAccessTokenHandle token);
if (!ok)
{
throw new Win32Exception(Marshal.GetLastWin32Error());
}
return token;
}
}
public static string ReadFileWithExplicitCredential(
string userName,
string? domain,
string password,
string path)
{
using SafeAccessTokenHandle token = NativeMethods.Logon(userName, domain, password);
return WindowsIdentity.RunImpersonated(token, () =>
{
return File.ReadAllText(path);
});
}
This example exists only to show the shape of the API.
In practice, first work out whether it is acceptable for the application to receive a password at all.
In many cases one of these alternatives is safer.
| What you want to do | Alternative |
|---|---|
| Let the whole service access a particular resource | Give a dedicated service account the minimum necessary ACL |
| Access files with the end user’s own privileges | Use Windows authentication and a delegation design |
| Perform an operation that requires administrator privileges | Expose an explicit administration API on the service side and control it with authorization in the application |
| Use a different account for part of the work only | Confine the impersonated region to a single method and keep an audit log |
11. Mind the Logon Type in LogonUser
The nature of the token LogonUser returns changes with the logon type.
Copy and paste without understanding this difference and it will not behave the way you expect.
| Logon type | What to watch for |
|---|---|
| Interactive | Close to an interactive logon. Convenient for local operations, but it depends on the execution environment and privileges |
| Network | Intended for network logons. The token it returns may not be usable directly for launching a process |
| NewCredentials | Locally it stays close to the current credentials, and it is sometimes used to supply the specified credentials when connecting to remote systems |
The point here is not to memorize particular logon types.
Two things matter.
- The logon type changes the behavior for local access, network access, and process launching
- You need to check whether the token you got back is a primary token or an impersonation token
A classic source of confusion, for example, is taking a token obtained with LOGON32_LOGON_NETWORK and passing it straight to CreateProcessAsUser, which then fails.
If launching a process is the goal, you need a primary token.
The design then becomes creating a primary token with DuplicateTokenEx as needed.
12. Do Not Take RevertToSelf Lightly
When you start impersonating through a Win32 API, you end it with RevertToSelf. That revert is not mere cleanup; it is the step that puts the security boundary back where it was.
Here is a bad example.
ImpersonateLoggedOnUser(token);
DoWork();
RevertToSelf();
It looks fine at a glance, but if DoWork() throws, RevertToSelf() never runs.
Always put it in a finally.
if (!ImpersonateLoggedOnUser(token))
{
throw new Win32Exception(Marshal.GetLastWin32Error());
}
try
{
DoWork();
}
finally
{
if (!RevertToSelf())
{
throw new Win32Exception(Marshal.GetLastWin32Error());
}
}
Carrying on as if nothing happened when RevertToSelf fails is dangerous too. Continue with downstream work in a state where you may not have returned to the original privileges, and that work keeps running with the privileges of a user you did not intend. At the very least, treat that unit of work as failed and err on the safe side.
.NET’s RunImpersonated / RunImpersonatedAsync are useful precisely as a scope construct that keeps you from forgetting the revert.
13. Keep the Impersonation Scope as Small as Possible
The most important design principle for impersonation is to impersonate only for the work that needs it.
Here is a bad example.
WindowsIdentity.RunImpersonated(token, () =>
{
ValidateRequest();
LoadConfiguration();
WriteDebugLog();
ReadUserFile();
UpdateDatabase();
SendNotification();
});
Impersonating this broad a region makes it hard to tell which operation runs with which privileges.
Access to the log destination might happen with the impersonated user’s privileges, and the log write might fail. The DB connection might be attempted with the impersonated user’s credentials instead of the service account’s. Notification handling and temporary file creation might all be affected by a privilege context they do not need.
The good version carves out only the operations that require impersonation.
ValidateRequest();
LoadConfiguration();
string content = WindowsIdentity.RunImpersonated(token, () =>
{
return File.ReadAllText(userFilePath);
});
UpdateDatabase(content);
WriteAuditLog(userName, userFilePath, success: true);
With this shape it is clear that only the File.ReadAllText part needs impersonation.
Impersonation is convenient, but the wider you spread it, the harder the code is to read and the more can go wrong.
14. In Async Code, Check Whether Impersonation Holds Until the Work Finishes
In a modern .NET application, much of the work is asynchronous: files, HTTP, databases, queues, storage.
That makes the combination of impersonation and async / await something to be careful with.
The basic rule is just this.
Await asynchronous work that needs impersonation inside RunImpersonatedAsync
Here is what happens when you break it, drawn in the same form as the diagram in section 8. First, though, one thing to get straight. Handing work to another thread does not necessarily drop the impersonation. Which mechanism you impersonated with decides how it breaks, and the two break in opposite directions.
| How you impersonate | Work handed to Task.Run |
What happens |
|---|---|---|
.NET’s RunImpersonated / RunImpersonatedAsync |
keeps running impersonated | Impersonation stretches beyond the scope written in the code. The caller receives neither the completion nor the exception |
Calling Win32’s ImpersonateLoggedOnUser directly |
runs as the process’s account | The real I/O lands outside the impersonated stretch and returns Access denied |
The .NET side behaves that way because RunImpersonated puts the impersonation token in an AsyncLocal. An AsyncLocal value flows along with the ExecutionContext, and Task.Run captures the ExecutionContext as of the call and restores it on a pool thread. The change handler that runs on each restore calls ImpersonateLoggedOnUser again on that thread, so the pool thread ends up with the same impersonation. That is how the runtime is implemented, in WindowsIdentity’s s_currentImpersonatedToken and CurrentImpersonatedTokenChanged.
A Win32 token, by contrast, is bound to a thread and is never copied to another thread on its own.
Following the .NET side over time, it looks like this.
sequenceDiagram
participant Th as Calling thread
participant Pool as A different thread pool thread
participant FS as File
Th->>Th: Start impersonating with RunImpersonated
Th->>Pool: Hand off the write with Task.Run
Note over Pool: The impersonation token travels<br/>along with the ExecutionContext
Th->>Th: Leave the scope and stop impersonating
Note over Th: What is undone is only the<br/>impersonation on this thread
Pool->>FS: The actual write runs after that
Note over Pool: Still the impersonated user.<br/>The caller has no idea when it ends
FS-->>Pool: Nobody receives the result or the exception
In the section 8 diagram, the I/O sat inside the impersonated stretch. In this one, the work moves to a different thread the moment it is handed to Task.Run, and the actual write lands outside the stretch as written in the code. The danger here is not that impersonation drops, but that it does not drop and you lose track of it. Concretely, three things happen at once.
- The extent of the impersonation becomes unreadable. The scope you wrote as “from here to here” no longer matches the region that actually runs impersonated
- Failures get swallowed. Nobody
awaits the task you launched, so anAccess deniednever surfaces as an exception anywhere - There is no right moment to close the token. Close the
SafeAccessTokenHandlewithusingand you close it out from under work that is still running
If instead you impersonate directly with Win32, the handed-off work runs as the process’s account. That tends to surface as a success in the test environment, where the service account happens to have the rights, followed by the first Access denied in production.
Either way, it does not happen if you await the work to completion inside RunImpersonatedAsync.
Here is the good version.
await WindowsIdentity.RunImpersonatedAsync(token, async () =>
{
await using FileStream stream = File.OpenRead(path);
using var reader = new StreamReader(stream);
string text = await reader.ReadToEndAsync();
await ProcessTextAsync(text);
});
Even in this shape, though, there is something to think about.
Does ProcessTextAsync really need to run as the impersonated user?
If only the file read needs impersonation, splitting it like this is safer.
string text = await WindowsIdentity.RunImpersonatedAsync(token, async () =>
{
return await File.ReadAllTextAsync(path);
});
await ProcessTextAsync(text);
The fact that you can await inside an impersonation scope does not mean you should put anything you like in it.
In asynchronous code too, keep the impersonated region minimal.
15. ASP.NET Core and Impersonation
Impersonation needs care when you use Windows authentication with ASP.NET Core as well.
Assuming that “the user signed in with Windows authentication, so the whole request runs as that user” is dangerous.
In general, the application process itself runs as the application pool identity or as the account the service runs under. You do get the user’s Windows identity as authentication information, but that does not mean all the work runs with the user’s privileges.
If a specific action has to run with the user’s privileges, create the scope explicitly with RunImpersonated / RunImpersonatedAsync.
The code looks roughly like this.
app.MapGet("/download", async (HttpContext context) =>
{
if (context.User.Identity is not WindowsIdentity user)
{
return Results.Unauthorized();
}
string path = GetPathFromRequest(context);
byte[] bytes = await WindowsIdentity.RunImpersonatedAsync(
user.AccessToken,
async () => await File.ReadAllBytesAsync(path));
return Results.File(bytes, "application/octet-stream");
});
Here too, only the file read is impersonated.
Whether response generation, logging, and the application’s own authorization decisions need to sit inside the impersonation scope deserves careful thought.
16. How to Read Access denied
Access denied is the error you see most often in implementations that use impersonation.
Concluding “impersonation failed” and nothing more when it appears takes you the long way around.
Break the investigation into separate angles.
| Angle | What to check |
|---|---|
| Is impersonation really in effect | Check WindowsIdentity.GetCurrent().Name inside the impersonation scope |
| Is the impersonation level sufficient | Is it at the level you need rather than Identification |
| Is the target resource’s ACL correct | Does the impersonated user have read / write rights |
| Local or remote | Does it succeed for local files and fail only for UNC |
| Is this a double hop | Are you trying to reach a file server from a web server with the user’s privileges |
| Have you left the impersonation scope | Is the actual I/O running outside the scope or on a separate task |
| Is the token type right | Are you passing an impersonation token where you launch a process |
| Is UAC or the integrity level involved | Even in Administrators, is the token unelevated |
Above all, do not relax just because the name checks out.
WindowsIdentity identity = WindowsIdentity.GetCurrent();
Console.WriteLine(identity.Name);
This log line is useful, but it is not enough.
At minimum, look at these as well.
Console.WriteLine(identity.ImpersonationLevel);
Console.WriteLine(identity.IsAuthenticated);
And if the target is a network share, check not just the application code but also the authentication method, the delegation settings, the SPNs, the service accounts, and the ACLs on the file server side.
17. UAC and the “I’m an Administrator but It Fails” Problem
In Windows, a user belonging to the Administrators group and the current token being elevated are not the same thing.
Where UAC is enabled, even an administrator’s processes normally run with a restricted token, and operations that require administrator privileges require elevation.
So this happens.
The user is in Administrators
↓
but the current token is unelevated
↓
writing to Program Files or HKLM returns Access denied
The same holds for impersonation.
Rather than reasoning “the impersonated user is an administrator, so the write must succeed,” you need to check what state the token you were actually handed is in.
When debugging, look at these angles.
- Group membership
- Whether each privilege is enabled or disabled
- The integrity level
- The elevation state
- Whether it is a restricted token
- Whether there is a linked elevated token
With Win32 APIs, GetTokenInformation lets you check TokenType, TokenImpersonationLevel, TokenElevationType, TokenIntegrityLevel, and so on.
As a practical design matter, though, confining the minimum necessary operations to a dedicated service or administration API is safer than “impersonate an administrator user and do whatever you like.”
18. Network Shares and the Double Hop
One of the most common impersonation questions we get is about accessing network shares.
Client PC
↓ Windows authentication
Web server / API server
↓ wants to impersonate and access
File server
In this setup, you can end up with “the user name resolves fine on the web server, but going out to the file server fails.”
This is the question of whether the user’s credentials can be re-delegated to another server.
Impersonating on the local server and delegating to another server are not the same thing.
The Impersonation level can be enough for local operations while still falling short of acting as the client toward a remote server.
If you want to use the end user’s own privileges across the network, you need a design covering Kerberos delegation, constrained delegation, SPNs, service accounts, and the authentication method.
This is where people tend to hit a dead end, so here are the options for the next move.
| Direction | What you do | Where it fits | What to watch for |
|---|---|---|---|
| Kerberos constrained delegation | Configure the relaying server’s account so it may delegate only to specific services | The relaying server and the file server are in the same domain and you can get the domain administrators’ cooperation | The configuration lives on the domain side, not in the application. You enumerate the delegation target SPNs explicitly |
| Resource-based constrained delegation | Put the setting that permits delegation on the account being delegated to, that is, on the file server side | Cross-domain cases, or when the administrators of the resource want to lead | It is a Windows Server 2012 and later mechanism. The place you configure it is the reverse of classic constrained delegation |
| Pass credentials explicitly | Connect with the credentials of a dedicated, purpose-limited account instead of the end user | You cannot configure delegation, or “acting as the actual user” is not a business requirement | You then have to store credentials. See the table in section 10 |
| Design the relay out | Consolidate file server access under a service account and authorize in the application | The business rules live in the application | The ACL is no longer the final decision. The audit log design becomes important |
| Let the client go direct | Have the client PC reach the file server directly instead of going through the server | Opening the shared folder from the UI is enough | You lose centralized control and log collection on the server side |
For the order of decisions, start by settling whether you really need to reach the file server with the end user’s own Windows privileges. If you do, move into the delegation design; if you do not, fall back to a design without the relay. Delegation settings cannot be completed by application developers alone, so once you decide you need them, the realistic move is to talk to the domain administrators early.
On the other hand, depending on the business requirements, you may not need the end user’s own Windows privileges on the file server at all.
In that case, a design like this is simpler.
Authenticate and authorize the user in the application
↓
Access the file server with a dedicated service account
↓
Record the user ID and the target file in the operation log
This makes the application’s authorization the final decision rather than the OS’s ACLs.
Which one is right depends on the business requirements.
What you must not do is leave it unclear which of the two you have chosen, so that impersonation, delegation, ACLs, and application authorization end up tangled together.
19. Token Handle Lifetimes
A token is a handle to a kernel object, so once you obtain one you have to close it as soon as you no longer need it.
In .NET, the basic approach is SafeAccessTokenHandle with using to manage the scope.
using SafeAccessTokenHandle token = NativeMethods.Logon(userName, domain, password);
string result = WindowsIdentity.RunImpersonated(token, () =>
{
return File.ReadAllText(path);
});
Here is a bad example.
// bad example: holding the token globally forever
private static SafeAccessTokenHandle? _cachedToken;
Holding a token for a long time leads to problems like these.
- Handle leaks
- You lose track of which operation uses which token
- Consistency with account disabling and privilege changes becomes hard to reason about
- It nudges you toward a design that retains authentication material for a long time
- It is hard to justify in an audit
The principle is this.
Obtain it when you need it
↓
Use it in the smallest region
↓
Always close it
Of course, authentication cost or operational requirements sometimes make caching worth considering. Even then, expiry, disposal, account changes, audit logging, and behavior on privilege changes all have to be part of the design.
20. What to Record in Audit Logs
For work that uses impersonation, log design matters too.
Make sure you can record at least the information in this table, and later investigation gets much easier.
| Item | Example |
|---|---|
| The requesting user | DOMAIN\alice |
| The account of the running process | DOMAIN\svc-app |
| The impersonated account | DOMAIN\alice, or a dedicated account |
| The target resource | File path, share name, registry key, and so on |
| The operation | Read, Write, Delete, CreateProcess, and so on |
| The result | Success, AccessDenied, Timeout, UnexpectedError |
| The error code | Win32 error code, HRESULT, exception type |
| The impersonation scope | Which method, which unit of work was impersonated |
There are also things that must never reach the log.
- Passwords
- Access token values
- Authentication headers
- Kerberos tickets or the credentials themselves
- File contents containing personal data
The purpose of the log is to let you trace, after the fact, on whose request, as which account, what was attempted, and how it failed or succeeded.
There is no need to record the credentials themselves.
21. Common Anti-Patterns
Here are the risky implementations you see most often around impersonation.
21.1 Impersonating the entire application
WindowsIdentity.RunImpersonated(token, () =>
{
RunEntireApplication();
});
Impersonate the whole application and you can no longer tell which operation runs with which privileges.
Limit impersonation to the specific I/O or the specific API calls that need it.
21.2 Reverting without finally
ImpersonateLoggedOnUser(token);
DoWork();
RevertToSelf();
This is dangerous because it does not revert when an exception is thrown.
Always use try / finally, or RunImpersonated.
21.3 Fire-and-forget while impersonated
WindowsIdentity.RunImpersonated(token, () =>
{
_ = Task.Run(DoWorkAsync);
});
You leave the impersonation scope before the work completes. Written this way, the task you launched keeps running impersonated, as section 14 explains, so the impersonated stretch in the code and the impersonated stretch in reality no longer line up. On top of that, nobody awaits it, so a failure produces no exception. If you impersonate directly with Win32, it goes the other way and runs as the process’s account.
If the work needs impersonation, await it inside RunImpersonatedAsync.
21.4 Judging success by the user name alone
Console.WriteLine(WindowsIdentity.GetCurrent().Name);
The user name can be exactly what you expected while the impersonation level or the privileges fall short.
Look at ImpersonationLevel, the target ACL, the logon type, and network delegation as well.
21.5 Impersonating an administrator account as a convenience account
A design along the lines of “this operation must not fail, so impersonate an administrator account” is dangerous.
Preparing a dedicated account with the minimum necessary privileges and narrowing the set of operations is safer.
21.6 Putting passwords in configuration files
{
"UserName": "DOMAIN\\admin",
"Password": "P@ssw0rd!"
}
This should be avoided.
If you have to handle credentials, use a mechanism that fits the environment: a secret store, Windows Credential Manager, DPAPI, a cloud Key Vault, or the secret management in your operations platform.
21.7 Treating process launching and file access as the same problem
For file access alone, an impersonation token can be enough.
If instead you want to launch a process as a different user, a separate set of concerns appears: primary tokens, profiles, desktops, environment variables, sessions, and privileges.
When you use CreateProcessAsUser or CreateProcessWithTokenW, treat it as a design problem distinct from impersonation.
22. Testing Angles
Verify impersonation code only in your own administrator environment and you will miss things.
At minimum, prepare these test cases.
| Case | What to verify |
|---|---|
| User with rights | Can read / write the target file |
| User without rights | Fails correctly as Access denied |
| Nonexistent user | Handled as an authentication failure |
| Wrong password | Fails without writing secrets to the log |
| Network share | Confirm the behavioral difference between local and UNC |
| Asynchronous work | Still runs within the expected region after await |
| Exception thrown | Impersonation is always undone |
| Concurrent requests | Impersonation of one user does not bleed into another |
| Running as a service | Runs under the real service account, not the developer’s interactive logon environment |
These two in particular are non-negotiable.
It succeeds
It fails when it should fail
For impersonation code, test not only the success case but also that a user without rights is reliably denied.
If an operation that should be denied succeeds, the impersonation or authorization design may well be wrong.
23. Implementation Checklist
Run through this checklist before and after implementation.
| Angle | What to check |
|---|---|
| Purpose | Can you explain why impersonation is needed |
| Alternatives | Did you consider whether a service account or application authorization would do |
| Extent | Is the impersonation scope minimal |
| Reverting | Does it reliably revert on exceptions too |
| Async | Do you await to completion inside RunImpersonatedAsync |
| Tokens | Are you confusing primary tokens and impersonation tokens |
| Impersonation level | Are you distinguishing Identification from Impersonation / Delegation |
| Network | Did you check whether UNC, the double hop, or Kerberos delegation are in play |
| UAC | Are you confusing Administrators membership with being elevated |
| Secrets | Are passwords stored in clear text |
| Handles | Are you closing SafeAccessTokenHandle with using |
| Logging | Can you trace the user, the impersonated account, the target, and the result |
| Testing | Did you verify with rights / without rights / exceptions / concurrency |
If a lot of items on this checklist snag, it is better to revisit the design before writing code.
24. Knowing When to Use It
Impersonation tokens are powerful, but they are not the first tool to reach for.
As a design matter, separating the cases keeps you from second-guessing yourself.
24.1 When you want to use the OS’s ACLs directly
If the ACLs on a file server or shared folder are the center of the business rules and the application should follow that decision, impersonating as the end user is meaningful.
Access as the end user
↓
Windows ACLs make the final decision
In this case, the design has to cover Windows authentication, impersonation levels, delegation, and the network topology.
24.2 When the application should authorize
If the business rules live in the application and the file server or database is under the application’s control, accessing with a service account and authorizing in the application can be easier to reason about.
Authenticate the user
↓
Authorize in the application
↓
Access the resource with a service account
↓
Record the user ID in the audit log
This approach forgoes impersonation, and in exchange the application’s authorization logic and its audit log become critical.
24.3 When you need administrative operations
Rather than running administrative operations directly with the user’s token, it is often safer to provide a dedicated service or API for them and do the authorization, input validation, auditing, and rollback there.
Client
↓
Request to the administration API
↓
The administration API authorizes
↓
The operation runs with the minimum necessary privileges
↓
Audit log
“Just impersonate an administrator” looks easy in the short term.
In the long term, though, it makes auditing, incident investigation, privilege changes, and security reviews painful.
25. Conclusion
Windows impersonation tokens are an important mechanism for using Windows privilege management correctly.
They are also an area where, used without understanding, the code easily looks like it works while being dangerous.
Here are the points to hold on to.
- An access token represents a security context: the user, the groups, the privileges, and more
- A process has a primary token
- An impersonation token attaches mainly to a thread and is used in access checks
- Impersonation is not privilege escalation
- Primary tokens and impersonation tokens serve different purposes
- The impersonation level decides whether you can merely identify, actually access, or delegate to a remote system
- If you impersonate through Win32 APIs, always
RevertToSelfin atry/finally - In .NET, express a small scope with
WindowsIdentity.RunImpersonated/RunImpersonatedAsync - If you use
LogonUser, mind credential management and the logon type - For network shares, Kerberos delegation and service account design matter as much as impersonation
- Manage token handles with
SafeAccessTokenHandleandusing - Test not just the success cases but also the cases that should be denied
What matters in an impersonation implementation is not the single fact that “it ran as a different user,” but being able to explain the following.
Which operation
on whose request
as which account
over which region only
where it reverts
and how success and failure are recorded
Get that far and impersonation stops being a frightening mechanism.
It becomes a practical tool for making the most of Windows ACLs, service accounts, existing file servers, and in-house domain assets.
References
- The complete sample code for this article: library, demo, and unit tests
https://github.com/gomurin0428/komurasoft-blog-samples/tree/main/windows-impersonation-token - Microsoft Learn: Access Tokens
https://learn.microsoft.com/en-us/windows/win32/secauthz/access-tokens - Microsoft Learn: Impersonation Tokens
https://learn.microsoft.com/en-us/windows/win32/secauthz/impersonation-tokens - Microsoft Learn: Impersonation Levels
https://learn.microsoft.com/en-us/windows/win32/secauthz/impersonation-levels - Microsoft Learn:
SECURITY_IMPERSONATION_LEVELenumeration
https://learn.microsoft.com/en-us/windows/win32/api/winnt/ne-winnt-security_impersonation_level - Microsoft Learn:
ImpersonateLoggedOnUserfunction
https://learn.microsoft.com/en-us/windows/win32/api/securitybaseapi/nf-securitybaseapi-impersonateloggedonuser - Microsoft Learn:
RevertToSelffunction
https://learn.microsoft.com/en-us/windows/win32/api/securitybaseapi/nf-securitybaseapi-reverttoself - Microsoft Learn:
LogonUserfunction
https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-logonusera - Microsoft Learn:
DuplicateTokenExfunction
https://learn.microsoft.com/en-us/windows/win32/api/securitybaseapi/nf-securitybaseapi-duplicatetokenex - Microsoft Learn:
TOKEN_INFORMATION_CLASSenumeration
https://learn.microsoft.com/en-us/windows/win32/api/winnt/ne-winnt-token_information_class - Microsoft Learn:
WindowsIdentity.RunImpersonated
https://learn.microsoft.com/en-us/dotnet/api/system.security.principal.windowsidentity.runimpersonated - Microsoft Learn:
WindowsIdentity.RunImpersonatedAsync
https://learn.microsoft.com/en-us/dotnet/api/system.security.principal.windowsidentity.runimpersonatedasync - The .NET runtime implementation:
WindowsIdentity.cs, with theAsyncLocalthat holds the impersonation token and theCurrentImpersonatedTokenChangedhandler that re-applies impersonation on a thread switch
https://github.com/dotnet/runtime/blob/main/src/libraries/System.Security.Principal.Windows/src/System/Security/Principal/WindowsIdentity.cs - Microsoft Learn: Configure Windows Authentication in ASP.NET Core
https://learn.microsoft.com/en-us/aspnet/core/security/authentication/windowsauth - Microsoft Learn: Kerberos Constrained Delegation Overview
https://learn.microsoft.com/en-us/windows-server/security/kerberos/kerberos-constrained-delegation-overview - Microsoft Learn:
ProtectedDataClass
https://learn.microsoft.com/en-us/dotnet/api/system.security.cryptography.protecteddata
Related Articles
Recent articles sharing the same tags. Deepen your understanding with closely related topics.
End of Servicing for Windows Printer Drivers — How Business Apps Should Prepare Their Report and Label Printing
Microsoft is phasing out v3/v4 printer drivers. What Windows protected print mode removes, and how to inventory and prepare report and la...
The Depths of Windows I/O (Part 4) — Cache Manager: When Does Your WriteFile Actually Reach the Disk?
Part 4 of an illustrated series on the Windows cache manager. It covers the cache implemented as a file mapping, read-ahead and lazy writ...
The Depths of Windows I/O (Part 2) — Synchronous and Asynchronous I/O: What OVERLAPPED Really Means
Part 2 of a series explaining Windows synchronous and asynchronous I/O (overlapped I/O) with diagrams. It covers what FILE_FLAG_OVERLAPPE...
The Depths of Windows I/O (Part 1) — Every Read and Write Becomes an IRP: The Big Picture of the I/O System
Part 1 of a series that explains the Windows I/O system from the ground up. We map out the Object Manager namespace, the three kinds of o...
Integrating Entra ID Authentication into WinForms/WPF Apps — A Practical Architecture with MSAL.NET and the WAM Broker
A practical, hands-on look at integrating Entra ID (formerly Azure AD) authentication into WinForms/WPF desktop apps: the public client m...
Related Topics
These topic pages place the article in a broader service and decision context.
Windows Technical Topics
Topic hub for KomuraSoft LLC's Windows development, investigation, and legacy-asset articles.
Where This Topic Connects
This article connects naturally to the following service pages.
Windows App Development
We support Windows desktop applications that involve resident processing, device integration, operational logging, and maintainable structure.
Frequently Asked Questions
Common questions about the topic of this article.
- What is a Windows impersonation token?
- An impersonation token is a token attached to a thread so that the thread undergoes access checks in a different security context. The whole process does not become another user; only the impersonating thread has its access to files, the registry, and other objects checked with that user's privileges. It is not magic that makes you an administrator, that is, it is not privilege escalation, but a mechanism that lets part of the work inside a server process be access-checked with the client user's privileges.
- How do primary tokens and impersonation tokens differ?
- A primary token represents a process's security context and is used to launch processes with APIs such as CreateProcessAsUser. An impersonation token is what a thread uses to run in a different security context, and it shows up in ImpersonateLoggedOnUser and in Named Pipe client impersonation. If you want to launch a process, you generally need a primary token; if all you have is an impersonation token, the flow is to create a primary token with DuplicateTokenEx. Confuse the two and you end up chasing errors such as Access denied.
- Why do I get Access denied even though I am impersonating?
- There are several angles to check. Inside the impersonation scope, look not only at the Name from WindowsIdentity.GetCurrent() but also at ImpersonationLevel, and confirm it is Impersonation or higher rather than Identification. Also check the target resource's ACL, whether the actual I/O runs outside the impersonation scope or on a separate task, whether this is the double-hop problem where local access succeeds and only UNC fails, and whether an unelevated token from UAC is involved. The name can be exactly what you expect while the privileges are still insufficient.
- How should I implement impersonation safely in .NET?
- The basic approach is to use WindowsIdentity.RunImpersonated / RunImpersonatedAsync and confine the impersonation scope inside the lambda. Await asynchronous work that needs impersonation inside RunImpersonatedAsync, and do not launch fire-and-forget tasks from inside the impersonation scope. When you impersonate through Win32 APIs, always call RevertToSelf in a try/finally. Keep the impersonation scope down to just the I/O that needs it, and manage token handles with SafeAccessTokenHandle and using.