Handling Windows Impersonation Tokens Correctly — Borrowing Privileges per Thread and Reverting Safely
· Go Komura · Windows, Security, Access Token, Impersonation, Win32, .NET, C#, Operations, Legacy Asset Reuse
1. What to Understand First
When building Windows applications or Windows services, you sometimes hit 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 app, 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
- Due to existing assets, switch the Windows account used per 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 run while still impersonated, blurring the privilege boundary
- Primary tokens and impersonation tokens get confused, and process launching fails
- You get stuck on “the user is in Administrators — 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 escalation. 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 to run on Windows, and unit tests for argument validation and guard behavior).
windows-impersonation-token - komurasoft-blog-samples (GitHub)
2. What Is an Access Token?
In Windows, an access token represents the security context of a user or process.
An access token contains roughly this information:
- The user’s SID
- Group memberships
- Rights and privileges
- Default owner
- Default DACL
- Restricted SIDs
- Integrity level
- Elevation state
- Impersonation level
- Token type
Many Windows objects have security descriptors: files, registry keys, services, named pipes, processes, threads, events, mutexes, and more.
When a thread tries to open a protected object, Windows checks the token’s information against the target object’s ACL.
The operating thread
↓
Which security context is used for the access?
↓
Look at the token's user, groups, and privileges
↓
Check them against the target object's ACL
↓
Decide allow / deny
Understanding this “which security context is used for the access” is the entry point to 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 carries a primary token representing that user’s security context.
For a Windows service, the process carries the primary token of the service’s run-as account.
Picture it like this:
MyService.exe
Primary Token: DOMAIN\svc-app
If a thread inside this service is not impersonating anything in particular, the process’s primary token is used when accessing files and the registry.
In other words, by default:
Thread A
Impersonation Token: none
↓
Access checks use the Process's Primary Token
Open C:\Data\foo.txt in this state, and the check is whether DOMAIN\svc-app has access rights.
4. Impersonation Tokens Attach to Threads
When impersonation begins, 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
Now, when Thread A opens a file, the access check uses DOMAIN\alice’s privileges.
Thread B, meanwhile, is not impersonating, so it is checked with DOMAIN\svc-app’s privileges.
Misunderstanding this leads to confusion like this:
// Thought we were impersonating on Thread A
StartImpersonation(token);
// But the work is dispatched to another thread
Task.Run(() =>
{
File.ReadAllText(path);
});
// And we revert right away
RevertToSelf();
In this case, the thread that actually reads the file does not necessarily run in the expected impersonation state.
Impersonation must be handled with a clear view of its relationship to scopes, threads, and asynchronous work.
5. “Impersonation” Is Not Privilege Escalation
The word “impersonation” sounds a bit dramatic, but what matters in practice is not confusing it with “privilege escalation.”
What impersonation enables is fundamentally this:
Run with the server process's privileges
↓
For part of the work only, run access checks with the client user's privileges
For example, if you want to use a file server’s ACLs as the authorization mechanism, but the server application always reads files as its service account, per-user ACLs are never reflected.
So instead, you impersonate the calling user for just the file-access portion of request handling:
HTTP / RPC / Named Pipe request
User: DOMAIN\alice
↓
Server application
Process: DOMAIN\svc-app
↓
Impersonate DOMAIN\alice for the file access portion only
↓
The file server's ACL allows / denies
This is useful when you want to rely on Windows’s existing ACLs rather than application-specific authorization logic.
But while impersonation is convenient, bad design quickly blurs privilege boundaries:
- Which operation is running as whom?
- Where did impersonation start?
- Where is it reliably reverted?
- Which log lines are being written under which user’s privileges?
- Does it revert on exceptions too?
- Does impersonation remain effective to the end of asynchronous work?
Making all of this explicit in the code is essential.
6. Keep Primary Tokens and Impersonation Tokens Separate in Your Head
Among Windows tokens, the easiest pair to confuse is the primary token and the impersonation token.
Broadly:
| Token | Main use | Representative examples |
|---|---|---|
| Primary token | Represents a process’s security context | Process launch, CreateProcessAsUser |
| Impersonation token | Lets a thread operate in a different security context | ImpersonateLoggedOnUser, SetThreadToken, Named Pipe client impersonation |
The especially important point: if you want to launch a process, you generally need a primary token.
Holding an impersonation token does not mean it can be used directly to launch 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 etc.
Conversely, if the goal is “perform only file access as a different user on this thread,” that is an impersonation token matter, not process launching.
Mix the two up, and you will be puzzling over errors like Access denied or The parameter is incorrect even though the API arguments look right.
7. Understand Impersonation Levels
Impersonation tokens have an impersonation level.
There are four representative levels:
| Impersonation level | Rough meaning |
|---|---|
| Anonymous | The server cannot obtain the client’s identity |
| Identification | The server can identify the client but cannot use its privileges to access objects |
| Impersonation | The server can operate as the client on the local system |
| Delegation | The server can also delegate the client’s privileges to remote systems |
The one people get stuck on most in practice is the difference between Identification and Impersonation.
Identification, as the name suggests, is a level for learning who the peer is.
It is insufficient for opening files with that user’s privileges.
Which leads to this:
WindowsIdentity.GetCurrent().Name looks like the expected user name
↓
But file access comes back Access denied
In this case, you must check the impersonation level as well, not just the name.
In .NET, WindowsIdentity.ImpersonationLevel offers a clue:
using System.Security.Principal;
WindowsIdentity identity = WindowsIdentity.GetCurrent();
Console.WriteLine(identity.Name);
Console.WriteLine(identity.ImpersonationLevel);
Access across the network requires even more care.
In configurations like “impersonate the user on a web server, then access another file server or DB server as that user,” you may run into the so-called double-hop problem.
In that case, impersonating in application code alone does not necessarily solve it. The design must encompass Kerberos, SPNs, delegation, constrained delegation, service accounts, and the target’s authentication scheme.
8. The Basic Shape of Impersonation
The conceptual shape when handling impersonation with the Win32 API is:
1. Obtain the token to impersonate with
2. Impersonate the current thread with that token
3. Execute only the necessary work
4. Always return to the original security context
5. Close the token handle
In code, always use try / finally.
if (!ImpersonateLoggedOnUser(tokenHandle))
{
throw new Win32Exception(Marshal.GetLastWin32Error());
}
try
{
// Run only this part as the impersonated user
DoWorkAsImpersonatedUser();
}
finally
{
if (!RevertToSelf())
{
// Failing to revert is dangerous; at minimum, do not continue processing
throw new Win32Exception(Marshal.GetLastWin32Error());
}
}
What matters even more than starting the impersonation is reverting reliably. Forget to revert, and subsequent work on that thread runs as the impersonated user.
Especially in applications using thread pools, what was meant to be a single operation can leak into other requests and other work.
So think of impersonation not as “start it, then revert,” but as confining it to a small scope.
9. In .NET, Use WindowsIdentity.RunImpersonated
In .NET, where possible, WindowsIdentity.RunImpersonated makes the impersonation scope easy to express in code.
If you hold a SafeAccessTokenHandle, you can write:
using Microsoft.Win32.SafeHandles;
using System.Security.Principal;
static string ReadFileAsUser(SafeAccessTokenHandle token, string path)
{
return WindowsIdentity.RunImpersonated(token, () =>
{
return File.ReadAllText(path);
});
}
The virtue of this shape is that the impersonated region is enclosed within the lambda.
RunImpersonated(token, () =>
{
// impersonated only here
});
// outside this, the original context
When you want to impersonate only for specific work — file access, registry access, calls into an existing library — this form is 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 fire-and-forget tasks from inside the impersonation scope:
// Bad example
WindowsIdentity.RunImpersonated(token, () =>
{
_ = Task.Run(() =>
{
File.WriteAllText(path, text);
});
});
This code makes it unclear when, and in which execution context, “the work that actually writes the file” runs.
For asynchronous work that must run impersonated, await it inside RunImpersonatedAsync and exit the scope only after the work completes.
10. Obtaining a Token with LogonUser
The classic API for obtaining a token from another user’s credentials is LogonUser — but it is an API to handle with caution.
LogonUser takes a username, domain, and password.
That means the application itself is handling credentials.
In practice, mind the following:
- Never put passwords in plain text in code or configuration files
- Prefer OS authentication, service accounts, delegation, or existing Windows authentication where possible
- Manage secrets in a proper secret store or operations platform
- Always close token handles
- Never log secrets other than the username
- Minimize the impersonated region
Here is a 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 purely to show the shape of the API.
In practice, first ask “should the application be receiving a password at all?”
In most cases, these alternatives are safer:
| What you want | Alternative |
|---|---|
| The whole service needs access to a specific resource | Grant a dedicated service account the minimum necessary ACLs |
| Access files with the end user’s own privileges | Use Windows authentication and a delegation design |
| Perform operations requiring administrator privileges | Provide an explicit admin API on the service side, controlled by app-side authorization |
| Run only part of the work as another account | Confine the impersonation scope to a method and keep audit logs |
11. Mind the Logon Type in LogonUser
The nature of the token obtained from LogonUser changes with the logon type.
Copy-paste without understanding this distinction, and it will not behave as expected.
| Logon type | Caveats |
|---|---|
| Interactive | Close to an interactive logon. Easy to use for local operations, but dependent on the runtime environment and rights |
| Network | For network logons. The returned token may not be directly usable for launching processes |
| NewCredentials | Locally close to the current credentials; sometimes used to apply the specified credentials only for remote connections |
The point here is not to memorize specific logon types.
Two things matter:
- The logon type changes behavior for local access, network access, and process launching
- You must verify whether the returned token is a primary token or an impersonation token
A classic source of confusion: a token obtained with LOGON32_LOGON_NETWORK is passed directly to CreateProcessAsUser and fails.
If your goal is to launch a process, you need a primary token.
The design then involves creating one with DuplicateTokenEx as needed.
12. Do Not Take RevertToSelf Lightly
If you start impersonation via the Win32 API, you end it with RevertToSelf. This revert is not mere cleanup — it is the critical step that restores the security boundary.
A bad example:
ImpersonateLoggedOnUser(token);
DoWork();
RevertToSelf();
It looks fine at a glance, but if DoWork() throws, RevertToSelf() never runs.
Always use finally:
if (!ImpersonateLoggedOnUser(token))
{
throw new Win32Exception(Marshal.GetLastWin32Error());
}
try
{
DoWork();
}
finally
{
if (!RevertToSelf())
{
throw new Win32Exception(Marshal.GetLastWin32Error());
}
}
Continuing on after RevertToSelf fails is also dangerous. If you proceed in a state where you may not have returned to the original privileges, subsequent work continues under an unintended user’s privileges. At minimum, treat that unit of work as failed and err on the safe side.
.NET’s RunImpersonated / RunImpersonatedAsync are useful precisely as scope expressions that prevent this revert-forgetting.
13. Keep the Impersonation Scope as Small as Possible
The single most important design principle for impersonation: impersonate only the work that needs it.
A bad example:
WindowsIdentity.RunImpersonated(token, () =>
{
ValidateRequest();
LoadConfiguration();
WriteDebugLog();
ReadUserFile();
UpdateDatabase();
SendNotification();
});
Impersonating this broadly makes it unclear which operation runs under which privileges.
For example, access to the log destination might happen under 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. Even notification and temp-file creation may suffer from an unnecessary privilege context.
The good shape extracts only the operations that need impersonation:
ValidateRequest();
LoadConfiguration();
string content = WindowsIdentity.RunImpersonated(token, () =>
{
return File.ReadAllText(userFilePath);
});
UpdateDatabase(content);
WriteAuditLog(userName, userFilePath, success: true);
In this shape, it is clear that only the File.ReadAllText portion needs impersonation.
Impersonation is convenient, but the wider you spread it, the harder it is to read and the easier it is to have an accident.
14. In Async Code, Verify “Is It Still Impersonated Until It Finishes?”
In modern .NET applications, much of the work is asynchronous: files, HTTP, DB, queues, storage.
So combining impersonation with async / await requires care.
The basic policy is just this:
Asynchronous work that needs impersonation is awaited inside RunImpersonatedAsync
A good example:
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 with this shape, though, there is still something to consider:
Does ProcessTextAsync need to run as the impersonated user at all?
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);
Just because you can await inside the impersonation scope does not mean everything belongs there.
In asynchronous code too, minimize the impersonated region.
15. ASP.NET Core and Impersonation
When using Windows authentication in ASP.NET Core, impersonation also requires care.
It is dangerous to assume “the user is logged in via Windows authentication, so the entire request runs as that user.”
In general, the application process itself runs as the application pool identity or the service’s run-as account. The user’s Windows identity is available as authentication information, but that does not mean all processing automatically runs with the user’s privileges.
If you need to perform a specific action with the user’s privileges, create an explicit scope with RunImpersonated / RunImpersonatedAsync.
The code looks 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 checks all belong inside the impersonation scope deserves careful thought.
16. How to Read Access denied
The error you see most in impersonation-based implementations is Access denied.
When it appears, thinking only “the impersonation failed” sends you the long way around.
Split the investigation into these angles:
| Angle | What to check |
|---|---|
| Are you actually impersonating? | Check WindowsIdentity.GetCurrent().Name inside the impersonation scope |
| Is the impersonation level sufficient? | Is it the required level, not Identification? |
| Is the target resource’s ACL correct? | Does the impersonated user have read / write rights? |
| Local or remote? | Does it succeed on local files but fail only on UNC paths? |
| Is this a double hop? | Are you trying to go from a web server to a file server with the user’s privileges? |
| Have you escaped the impersonation scope? | Is the actual I/O running outside the scope or on another task? |
| Is the token type right? | Are you passing an impersonation token to a process launch? |
| Any UAC / integrity level effects? | Even with Administrators membership, is the token unelevated? |
Above all, do not relax just because the name looks right.
WindowsIdentity identity = WindowsIdentity.GetCurrent();
Console.WriteLine(identity.Name);
This log line is useful, but not sufficient.
At minimum, also look at:
Console.WriteLine(identity.ImpersonationLevel);
Console.WriteLine(identity.IsAuthenticated);
And if the target is a network share, check not only application code but also the authentication scheme, delegation settings, SPNs, service accounts, and the file server’s ACLs.
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.
In environments with UAC enabled, even administrator users normally run processes with a restricted token, and operations requiring administrator privileges need elevation.
So this happens:
The user belongs to Administrators
↓
But the current token is unelevated
↓
Writes to Program Files or HKLM return Access denied
The same applies to impersonation.
Do not reason “the impersonated user is an administrator, so the write should work” — verify the actual state of the token being passed.
When debugging, examine:
- Group memberships
- Which privileges are enabled / disabled
- Integrity level
- Elevation state
- Whether it is a restricted token
- Whether a linked elevated token exists
With the Win32 API, 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 admin API is safer than “impersonate an administrator user and do anything.”
18. Network Shares and the Double Hop
One of the most frequent impersonation questions concerns access to network shares.
Client PC
↓ Windows authentication
Web server / API server
↓ wants to impersonate and access
File server
In this configuration, you may find “the user name is available on the web server, yet going to the file server fails.”
This is the question of whether the user’s credentials can be re-delegated to another server.
Impersonation on the local server and delegation to another server are not the same thing.
An Impersonation-level token may serve for local operations yet be insufficient for 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 encompassing Kerberos delegation, constrained delegation, SPNs, service accounts, and authentication schemes.
On the other hand, depending on business requirements, you may not need to reach the file server with the end user’s own Windows privileges at all.
In that case, this design is simpler:
Authenticate and authorize the user in the application
↓
Access the file server with a dedicated service account
↓
Record the user ID and target file in the operation log
This is the design where “the application’s authorization is the final word,” rather than “the OS’s ACLs are the final word.”
Which is correct depends on the business requirements.
But if you do not make explicit which approach you have chosen, impersonation, delegation, ACLs, and app-side authorization blur together into something hard to reason about.
19. Token Handle Lifetimes
A token is a handle to a kernel object, so once obtained it must be closed when no longer needed.
In .NET, the rule is to use SafeAccessTokenHandle and manage the scope with using.
using SafeAccessTokenHandle token = NativeMethods.Logon(userName, domain, password);
string result = WindowsIdentity.RunImpersonated(token, () =>
{
return File.ReadAllText(path);
});
A bad example:
// Bad example: holding the token globally forever
private static SafeAccessTokenHandle? _cachedToken;
Holding tokens long-term leads to problems like these:
- Handle leaks
- Losing track of which work is using which token
- Unclear consistency with account disablement and permission changes
- A design that tends toward holding credentials for a long time
- Hard to explain in an audit
The principle:
Obtain when needed
↓
Use within the smallest scope
↓
Always close
Of course, depending on authentication cost and operational requirements, caching may be worth considering. Even then, the design must include expiration, disposal, account changes, audit logging, and handling of permission changes.
20. What to Record in Audit Logs
For work that uses impersonation, log design matters too.
At minimum, being able to record the information in this table makes later investigation much easier:
| Item | Example |
|---|---|
| Requesting user | DOMAIN\alice |
| Executing process’s account | DOMAIN\svc-app |
| Impersonated account | DOMAIN\alice or a dedicated account |
| Target resource | File path, share name, registry key, etc. |
| Operation | Read, Write, Delete, CreateProcess, etc. |
| Result | Success, AccessDenied, Timeout, UnexpectedError |
| Error code | Win32 error code, HRESULT, exception type |
| Impersonation scope | Which method, which unit of work was impersonated |
There are also things that must never appear in logs:
- Passwords
- Access token values
- Authentication headers
- Kerberos tickets or the credentials themselves
- File contents containing personal information
The purpose of the log is to be able to trace, after the fact, “at whose request, as which account, what was attempted, and how it failed or succeeded.”
You never need to record the credentials themselves.
21. Common Anti-Patterns
Here are the dangerous implementations seen most often around impersonation.
21.1 Impersonating the entire application
WindowsIdentity.RunImpersonated(token, () =>
{
RunEntireApplication();
});
Impersonate the entire application and you can no longer tell which operation runs under which privileges.
Limit impersonation to the necessary I/O and specific API calls.
21.2 Reverting without finally
ImpersonateLoggedOnUser(token);
DoWork();
RevertToSelf();
Dangerous because it does not revert on exceptions.
Always use try / finally, or RunImpersonated.
21.3 Fire-and-forget while impersonated
WindowsIdentity.RunImpersonated(token, () =>
{
_ = Task.Run(DoWorkAsync);
});
The impersonation scope is exited before the work completes, so it may run under unexpected privileges.
If the work needs impersonation, await it inside RunImpersonatedAsync.
21.4 Judging success by the user name alone
Console.WriteLine(WindowsIdentity.GetCurrent().Name);
Even with the expected user name, the impersonation level or privileges may be insufficient.
Also look at ImpersonationLevel, the target ACLs, the logon type, and network delegation.
21.5 Impersonating an administrator account as a convenience account
The design “we don’t want this operation to fail, so impersonate an administrator account” is dangerous.
It is safer to prepare a dedicated account with the minimum necessary privileges and narrow the operations.
21.6 Putting passwords in configuration files
{
"UserName": "DOMAIN\\admin",
"Password": "P@ssw0rd!"
}
Avoid this.
If you must handle credentials, use a mechanism suited to your environment: a secret store, Windows Credential Manager, DPAPI, a cloud Key Vault, or your operations platform’s secret management.
21.7 Treating process launching and file access as the same problem
For file access alone, an impersonation token may suffice.
But launching a process as another user raises a different set of concerns: primary tokens, profiles, desktops, environment variables, sessions, privileges.
If you use CreateProcessAsUser or CreateProcessWithTokenW, treat it as a separate design from impersonation.
22. Testing Angles
Verifying impersonation only in your local administrator environment leads to blind spots.
At minimum, prepare these test cases:
| Case | What to verify |
|---|---|
| User with permission | Can read / write the target file |
| User without permission | Fails correctly with Access denied |
| Nonexistent user | Handled as an authentication failure |
| Wrong password | Fails without leaking secrets into logs |
| Network share | Verify behavior differences between local and UNC |
| Asynchronous work | Runs in the expected scope even after await |
| Exception thrown | Impersonation is always released |
| Concurrent requests | Other users’ impersonations never bleed into each other |
| Running as a service | Works under the real service account, not the developer’s interactive logon |
Two of these are non-negotiable:
It succeeds
It fails when it should fail
For impersonation, test not only the success cases but also that users without permission are reliably denied.
If an operation that should be denied succeeds, the impersonation or authorization design is probably wrong.
23. Implementation Checklist
Before and after implementing, run through this checklist:
| Angle | Check |
|---|---|
| Purpose | Can you explain why impersonation is needed? |
| Alternatives | Did you consider whether a service account or app-side authorization would do? |
| Scope | Is the impersonation scope minimal? |
| Reverting | Does it reliably revert on exceptions too? |
| Async | Is everything awaited to completion inside RunImpersonatedAsync? |
| Tokens | Are primary and impersonation tokens kept distinct? |
| Impersonation level | Are you distinguishing Identification from Impersonation / Delegation? |
| Network | Did you confirm whether UNC, double hop, or Kerberos delegation is involved? |
| UAC | Are you conflating Administrators membership with being elevated? |
| Secrets | Are passwords stored in plain text anywhere? |
| Handles | Is SafeAccessTokenHandle closed via using? |
| Logging | Can you trace the user, the impersonated account, the target, and the result? |
| Tests | Did you verify with / without permission, exceptions, and concurrency? |
If many items on this checklist trip you up, it is better to revisit the design before writing the code.
24. Knowing When to Use It
Impersonation tokens are powerful, but they are not always the tool to reach for first.
As a design matter, thinking in terms of use cases keeps you from going astray.
24.1 When you want to use the OS’s ACLs directly
If the ACLs on file servers and shared folders are the heart of the business rules, and the application should defer to that judgment, impersonating as the end user is meaningful.
Access as the end user
↓
Windows ACLs make the final decision
In this case, design everything together: 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 and DB are under the application’s control, accessing with a service account and authorizing in the application is often clearer.
Authenticate the user
↓
Authorize in the application
↓
Access resources with the service account
↓
Record the user ID in the audit log
In this approach, instead of impersonation, the application’s authorization logic and audit logs carry the weight.
24.3 When you need administrative operations
Rather than executing administrative operations directly with the user’s token, it is usually safer to provide a dedicated administrative service or API that performs authorization, input validation, auditing, and rollback.
Client
↓
Request to the admin API
↓
The admin API authorizes
↓
Operates with the minimum necessary privileges
↓
Audit log
“Just impersonate an administrator” looks easy in the short term.
In the long term, it makes auditing, incident investigation, permission changes, and security reviews painful.
25. Conclusion
Windows impersonation tokens are an important mechanism for using Windows’s privilege management correctly.
At the same time, this is an area where code used without understanding tends to look like it works while actually being dangerous.
The key points:
- An access token represents a security context: user, groups, privileges, and more
- A process has a primary token
- Impersonation tokens attach mainly to threads and are used in access checks
- Impersonation is not privilege escalation
- Primary tokens and impersonation tokens serve different purposes
- The impersonation level determines whether you can merely identify, actually access, or delegate to remote systems
- When impersonating via the Win32 API, always
RevertToSelfintry/finally - In .NET, express small scopes with
WindowsIdentity.RunImpersonated/RunImpersonatedAsync - When using
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 success cases but the cases that should be denied
What matters in an impersonation implementation is not the single fact that “it ran as another user,” but being able to explain all of this:
Which operation,
at whose request,
as which account,
within which scope only,
where it was reverted,
and how success and failure are recorded.
If you have all of this sorted out, impersonation is nothing to be afraid of.
It becomes a practical tool for making the most of Windows ACLs, service accounts, existing file servers, and your in-house domain assets.
References
- The complete sample code for this article (library, demo, 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 - Microsoft Learn: Configure Windows Authentication in ASP.NET Core
https://learn.microsoft.com/en-us/aspnet/core/security/authentication/windowsauth
Related Articles
Recent articles sharing the same tags. Deepen your understanding with closely related topics.
What to Do Before Disposing of a Windows PC — A Practical Checklist for Data Erasure, Account Unlinking, and Backups
What to do before disposing of, transferring, selling, or returning a leased Windows PC — covering backups, data erasure, BitLocker, Micr...
What Is a PDB (Program Database)? — Understanding Debug Information, Symbols, and Source Link
What a PDB (Program Database) is, what it does and does not contain, and how it relates to Debug / Release, Portable PDBs, Source Link, s...
Windows App Outsourcing and Contract Development: What to Sort Out Before You Ask
Before commissioning Windows app outsourcing or contract development, here is how to sort out existing software modification, device inte...
The Misconception That TCP Lets You Receive in the Same Units You Send — Designing Reception Around a Byte Stream
Assuming TCP delivers data in the same units as Send or Write leads to fragmentation, coalescing, garbled text, and broken protocols. Thi...
Telling GC Lag from a Memory Leak in .NET — A Practical Procedure for Observing, Comparing, and Proving Memory Growth
A procedure for determining whether growing memory in a .NET application is simply waiting on garbage collection or a genuine memory leak...
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.
Author Profile
Profile page for the article author.
Go Komura
Representative of KomuraSoft LLC
Focused on Windows software development, technical consulting, and investigations into failures that are difficult to reproduce.
Public links