A native C++ DLL plugin for Inno Setup that reliably retrieves the original desktop user's identity during elevated installations.
Β© 2008-2026 Rizonetech (Pty) Ltd. All rights reserved.
When an Inno Setup installer runs with administrative privileges (via UAC elevation), the standard GetUserNameString() function returns the administrative account used for elevationβnot the user who launched the installer.
This is a critical problem when you need to:
- Deploy configuration files to the user's
%AppData%directory - Create user-specific settings during installation
- Clean up the correct user profile during uninstallation
This issue is especially problematic in "Over-the-Shoulder" elevation scenarios, where a non-admin user enters an administrator's credentials to elevate. In this case, GetUserNameString() returns the admin's username, causing configuration files to be created in the wrong profile.
GetOriginalUser uses the Windows Terminal Services (WTS) Session API to query who owns the current desktop session. This approach:
- β Works regardless of elevation credentials used
- β Uses only legitimate, documented Windows APIs
- β Zero antivirus false positive risk
- β Full RDP/Remote Desktop support
π See docs/Architecture.md for technical details.
Download the DLL from the Releases page.
- OriginalUser_x86.dll (32-bit): Use this for Inno Setup β all Inno Setup installers are 32-bit
- OriginalUser_x64.dll (64-bit): For other 64-bit installer frameworks (NSIS, WiX, custom tools)
[Files]
; Embed the DLL - 'dontcopy' keeps it in temp
Source: "OriginalUser_x86.dll"; DestName: "OriginalUser.dll"; Flags: dontcopy
[Code]
// Import the function
procedure GetOriginalUserDLL(Buffer: String; BufSize: Integer);
external 'GetOriginalUser@files:OriginalUser.dll stdcall delayload';
// Wrapper function with proper string handling
function GetOriginalUser(): String;
var
Buffer: String;
begin
SetLength(Buffer, 256);
try
GetOriginalUserDLL(Buffer, 256);
except
Result := '';
Exit;
end;
// Clean up null terminator
if Pos(#0, Buffer) > 0 then
Result := Copy(Buffer, 1, Pos(#0, Buffer) - 1)
else
Result := Trim(Buffer);
// Fallback if detection failed
if Result = '' then
Result := GetUserNameString();
end;
procedure InitializeWizard;
begin
MsgBox('Process User: ' + GetUserNameString() + #13#10 +
'Desktop User: ' + GetOriginalUser(), mbInformation, MB_OK);
end;| Function | Description | Return Format |
|---|---|---|
GetOriginalUser |
Full user identity | DOMAIN\Username |
GetOriginalUserName |
Username only | Username |
GetOriginalUserSID |
User's SID string | S-1-5-21-... |
GetOriginalUserAppData |
Roaming AppData path | C:\Users\...\AppData\Roaming |
GetOriginalUserLocalAppData |
Local AppData path | C:\Users\...\AppData\Local |
GetOriginalUserDocuments |
Documents folder path | C:\Users\...\Documents |
GetOriginalUserDesktop |
Desktop folder path | C:\Users\...\Desktop |
GetOriginalUserProfile |
Profile folder path | C:\Users\username |
IsRunningAsOriginalUser |
Check if elevated | 1, 0, or -1 (error) |
IsInteractiveSession |
Check for interactive desktop session | 1 (desktop), 0 (headless), -1 (error) |
GetSessionId |
Get Windows session ID | Session ID (0+ or -1 on error) |
GetLastOriginalUserError |
Last error code | See table below |
All functions return an integer error code (0 = success):
int __stdcall GetOriginalUser(wchar_t* buffer, int bufSize);
int __stdcall GetOriginalUserName(wchar_t* buffer, int bufSize);
int __stdcall GetOriginalUserSID(wchar_t* buffer, int bufSize);
int __stdcall GetOriginalUserProfile(wchar_t* buffer, int bufSize);
int __stdcall GetOriginalUserAppData(wchar_t* buffer, int bufSize);
int __stdcall GetOriginalUserLocalAppData(wchar_t* buffer, int bufSize);
int __stdcall GetOriginalUserDocuments(wchar_t* buffer, int bufSize);
int __stdcall GetOriginalUserDesktop(wchar_t* buffer, int bufSize);
int __stdcall IsRunningAsOriginalUser();
int __stdcall IsInteractiveSession();
int __stdcall GetSessionId();
int __stdcall GetLastOriginalUserError();| Code | Description |
|---|---|
| 0 | Success |
| 1 | Invalid buffer (null or zero size) |
| 2 | Failed to get session ID |
| 3 | Failed to query session username |
| 4 | Failed to query session domain |
| 5 | No user logged in to session |
| 6 | Failed to lookup account (SID conversion) |
| 7 | Failed to convert SID to string |
| 8 | Buffer too small for result |
| 9 | Failed to read registry key |
| 10 | Failed to get folder path |
| 11 | Cannot open user registry |
| 12 | (Reserved) |
| 13 | Non-interactive session (Session 0 / headless) |
function GetLastErrorDLL(): Integer;
external 'GetLastOriginalUserError@files:OriginalUser.dll stdcall delayload';
function GetOriginalUserWithError(): String;
var
Buffer: String;
ErrCode: Integer;
begin
SetLength(Buffer, 256);
GetOriginalUserDLL(Buffer, 256);
ErrCode := GetLastErrorDLL();
if ErrCode <> 0 then
begin
Log('OriginalUser error: ' + IntToStr(ErrCode));
Result := GetUserNameString(); // Fallback
end
else
Result := Copy(Buffer, 1, Pos(#0, Buffer) - 1);
end;When deploying via enterprise tools like SCCM, Intune, or PDQ Deploy, installers run in Session 0 (headless mode) where there is no interactive user. GetOriginalUser provides detection functions to handle this gracefully.
[Files]
Source: "config.ini"; DestDir: "{code:GetConfigDir}"; Check: IsNotEnterpriseInstall
[Code]
function IsInteractiveSessionDLL(): Integer;
external 'IsInteractiveSession@files:OriginalUser.dll stdcall delayload';
function IsInteractiveSession(): Boolean;
begin
try
Result := (IsInteractiveSessionDLL() = 1);
except
Result := True; // Assume interactive if DLL fails
end;
end;
function IsNotEnterpriseInstall(): Boolean;
begin
Result := IsInteractiveSession();
end;
function GetConfigDir(): String;
begin
if IsInteractiveSession() then
Result := GetOriginalUserAppData() + '\MyApp' // User's AppData
else
Result := ExpandConstant('{commonappdata}\MyApp'); // ProgramData
end;| Scenario | Session | Detection | Config Location |
|---|---|---|---|
| Manual install (interactive) | 1+ | IsInteractiveSession() = 1 |
User's AppData |
| SCCM/Intune deployment | 0 | IsInteractiveSession() = 0 |
ProgramData (template) |
| Scheduled task (SYSTEM) | 0 | IsInteractiveSession() = 0 |
ProgramData (template) |
π See examples/EnterpriseDemo.iss for a complete example.
The Receipt Pattern solves the "changing conditions" problem during uninstall. During install, you know who the user is - but during uninstall, a different admin may run it, making runtime detection unreliable.
If you deploy config to C:\Users\Bob\AppData\Rizonesoft\MyApp during install, a later uninstall might:
- Be run by a different admin user
- Run in an enterprise (Session 0) context
- Not have the DLL available
Using GetOriginalUser at uninstall time could return the wrong path!
// During INSTALL - save the path we deployed to
procedure SaveUninstallReceipt(ConfigPath: String);
begin
SetIniString('Uninstall', 'UserConfigPath', ConfigPath,
ExpandConstant('{app}\uninstall.ini'));
end;
// During UNINSTALL - read the receipt (no DLL needed!)
procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep);
var
Path: String;
begin
if CurUninstallStep = usUninstall then
begin
Path := GetIniString('Uninstall', 'UserConfigPath', '',
ExpandConstant('{app}\uninstall.ini'));
if (Path <> '') and DirExists(Path) then
DelTree(Path, True, True, True);
end;
end;- Install: Use
GetOriginalUserAppData()to get the correct path - Save: Write the path to
{app}\uninstall.ini - Uninstall: Read the saved path - works regardless of who runs it
π See examples/ReceiptDemo.iss for the complete pattern.
- Visual Studio 2017 or later (tested with VS2026)
- Windows SDK 10.0
# Clone the repository
git clone https://github.com/rizonesoft/GetOriginalUser.git
cd GetOriginalUser
# Build with MSBuild (from Developer Command Prompt)
msbuild OriginalUser.sln /p:Configuration=Release /p:Platform=Win32
msbuild OriginalUser.sln /p:Configuration=Release /p:Platform=x64Output files:
bin/OriginalUser_x86.dll(32-bit)bin/OriginalUser_x64.dll(64-bit)
| Component | Supported Versions |
|---|---|
| Windows | 7 SP1, 8.1, 10, 11 |
| Inno Setup | 5.5+ (tested with 6.x) |
| Visual Studio | 2017, 2019, 2022, 2026 |
- Notepad3 - Uses this pattern for AppData deployment
- Inno Setup - The installer system this plugin extends
This project is licensed under the MIT License - see the LICENSE file for details.
- Rizonesoft - Development and maintenance
- Jordan Russell - Inno Setup creator
- Microsoft - Windows API documentation
Solving the "Over-the-Shoulder" UAC problem, one installer at a time.
