Skip to content

Inno Setup plugin for identifying the original desktop user during elevated (UAC) installations

License

Notifications You must be signed in to change notification settings

rizonesoft/GetOriginalUser

GetOriginalUser

GetOriginalUser Banner

License Sponsor Donate

platform CodeQL build nightly

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.


The Problem

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

Over-the-Shoulder (OTS) Elevation

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.

The Solution

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.


Quick Start

1. Download the DLL

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)

2. Add to Your Inno Setup Script

[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;

API Reference

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

Function Signatures

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();

Error Codes

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)

Inno Setup Error Handling Example

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;

Enterprise Deployment (SCCM, Intune, PDQ Deploy)

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.

Hybrid Implementation Pattern

[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;

Deployment Behavior

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.


Receipt Pattern for Reliable Uninstallation

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.

The Problem

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!

The Solution: Save a Receipt

// 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;

Why This Works

  1. Install: Use GetOriginalUserAppData() to get the correct path
  2. Save: Write the path to {app}\uninstall.ini
  3. Uninstall: Read the saved path - works regardless of who runs it

πŸ“– See examples/ReceiptDemo.iss for the complete pattern.


Building from Source

Requirements

  • Visual Studio 2017 or later (tested with VS2026)
  • Windows SDK 10.0

Build Steps

# 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=x64

Output files:

  • bin/OriginalUser_x86.dll (32-bit)
  • bin/OriginalUser_x64.dll (64-bit)

Compatibility

Component Supported Versions
Windows 7 SP1, 8.1, 10, 11
Inno Setup 5.5+ (tested with 6.x)
Visual Studio 2017, 2019, 2022, 2026

Related Projects

  • Notepad3 - Uses this pattern for AppData deployment
  • Inno Setup - The installer system this plugin extends

License

This project is licensed under the MIT License - see the LICENSE file for details.


Credits

  • 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.

About

Inno Setup plugin for identifying the original desktop user during elevated (UAC) installations

Topics

Resources

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Sponsor this project

 

Packages

No packages published