Priming a Non-persistent Windows Image using an Autologon Process with an Auto Logoff Timer

Several years ago, and inspired by an article written by George Spiers to reduce login times, where “the second logon is quicker”, together with some code from Maurice Daly, I created a methodology and scripts that is designed to Autologon a non-persistent Session Host (both VDI and RDS), and then log it off again before another script will Start the Citrix Desktop Service (BrokerAgent).

It has been working flawlessly for years. However, I was never 100% happy with it because the process was using a domain (service) account for the Autologon process. The main challenge here was trying to change the password on a regular basis to stay compliant when managing multiple images. You cannot realistically do it without an outage. And in a 24×7 environment, it becomes difficult and onerous. I also felt that using a domain account can be “heavy” during a boot storm as you need to ensure you are excluding this account from profile management and policies where possible. Sometimes that is easier said than done. There is a level of risk here, as someone can easily make a change that will cause issues. The ability to roll the password and stay compliant was my biggest concern and where I got stuck for quite some time.

I spent a lot of time whiteboarding the process flow and all the moving parts (the scripts, registry values involved, etc). I realised that the only way to address this was to have a local account with a strong secure password (stored as an LSA Secret) that changes every time you rebuild the image, or re-run the script. You never need to record or know the password, and it can be different for each image. The local account is added to the local Users group by default, as there is no need for it to be a local Administrator. If you have the “Allow log on locally” policy set that excludes the local Users group, you’ll either need to adjust this policy or add the user to the appropriate local group that allows them to logon locally. If needed there is a variable you can set that will add the account to the local Administrators group. The only constant needs to be the name of the local account so that it works in conjunction with my Start the Citrix Desktop Service (BrokerAgent) script.

Then I got caught out by the Winlogon DefaultDomainName registry value without even realising that since Windows Vista and Server 2008 R1 it was no longer used to set the default logon Domain for the computer. Silly me! I had a Group Policy that would apply the Domain Name to this value by a registry preference, as that was once a best practice. So this would fill the DefaultDomainName registry value, breaking the Autologon of a local account. Together with these scripts and changes I made to the Start the Citrix Desktop Service (BrokerAgent) script, I was finally able to get the process working as needed.

Maybe these scenarios is also why Microsoft moved this Winlogon DefaultDomainName dependency starting from Windows Vista and Server 2008 R1 to a different registry location controlled by the “Assign a default domain for logon” group policy setting.

Now that I’ve overcome these challenges, I’m happy to share the scripts and process.

Here is a video of a Session Host starting up, auto logging on using a local account named PrimeMySystem, and then auto logging off gracefully.

Cool, isn’t it? 🙂

This process uses two PowerShell scripts and a batch script to make it simple to deploy.

  • AutoLogonLogoffTimer.ps1 (825 downloads ) – This PowerShell script is the UI with the countdown started by the Scheduled Task when the PrimeMySystem account logs on, which then gracefully logs off the session.
  • AutoLogonScheduledTask.ps1 (720 downloads ) – This PowerShell script creates the local account, generates a strong secure password, sets the Autologon registry values, stores the password as an LSA Secret, copies the AutoLogonLogoffTimer.ps1 to the C:\Scripts folder, and creates the “Session Host Autologon Logoff Task” Scheduled Task.
  • AutoLogonScheduledTask.cmd (755 downloads ) – This batch script can be used to run the AutoLogonScheduledTask.ps1 script with the required parameters.

To deploy, download all 3 scripts and place them in the same folder. Run the batch script as administrator to complete the configuration and copy the AutoLogonLogoffTimer.ps1 script into place.

As always, my scripts are well documented to help make them easy to follow.

Here is the full code view of the AutoLogonScheduledTask.cmd (755 downloads ) script

@Echo Off
cls
:: This script will deploy the AutoLogon process

SetLocal

Set LocalLocation=%WinDir%\Temp

copy /y "%~dp0AutoLogonScheduledTask.ps1" "%LocalLocation%"
copy /y "%~dp0AutoLogonLogoffTimer.ps1" "%LocalLocation%"

PUSHD "%LocalLocation%"

IF /I "%1"=="MDTBuildAccount" GOTO MDT
IF /I "%1"=="" GOTO LocalAccount

:MDT
powershell.exe -ExecutionPolicy Bypass -Command "& '"%LocalLocation%\AutoLogonScheduledTask.ps1"' -MDT -RemoveLegalPrompt -WaitForNetwork -RemoveDynamicSiteName"

GOTO Finish

:LocalAccount
powershell.exe -ExecutionPolicy Bypass -Command "& '"%LocalLocation%\AutoLogonScheduledTask.ps1"' -NewLocalAdmin:'PrimeMySystem' -PasswordLength:20 -SpecialCharacters:5 -RemoveLegalPrompt -RemoveDynamicSiteName"

:Finish

POPD

del /q "%LocalLocation%\AutoLogonScheduledTask.ps1"
del /q "%LocalLocation%\AutoLogonLogoffTimer.ps1"

EndLocal
Exit /b 0

Here is the full code view of the AutoLogonScheduledTask.ps1 (720 downloads ) script

<#
  This script sets the Citrix VDA autologon process by creating a scheduled task to logoff the auto logged on
  session and copy the script into place. LSA secrets is used to store the password securely.

  The script was first written in May 2017 and inspired by the "Autologon account..." section of an article
  written by George Spiers:
  - https://jgspiers.com/citrix-director-reduce-logon-times/

  To secrure the password I have used a PowerShell function, written by Andy Arismendi called Set-SecureAutoLogon,
  to manage the LSA secrets:
  - https://andyarismendi.blogspot.com/2011/10/powershell-set-secureautologon.html
  There are also other references to a similar process here:
  - https://www.onevinn.com/blog/windows-10-secure-autologon-powershell
  - https://github.com/Ccmexec/MEMCM-OSD-Scripts/tree/master/Kiosk%20scripts

  In April 2022 it became clear that using a Domain account became too onerous for rolling password changes between
  multiple images due to Cyber and/or password policies, etc. So I started to work on a process that uses a local
  account with a strong randomly generated password that is NOT exposed (recorded) and ONLY stored as an LSA secret.
  I ran into a few challenges when using a local account for the Autologon process mainly due to the legacy
  DefaultDomainName value being set by a Group Policy Preference. So I parked it for a while whilst I considered the
  best approach. I enhanced the StartCitrixDesktopService.ps1 script on 31st January 2023 allowing for the
  DefaultDomainName value to be managed, and have only just returned to complete the local account enhancements as
  of 16th July 2025. In order for this to work you must NOT set the DefaultDomainName by Group Policy under the
  "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" registry key. Note that the "Assign a
  default domain for logon" Group Policy setting sets the DefaultLogonDomain value under the
  "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System" registry key, which does not affect
  the Autologon process. If you want to set the DefaultDomainName value, use the additional functionality that I
  have added into the StartCitrixDesktopService.ps1 script.
  Reference:
  - https://www.jhouseconsulting.com/2019/03/04/controlling-the-starting-of-the-citrix-desktop-service-brokeragent-1894

  This script uses the Get-LocalUser, New-LocalUser, Set-LocalUser, Get-LocalGroupMember and Add-LocalGroupMember
  cmdlets, which require PowerShell 5.1 (WMF 5.1) or later for the most part, so I have included functions that
  support older versions of PowerShell.

  Syntax Examples:

    - To create and set the Autologon credentials and Scheduled Task as a new local user account
      called "PrimeMySystem", including removal of the legal prompts and the DynamicSiteName.
      AutoLogonScheduledTask.ps1 -NewLocalUser:"PrimeMySystem" -PasswordLength:20 -SpecialCharacters:5 -RemoveLegalPrompt -RemoveDynamicSiteName

    - To set the Autologon credentials and Scheduled Task as the MDT Domain Join account, including
      removal of the legal prompts, the DynamicSiteName, and set the always wait for the network policy.
      AutoLogonScheduledTask.ps1 -MDT -RemoveLegalPrompt -WaitForNetwork -RemoveDynamicSiteName

  Where...
  -NewLocalUser          = New local user to create that will be used for the AutoLogon process.
  -MakeUserAnAdmin       = Add the user to the local Administrators group. Defaults to false.
  -PasswordLength        = Length of the randomly generated strong password. Defaults to 20.
  -SpecialCharacters     = Number of special characters to use in the password. Defaults to 5.
  -DomainAdminDomain     = Domain in FQDN format preferrably
  -DomainAdmin           = Username that has permissions to move computer objects in AD.
                           You would typically use the Domain join account here.
  -DomainAdminPassword   = Password
  -Decode                = Decode (Optional). This is needed if you pass the DomainAdminDomain,
                           DomainAdmin and DomainAdminPassword variables from MDT.
  -MDT                   = Get the DomainAdminDomain DomainAdmin DomainAdminPassword variables and
                           automatically decrypt them. Note that the "Microsoft.SMS.TSEnvironment"
                           object is only available during the Task Sequences. You cannot use this
                           parameter to test this script outside of MDT.
  -RemoveLegalPrompt     = Remove Legal Banner
  -WaitForNetwork        = Enable Wait For Network
  -RemoveDynamicSiteName = Remove the DynamicSiteName value from registry. This is important if
                           building an image that will be deployed across multiple Active Directory
                           Sites.

  The registry values Keys for Autologon are found under the following key:
  - HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\Current Version\Winlogon
  - The values are:
    - DefaultUserName (REG_SZ) the account name used for the automatic logon.
    - DefaultPassword (REG_SZ) the password for the account specified by the DefaultUserName. We
      do not use this, as it is stored in plain text. Instead we store it securly as an LSA Secret.
    - DefaultDomainName (REG_SZ) the domain name to which the account specified above is a member.
      We leave this blank (empty) in order to use a local account.
    - AutoAdminLogon (REG_SZ) Setting will cause the automatic logon to occur with the above
      credentials, which includes the stored LSA Secret.
    - AutoLogonCount (REG_DWORD) is set to the number of times you want the AutoAdminLogon to take
      place. We don't need to set this here, as the AutoAdminLogon setting will allow the Session
      Host to autologon, which is all we need.
  The system will logon automatically with the specified credentials and decrement the AutoLogonCount
  value until it reaches zero. When it reaches zero, the DefaultPassword, stored LSA Secret, and
  AutoLogonCount values are deleted and the AutoAdminLogon value is set to 0. However, if the
  AutoLogonCount value is missing altogether, the AutoAdminLogon value will remain set to 1 and the
  system will continue to autologon after every reboot. This is okay for image management. But if
  you want to use this for other purposes, setting the AutoLogonCount to 1 will achieve the same
  outcome.

  Script name: AutoLogonScheduledTask.ps1
  Release 1.8
  Written by Jeremy Saunders (jeremy@jhouseconsulting.com) 12th May 2017
  Modified by Jeremy Saunders (jeremy@jhouseconsulting.com) 28th July 2025

#>
#-------------------------------------------------------------
[cmdletbinding()]
param (
    [string]$NewLocalUser,
    [switch]$MakeUserAnAdmin,
    [int]$PasswordLength = 20,
    [int]$SpecialCharacters = 5,
    [string]$DomainAdmin,
    [string]$DomainAdminPassword,
    [System.Security.SecureString]$SecurePassword,
    [string]$DomainAdminDomain,
    [Int]$AutoLogonCount,
    [switch]$RemoveLegalPrompt,
    [switch]$WaitForNetwork,
    [switch]$RemoveDynamicSiteName,
    [switch]$Backup
)

# Set Powershell Compatibility Mode
Set-StrictMode -Version 2.0

# Enable verbose, warning and error mode
$VerbosePreference = 'Continue'
$WarningPreference = 'Continue'
$ErrorPreference = 'Continue'

#-------------------------------------------------------------

Function IsTaskSequence([switch] $verbose) {
  # This code was taken from a discussion on the CodePlex PowerShell App Deployment Toolkit site.
  # It was posted by mmashwani.
  # The only issue with this function is that it writes terminating errors to the transcription log, which looks
  # rather ugly. So this function will eventually be updated so that it checks for the existenace of the relevant
  # snapins (Get-PSSnapin) and assemblies ([AppDomain]::CurrentDomain.GetAssemblies()) that have been loaded.
  # References:
  # - https://richardspowershellblog.wordpress.com/2007/09/30/assemblies-loaded-in-powershell/
  # - http://learningpcs.blogspot.com.au/2012/06/powershell-v2-test-if-assembly-is.html
  Try {
      [__ComObject]$SMSTSEnvironment = New-Object -ComObject Microsoft.SMS.TSEnvironment -ErrorAction 'SilentlyContinue' -ErrorVariable SMSTSEnvironmentErr
  }
  Catch {
    # The Microsoft.SMS.TSEnvironment assembly is not present.
  }
  If ($SMSTSEnvironmentErr) {
    Write-Verbose "Unable to load ComObject [Microsoft.SMS.TSEnvironment]. Therefore, script is not currently running from an MDT or SCCM Task Sequence." -verbose:$verbose
    Return $false
  }
  ElseIf ($null -ne $SMSTSEnvironment) {
    Write-Verbose "Successfully loaded ComObject [Microsoft.SMS.TSEnvironment]. Therefore, script is currently running from an MDT or SCCM Task Sequence." -verbose:$verbose
    Return $true
  }
}

#-------------------------------------------------------------
$invalidChars = [io.path]::GetInvalidFileNamechars() 
$datestampforfilename = ((Get-Date -format s).ToString() -replace "[$invalidChars]","-")

# Get the current script path
$ScriptPath = {Split-Path $MyInvocation.ScriptName}
$ScriptPath = $(&$ScriptPath)
$ScriptName = [System.IO.Path]::GetFilenameWithoutExtension($MyInvocation.MyCommand.Path.ToString())
$Logfile = "$ScriptName-$($datestampforfilename).txt"
$logPath = "$($env:windir)\Temp"

If (IsTaskSequence) {
  $tsenv = New-Object -COMObject Microsoft.SMS.TSEnvironment 
  $logPath = $tsenv.Value("LogPath")
}

$logfile = "$logPath\$Logfile"

try {
  # The Microsoft.BDD.TaskSequencePSHost.exe (TSHOST) does not support
  # transcription, so we wrap this in a try catch to prevent errors.
  Start-Transcript $logFile
}
catch {
  Write-Verbose "This host does not support transcription"
}

#-------------------------------------------------------------

Function New-RandomPassword {
  # This function was posted on Reddit by OPconfused: https://www.reddit.com/r/PowerShell/comments/17wz2xh/powershell_generate_random_password/
  param(
        [Parameter(Mandatory)]
        [Alias('length')]
        [ValidateRange(0,30)]
        [int]$PasswordLength
  )

  $validCharacters = 48..57 + 65..90 + 97..122 + (
      '!', '@', '#', '$', '%', '^', '&', '*'
  ) | ForEach-Object { [char]$_ }

  return (1..$PasswordLength | ForEach-Object {
      Get-Random $validCharacters
  }) -join ''
}

Function GenerateStrongPassword {
  # http://woshub.com/generating-random-password-with-powershell/
  param (
    [parameter(Mandatory=$true)][int]$PasswordLength,
    [parameter(Mandatory=$true)][int]$SpecialCharacters
  )
  Add-Type -AssemblyName System.Web
  $PassComplexCheck = $false
  do {
    $newPassword = [System.Web.Security.Membership]::GeneratePassword($PasswordLength,$SpecialCharacters)
    If ( ($newPassword -cmatch "[A-Z\p{Lu}\s]") `
        -and ($newPassword -cmatch "[a-z\p{Ll}\s]") `
        -and ($newPassword -match "[\d]") `
        -and ($newPassword -match "[^\w]")
      ) {
      $PassComplexCheck = $True
    }
  } While ($PassComplexCheck -eq $false)
  return $newPassword
}

function Test-RegistryValue {
  param (
    [parameter(Mandatory=$true)]
    [ValidateNotNullOrEmpty()]$Path,
    [parameter(Mandatory=$true)]
    [ValidateNotNullOrEmpty()]$Value
  )
  # Create a drive to HKEY_CLASSES_ROOT & HKEY_CURRENT_USER. By default these two
  # registry geys are not available for mounting and using in PowerShell.
  if (!(get-psdrive hkcr -ea 0)){New-PSDrive -Name HKCR -PSProvider Registry -Root HKEY_CLASSES_ROOT | out-null}
  if (!(get-psdrive hku -ea 0)){New-PSDrive -Name HKU -PSProvider Registry -Root HKEY_USERS | out-null}
  $ErrorActionPreference = "stop"
  try {
    If ((Get-ItemProperty -Path "$Path" | Select-Object -ExpandProperty "$Value") -ne $null) {
      return $true
    } Else {
      return $false
    }
  }
  catch {
    return $false
  }
  finally { $ErrorActionPreference = "Continue" }
}

function Test-RegistryPath {
  param (
    [parameter(Mandatory=$true)]
    [ValidateNotNullOrEmpty()]$Path
  )
  # Create a drive to HKEY_CLASSES_ROOT & HKEY_CURRENT_USER. By default these two
  # registry geys are not available for mounting and using in PowerShell.
  if (!(get-psdrive hkcr -ea 0)){New-PSDrive -Name HKCR -PSProvider Registry -Root HKEY_CLASSES_ROOT | out-null}
  if (!(get-psdrive hku -ea 0)){New-PSDrive -Name HKU -PSProvider Registry -Root HKEY_USERS | out-null}
  $ErrorActionPreference = "stop"
  try {
    Get-Item -Path "$Path" | Out-Null
    return $true
  }
  catch {
    return $false
  }
  finally { $ErrorActionPreference = "Continue" }
}

function createLocalUser {
  # This function was written for backward PowerShell compatibility.
  # - It creates a new users and adds it to the default "Users" group.
  # - It only supports passing a clear text password (no SecureString compatibility).
  # - UserFlags is a bitmask. The 0x10000 flag corresponds to "Password never expires".
  # - AccountExpirationDate is not supported by the WinNT ADSI Provider.
  param (
    [string]$Username,
    [string]$Password,
    [string]$Description,
    [switch]$PasswordNeverExpires
  )
  try {
    # Check if user already exists
    $existingUser = [ADSI]"WinNT://$env:COMPUTERNAME/$Username,user"
    if ($existingUser.Name -eq $Username) {
      $existingUser.SetPassword($Password)
      $existingUser.SetInfo()
      # Set 'password never expires' if requested
      if ($PasswordNeverExpires) {
        #DONT_EXPIRE_PASSWORD 0x10000 65536
        $existingUser.UserFlags = 65536
        $existingUser.SetInfo()
      }
      return $True
    }
  } catch {
    # User does not exist, continue
  }
  try {
    # Create new user
    $computer = [ADSI]"WinNT://$env:COMPUTERNAME"
    $user = $computer.Create("User", $Username)
    $user.SetPassword($Password)
    $user.SetInfo()
    $user.Description = $Description
    $user.SetInfo()
    # Set 'password never expires' if requested
    if ($PasswordNeverExpires) {
      #DONT_EXPIRE_PASSWORD 0x10000 65536
      $user.UserFlags = 65536
      $user.SetInfo()
    }
    # Add user to 'Users' group
    $group = [ADSI]"WinNT://$env:COMPUTERNAME/Users,group"
    $group.Add("WinNT://$env:COMPUTERNAME/$Username,user")
    return $True
  } catch {
    return $False
  }
}

function addUser2Group
{   
    # This function was written by Ethol Palmer and posted to stackoverflow:
    # - https://stackoverflow.com/questions/13929960/add-user-to-local-group/14262326#14262326
    Param(
        [string]$user,
        [string]$group
    )

    $cname = gc env:computername
    $objUser = [ADSI]("WinNT://$user")
    $objGroup = [ADSI]("WinNT://$cname/$group,group")
    $members = $objGroup.Invoke('Members')
    $found = $false

    foreach($m in $members)
    {
        if($m.GetType().InvokeMember('Name', 'GetProperty', $null, $m, $null) -eq $user)
        {
            $found = $true
        }
    }

    if(-not $found)
    {
        $objGroup.PSBase.Invoke('Add',$objUser.PSBase.Path)
    }

    $members = $objGroup.PSBase.Invoke('Members')
    $found = $false
    foreach($m in $members)
    {
        if($m.GetType().InvokeMember('Name', 'GetProperty', $null, $m, $null) -eq $user)
        {
            $found = $true
        }
    }

    return $found
}

#-------------------------------------------------------------

$ExitCode = 0
$GoodToProceed = $False


If ([string]::IsNullOrEmpty($NewLocalUser)) {
  If ($MDT) {
    If (IsTaskSequence) {
      Write-Verbose "Reading Task Sequence variables" -verbose
      $Decode = $True
      $DomainAdminDomain = $tsenv.Value("UserDomain")
      $DomainAdmin = $tsenv.Value("UserID")
      $DomainAdminPassword = $tsenv.Value("UserPassword")
    } Else {
      Write-Verbose "This script is not running from a task sequence" -verbose
    }
  }
  If (![string]::IsNullOrEmpty($DomainAdminDomain) -AND ![string]::IsNullOrEmpty($DomainAdmin) -AND (![string]::IsNullOrEmpty($DomainAdminPassword) -OR ![string]::IsNullOrEmpty($SecurePassword))) {
    $GoodToProceed = $True
    If ($Decode) {
      # Decode the base64 encoded blob using UTF-8
      $DomainAdminDomain = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($DomainAdminDomain))
      $DomainAdmin = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($DomainAdmin))
      $DomainAdminPassword = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($DomainAdminPassword))
    }
    If (![string]::IsNullOrEmpty($DomainAdminPassword)) {
      $DomainAdminPasswordSecureString = ConvertTo-SecureString -String $DomainAdminPassword -AsPlainText -Force
    } Else {
      $DomainAdminPasswordSecureString = $SecurePassword
    }
    $DefaultUserName = $DomainAdmin
    $DefaultDomainName = $DomainAdminDomain
    $PasswordSecureString = $DomainAdminPasswordSecureString
    $SecurityPrincipal = "$DefaultDomainName\$DefaultUserName"
  }
}

If (![string]::IsNullOrEmpty($NewLocalUser)) {
  $GoodToProceed = $True

  #$StrongPassword = New-RandomPassword -PasswordLength:$PasswordLength
  $StrongPassword = GenerateStrongPassword -PasswordLength:$PasswordLength -SpecialCharacters:$SpecialCharacters
  $StrongPasswordAsSecureString = ConvertTo-SecureString "$StrongPassword" -AsPlainText -Force

  $CreateLocalUser = $True

  # I have included the createLocalUser function if you are running a PowerShell version less than 5.1.
  If (($PSVersionTable.PSVersion.Major -eq 5 -AND $PSVersionTable.PSVersion.Minor -ge 1) -OR $PSVersionTable.PSVersion.Major -gt 5) {
    $ErrorActionPreference = "stop"
    Try {
      Get-LocalUser -Name "$NewLocalUser" | out-null
      $CreateLocalUser = $False
    }
    Catch {
      # Local Admin not found
    }
    $ErrorActionPreference = "Continue"
    If ($CreateLocalUser) {
      write-verbose "Creating the `"$NewLocalUser`" account with a randomly generated strong password" -verbose
      New-LocalUser "$NewLocalUser" -Password $StrongPasswordAsSecureString -FullName "$NewLocalUser" -Description "$NewLocalUser Account" -PasswordNeverExpires -AccountNeverExpires | out-null
    } Else {
      write-verbose "The `"$NewLocalUser`" account already exists" -verbose
      write-verbose "Setting its password to a randomly generated strong password" -verbose
      Set-LocalUser -Name "$NewLocalUser" -Password $StrongPasswordAsSecureString
    }
  } Else {
    write-verbose "Creating the `"$NewLocalUser`" account with a randomly generated strong password" -verbose
    createLocalUser -Username "$NewLocalUser" -Password $StrongPassword -Description "$NewLocalUser Account" -PasswordNeverExpires | out-null
  }

  # I have included the addUser2Group function if you are running a PowerShell version less than 5.1.
  $LocalGroupName = "Users"
  If ($MakeUserAnAdmin) {
    $LocalGroupName = "Administrators"
  }
  If (($PSVersionTable.PSVersion.Major -eq 5 -AND $PSVersionTable.PSVersion.Minor -ge 1) -OR $PSVersionTable.PSVersion.Major -gt 5) {
    $isInGroup = (Get-LocalGroupMember -Group "$LocalGroupName").Name -contains "$env:COMPUTERNAME\$NewLocalUser"
    If ($isInGroup -eq $False) {
      write-verbose "Adding the `"$NewLocalUser`" account to the local `"$LocalGroupName`" group" -verbose
      Add-LocalGroupMember -Group "$LocalGroupName" -Member "$NewLocalUser"
    } Else {
      write-verbose "The `"$NewLocalUser`" account is already a member of the local `"$LocalGroupName`" group" -verbose
    }
  } Else {
    write-verbose "Adding the `"$NewLocalUser`" account to the local `"$LocalGroupName`" group" -verbose
    addUser2Group -user "$NewLocalUser" -group "$LocalGroupName" | out-null
  }

  $DefaultUserName = "$NewLocalUser"
  $DefaultDomainName = ""
  $PasswordSecureString = $StrongPasswordAsSecureString
  #$SecurityPrincipal = "$ENV:COMPUTERNAME\$DefaultUserName"
  $SecurityPrincipal = "$DefaultUserName"
}

If ($GoodToProceed) {

  [string] $WinlogonPath = "HKLM:\Software\Microsoft\Windows NT\CurrentVersion\Winlogon"
  [string] $WinlogonPolicyPath = "HKLM:\Software\Policies\Microsoft\Windows NT\CurrentVersion\Winlogon"
  [string] $WinlogonBannerPolicyPath = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System"
  [string] $NetlogonParametersPath = "HKLM:\System\CurrentControlSet\Services\Netlogon\parameters"

  [string] $Enable = 1
  [string] $Disable = 0

  #region C# Code to P-invoke LSA LsaStorePrivateData function.
  Add-Type @"
    using System;
    using System.Collections.Generic;
    using System.Text;
    using System.Runtime.InteropServices;
 
    namespace ComputerSystem
    {
       public class LSAutil
       {
           [StructLayout(LayoutKind.Sequential)]
           private struct LSA_UNICODE_STRING
           {
               public UInt16 Length;
               public UInt16 MaximumLength;
               public IntPtr Buffer;
           }
 
           [StructLayout(LayoutKind.Sequential)]
           private struct LSA_OBJECT_ATTRIBUTES
           {
               public int Length;
               public IntPtr RootDirectory;
               public LSA_UNICODE_STRING ObjectName;
               public uint Attributes;
               public IntPtr SecurityDescriptor;
               public IntPtr SecurityQualityOfService;
           }
 
           private enum LSA_AccessPolicy : long
           {
               POLICY_VIEW_LOCAL_INFORMATION = 0x00000001L,
               POLICY_VIEW_AUDIT_INFORMATION = 0x00000002L,
               POLICY_GET_PRIVATE_INFORMATION = 0x00000004L,
               POLICY_TRUST_ADMIN = 0x00000008L,
               POLICY_CREATE_ACCOUNT = 0x00000010L,
               POLICY_CREATE_SECRET = 0x00000020L,
               POLICY_CREATE_PRIVILEGE = 0x00000040L,
               POLICY_SET_DEFAULT_QUOTA_LIMITS = 0x00000080L,
               POLICY_SET_AUDIT_REQUIREMENTS = 0x00000100L,
               POLICY_AUDIT_LOG_ADMIN = 0x00000200L,
               POLICY_SERVER_ADMIN = 0x00000400L,
               POLICY_LOOKUP_NAMES = 0x00000800L,
               POLICY_NOTIFICATION = 0x00001000L
           }
 
           [DllImport("advapi32.dll", SetLastError = true, PreserveSig = true)]
           private static extern uint LsaRetrievePrivateData(
                       IntPtr PolicyHandle,
                       ref LSA_UNICODE_STRING KeyName,
                       out IntPtr PrivateData
           );
 
           [DllImport("advapi32.dll", SetLastError = true, PreserveSig = true)]
           private static extern uint LsaStorePrivateData(
                   IntPtr policyHandle,
                   ref LSA_UNICODE_STRING KeyName,
                   ref LSA_UNICODE_STRING PrivateData
           );
 
           [DllImport("advapi32.dll", SetLastError = true, PreserveSig = true)]
           private static extern uint LsaOpenPolicy(
               ref LSA_UNICODE_STRING SystemName,
               ref LSA_OBJECT_ATTRIBUTES ObjectAttributes,
               uint DesiredAccess,
               out IntPtr PolicyHandle
           );
 
           [DllImport("advapi32.dll", SetLastError = true, PreserveSig = true)]
           private static extern uint LsaNtStatusToWinError(
               uint status
           );
 
           [DllImport("advapi32.dll", SetLastError = true, PreserveSig = true)]
           private static extern uint LsaClose(
               IntPtr policyHandle
           );
 
           [DllImport("advapi32.dll", SetLastError = true, PreserveSig = true)]
           private static extern uint LsaFreeMemory(
               IntPtr buffer
           );
 
           private LSA_OBJECT_ATTRIBUTES objectAttributes;
           private LSA_UNICODE_STRING localsystem;
           private LSA_UNICODE_STRING secretName;
 
           public LSAutil(string key)
           {
               if (key.Length == 0)
               {
                   throw new Exception("Key lenght zero");
               }
 
               objectAttributes = new LSA_OBJECT_ATTRIBUTES();
               objectAttributes.Length = 0;
               objectAttributes.RootDirectory = IntPtr.Zero;
               objectAttributes.Attributes = 0;
               objectAttributes.SecurityDescriptor = IntPtr.Zero;
               objectAttributes.SecurityQualityOfService = IntPtr.Zero;
 
               localsystem = new LSA_UNICODE_STRING();
               localsystem.Buffer = IntPtr.Zero;
               localsystem.Length = 0;
               localsystem.MaximumLength = 0;
 
               secretName = new LSA_UNICODE_STRING();
               secretName.Buffer = Marshal.StringToHGlobalUni(key);
               secretName.Length = (UInt16)(key.Length * UnicodeEncoding.CharSize);
               secretName.MaximumLength = (UInt16)((key.Length + 1) * UnicodeEncoding.CharSize);
           }
 
           private IntPtr GetLsaPolicy(LSA_AccessPolicy access)
           {
               IntPtr LsaPolicyHandle;
 
               uint ntsResult = LsaOpenPolicy(ref this.localsystem, ref this.objectAttributes, (uint)access, out LsaPolicyHandle);
 
               uint winErrorCode = LsaNtStatusToWinError(ntsResult);
               if (winErrorCode != 0)
               {
                   throw new Exception("LsaOpenPolicy failed: " + winErrorCode);
               }
 
               return LsaPolicyHandle;
           }
 
           private static void ReleaseLsaPolicy(IntPtr LsaPolicyHandle)
           {
               uint ntsResult = LsaClose(LsaPolicyHandle);
               uint winErrorCode = LsaNtStatusToWinError(ntsResult);
               if (winErrorCode != 0)
               {
                   throw new Exception("LsaClose failed: " + winErrorCode);
               }
           }
 
           public void SetSecret(string value)
           {
               LSA_UNICODE_STRING lusSecretData = new LSA_UNICODE_STRING();
 
               if (value.Length > 0)
               {
                   //Create data and key
                   lusSecretData.Buffer = Marshal.StringToHGlobalUni(value);
                   lusSecretData.Length = (UInt16)(value.Length * UnicodeEncoding.CharSize);
                   lusSecretData.MaximumLength = (UInt16)((value.Length + 1) * UnicodeEncoding.CharSize);
               }
               else
               {
                   //Delete data and key
                   lusSecretData.Buffer = IntPtr.Zero;
                   lusSecretData.Length = 0;
                   lusSecretData.MaximumLength = 0;
               }
 
               IntPtr LsaPolicyHandle = GetLsaPolicy(LSA_AccessPolicy.POLICY_CREATE_SECRET);
               uint result = LsaStorePrivateData(LsaPolicyHandle, ref secretName, ref lusSecretData);
               ReleaseLsaPolicy(LsaPolicyHandle);
 
               uint winErrorCode = LsaNtStatusToWinError(result);
               if (winErrorCode != 0)
               {
                   throw new Exception("StorePrivateData failed: " + winErrorCode);
               }
           }
       }
   }
"@
  #endregion

  try {
    $ErrorActionPreference = "Stop"
    $decryptedPass = [Runtime.InteropServices.Marshal]::PtrToStringAuto(
                     [Runtime.InteropServices.Marshal]::SecureStringToBSTR($PasswordSecureString))
    $ErrorActionPreference = "Continue"
    If ($Backup) {
      try {
        $WinlogonPathProps = Get-ItemProperty -Path $WinlogonPath

        write-verbose "Backing up settings from: $($WinlogonPath)" -verbose
        write-verbose "- AutoAdminLogon: $($WinlogonPathProps.AutoAdminLogon)" -verbose
        If ($WinlogonPathProps -match "^ForceAutoLogon$") {
          write-verbose "- ForceAutoLogon: $($WinlogonPathProps.ForceAutoLogon)" -verbose
        }
        write-verbose "- DefaultUserName: $($WinlogonPathProps.DefaultUserName)" -verbose
        write-verbose "- DefaultDomainName: $($WinlogonPathProps.DefaultDomainName)" -verbose
        If ($WinlogonPathProps -match "^DefaultPassword$") {
          write-verbose "- DefaultPassword: $($WinlogonPathProps.DefaultPassword)" -verbose
        }
        If ($WinlogonPathProps -match "^AutoLogonCount$") {
          write-verbose "- AutoLogonCount: $($WinlogonPathProps.AutoLogonCount)" -verbose
        }

        # The winlogon logon banner settings.
        write-verbose "- LegalNoticeCaption: $($WinlogonPathProps.LegalNoticeCaption)" -verbose
        write-verbose "- LegalNoticeText: $($WinlogonPathProps.LegalNoticeText)" -verbose

        # The system policy logon banner settings.
        $WinlogonBannerPolicyPathProps = Get-ItemProperty -Path $WinlogonBannerPolicyPath
        write-verbose "Backing up settings from: $($WinlogonBannerPolicyPath)" -verbose
        write-verbose "- legalnoticecaption: $($WinlogonBannerPolicyPathProps.legalnoticecaption)" -verbose
        write-verbose "- legalnoticetext: $($WinlogonBannerPolicyPathProps.legalnoticetext)" -verbose
      }
      catch {
       #$_.Exception.Message
      }
    }
         
    # Store the password securely.
    $ErrorActionPreference = "Stop"
    write-verbose "Storing the password securely as an LSA Secret." -verbose
    $lsaUtil = New-Object ComputerSystem.LSAutil -ArgumentList "DefaultPassword"
    $lsaUtil.SetSecret($decryptedPass)
    $ErrorActionPreference = "Continue"

    # Store the autologon registry settings.
    write-verbose "Setting the AutoAdminLogon registry value." -verbose
    Set-ItemProperty -Path $WinlogonPath -Name AutoAdminLogon -Value $Enable -Type STRING -Force
    write-verbose "Setting the DefaultUserName registry value." -verbose
    Set-ItemProperty -Path $WinlogonPath -Name DefaultUserName -Value $DefaultUserName -Type STRING -Force
    write-verbose "Setting the DefaultDomainName registry value." -verbose
    Set-ItemProperty -Path $WinlogonPath -Name DefaultDomainName -Value $DefaultDomainName -Type STRING -Force
 
    if ($AutoLogonCount) {
      write-verbose "Setting the AutoLogonCount registry value." -verbose
      Set-ItemProperty -Path $WinlogonPath -Name AutoLogonCount -Value $AutoLogonCount -Type DWORD -Force
    } else {
      write-verbose "Removing the AutoLogonCount registry value." -verbose
      Remove-ItemProperty -Path $WinlogonPath -Name AutoLogonCount -ErrorAction SilentlyContinue
    }
 
    # Remove an existing DefaultPassword value which will break the secure-autologon
    write-verbose "Removing the DefaultPassword registry value." -verbose
    Remove-ItemProperty -Path $WinlogonPath -Name DefaultPassword -ErrorAction SilentlyContinue

    if ($RemoveDynamicSiteName) {
      # Remove the DynamicSiteName registry value
      write-verbose "Removing the DynamicSiteName registry value." -verbose
      If (Test-RegistryValue -Path "$NetlogonParametersPath" -Value "DynamicSiteName") {
        Remove-ItemProperty -Path $NetlogonParametersPath -Name DynamicSiteName -Force
      }
    }

    if ($WaitForNetwork) {
      # Always wait for the network at computer startup and logon. The autologon could fail if
      # the network isn't ready.
      write-verbose "Setting the Always wait for the network at computer startup and logon (SyncForegroundPolicy) registry value." -verbose
      If (!(Test-RegistryPath -Path "$WinlogonPolicyPath")) {
        New-Item -Path "$WinlogonPolicyPath" -Force | Out-Null
      }
      Set-ItemProperty -Path $WinlogonPolicyPath -Name SyncForegroundPolicy -Value 1 -Type DWORD -Force
    }

    if ($RemoveLegalPrompt) {
      write-verbose "Removing the LegalNoticeCaption and LegalNoticeText registry values" -verbose
      Set-ItemProperty -Path $WinlogonPath -Name LegalNoticeCaption -Value $null -Force
      Set-ItemProperty -Path $WinlogonPath -Name LegalNoticeText -Value $null -Force

      If (Test-RegistryPath -Path "$WinlogonPolicyPath") {
        Set-ItemProperty -Path $WinlogonBannerPolicyPath -Name legalnoticecaption -Value $null -Force
        Set-ItemProperty -Path $WinlogonBannerPolicyPath -Name legalnoticetext -Value $null -Force
      }
    }

    write-verbose "Successfully set autologon" -verbose

  } catch {
    #$_.Exception.Message
    $ExitCode = 1
  }

  # Copy the auto logoff script into place

  $Scripts = "$env:SystemDrive\Scripts"
  If (-not(Test-Path -Path "$Scripts")) {
    New-Item -Path "$Scripts" -ItemType Directory | Out-Null
  }

  # Push the current location onto a location stack and then change the current location to the location specified
  Push-Location "$ScriptPath"

  If (Test-Path -path "$ScriptPath\AutoLogonLogoffTimer.ps1") {
    write-verbose "Copying the AutoLogonLogoffTimer.ps1 script into place." -verbose
    copy-item -path ".\AutoLogonLogoffTimer.ps1" -Destination "$Scripts\" -Recurse -Force -Verbose
  } Else {
    write-warning "The AutoLogonLogoffTimer.ps1 script is missing!" -verbose
  }

  # Change the current location back to the location most recently pushed onto the stack
  Pop-Location

  # Create the Scheduled Task
  write-verbose "Creating the Scheduled Task." -verbose

  # The name of the scheduled task
  $taskName = "Session Host Autologon Logoff Task"

  # The task description
  $taskDescription = "This task is created to logoff the autologon session after a Session Host has been restarted"

  # We can delay the logoff task by x seconds if needed to give the autologon process a chance to complete
  $AddDelayTrigger = $True
  $DelayedStartInSeconds = 30

  # The Task Action command
  #$TaskCommand = "${env:SystemRoot}\system32\WindowsPowerShell\v1.0\powershell.exe"
  $TaskCommand = @(Get-Command powershell.exe)[0].Definition

  # The script to be executed
  $TaskScript = "$Scripts\AutoLogonLogoffTimer.ps1"

  # The Task Action command argument
  #$TaskArguments = '-Executionpolicy bypass -WindowStyle Minimized -Command "& ' + " '" + $TaskScript + "'"
  $TaskArguments = '-Executionpolicy bypass -WindowStyle Minimized -Command "& ' + " '" + $TaskScript + "'" + '"'

  $taskSecurityPrincipal = $SecurityPrincipal

  # Create the TaskService object.
  Try {
    [Object] $service = new-object -com("Schedule.Service")
    If (!($service.Connected)){
      Try {
        $service.Connect()
        # Get a folder to create a task definition in
        # This is actually the %SystemRoot%\System32\Tasks folder.
        $rootFolder = $service.GetFolder("\")

        # Delete the task if already present
        $ScheduledTasks = $rootFolder.GetTasks(0)
        $Task = $ScheduledTasks | Where-Object{$_.Name -eq "$TaskName"}
        If ($Task -ne $Null){
          Try {
            $rootFolder.DeleteTask($Task.Name,0)
            # 'Success'
          }
          Catch [System.Exception]{
            # 'Exception Returned'
          }
        } Else {
          # "Task Not Found"
        }

        # Create the new task
        $taskDefinition = $service.NewTask(0)

        $taskPrincipal = $taskDefinition.Principal
        # InteractiveToken
        $taskPrincipal.LogonType = 3
        # Must be a valid user account
        $taskPrincipal.UserID = $taskSecurityPrincipal
        $taskPrincipal.RunLevel = 0

        # Create a registration trigger with a trigger type of (9) LogonTrigger
        $triggers = $taskDefinition.Triggers
        $trigger = $triggers.Create(9)
        $trigger.ExecutionTimeLimit = "PT30M"
        If ($AddDelayTrigger) {
          # The delay time in seconds before the task runs once it's been triggered
          $trigger.Delay = "PT${DelayedStartInSeconds}S"
        }

        # Begin the task only when the autologon user logs on
        $trigger.UserID = $taskSecurityPrincipal
        $trigger.Enabled = $true

        # Create the action for the task to execute.
        $Action = $taskDefinition.Actions.Create(0)
        $Action.Path = $TaskCommand
        $Action.Arguments = $TaskArguments
        $Action.WorkingDirectory = ""

        # Register (create) the task.
        $Settings = $taskDefinition.Settings
        # Set the Task Compatibility to V2 (Windows 7/2008R2)
        $Settings.Compatibility = 3
        # The default task priority 7 (below normal), so we set this back to normal
        $Settings.Priority = 6
        $Settings.AllowDemandStart = $true
        $Settings.StopIfGoingOnBatteries = $false
        $Settings.DisallowStartIfOnBatteries = $false

        $regInfo = $taskDefinition.RegistrationInfo
        $regInfo.Description = $taskDescription
        $regInfo.Author = $Env:Username

        # Note that the task is created as an XML file under the %SystemRoot%\System32\Tasks folder
        # 6 == Task Create or Update
        # 3 == LogonTypeInteractive
        $rootFolder.RegisterTaskDefinition($taskName, $taskDefinition, 6, '', '', 3) | Out-Null
        write-verbose "- Scheduled Task Created Successfully" -verbose
      }
      Catch [System.Exception]{
        write-warning "- Scheduled Task Creation Failed" -verbose
        $ExitCode = 1
      }
    }
  }
  Catch [System.Exception]{
    write-warning "- Scheduled Task Creation Failed" -verbose
    $ExitCode = 1
  }

} Else {
  write-warning "Missing credentials." -verbose
  $ExitCode = 1
}

try {
  # The Microsoft.BDD.TaskSequencePSHost.exe (TSHOST) does not support
  # transcription, so we wrap this in a try catch to prevent errors.
  Stop-Transcript
}
catch {
  Write-Verbose "This host does not support transcription"
}
Exit $ExitCode

Here is the full code view of the AutoLogonLogoffTimer.ps1 (825 downloads ) script

<#
  This script provides a countdown timer with progress bar in a nice UI that
  will automatically logoff the session when the countdown reaches 0.

  It was primarily written to help manage the post reboot process of Citrix
  VDA Session Hosts. When a Citrix VDA restarts as part of scheduled reboots,
  for example, or when non-persistent desktops reboot to reset, the first
  logon is generally always the longest. So we use an autologon account to
  prime the VDA when it restarts. This script will logoff the auto logged on
  session when the countdown reaches 0.

  I have found it more reliable to use shutdown.exe instead of logoff.exe so
  that we can forcibly logoff the session as logoff.exe sometimes gets stuck
  due to running processes.

  So use...
    shutdown.exe /l /f
      where...
        /l = logoff
        /f = force

  Note that the "Last Run Result" of the scheduled task will be 0x40010004
  because we forcefully logoff the session which kills the process (this
  script) causing it to exit with a code of 0x40010004.

  Based on a script written by Maurice Daly on 04/10/2016
  - https://modalyitblog.wordpress.com/2016/10/03/powershell-gui-reboot-prompt/

  Script name: AutoLogonLogoffTimer.ps1
  Release 1.2
  Modified by Jeremy Saunders (jeremy@jhouseconsulting.com) 20th June 2017

#>
#-------------------------------------------------------------
[cmdletbinding()]
param (
       [int]$Seconds=15
      )

# Set Powershell Compatibility Mode
Set-StrictMode -Version 2.0

# Enable verbose, warning and error mode
$VerbosePreference = 'Continue'
$WarningPreference = 'Continue'
$ErrorPreference = 'Continue'

#-------------------------------------------------------------

# Hide the PowerShell console window without hiding the other child windows that it spawns
# - http://powershell.cz/2013/04/04/hide-and-show-console-window-from-gui/
$Code = @"
[DllImport("Kernel32.dll")]
public static extern IntPtr GetConsoleWindow();
[DllImport("user32.dll")]
public static extern bool ShowWindow(IntPtr hWnd, Int32 nCmdShow);
"@
# Create new types as per the definition above. 
Add-Type -Namespace Console -Name Window -MemberDefinition $code -PassThru | out-null
Function Show-Console {
  $consolePtr = [Console.Window]::GetConsoleWindow()
  #5 show
  [Console.Window]::ShowWindow($consolePtr, 5)
}
Function Hide-Console {
  $consolePtr = [Console.Window]::GetConsoleWindow()
  #0 hide
  [Console.Window]::ShowWindow($consolePtr, 0)
}
Hide-Console | out-null

#-------------------------------------------------------------

#----------------------------------------------
#region Import Assemblies
#----------------------------------------------
[void][Reflection.Assembly]::Load('System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089')
[void][Reflection.Assembly]::Load('System.Data, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089')
[void][Reflection.Assembly]::Load('System.Drawing, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a')
#endregion Import Assemblies

function Main {
<#
    .SYNOPSIS
        The Main function starts the project application.
    
    .PARAMETER Seconds
        $Seconds is the number of seconds for the countdown timer.
    
    .NOTES
        Use this function to initialize your script and to call GUI forms.
		
    .NOTES
        To get the console output in the Packager (Forms Engine) use: 
		$ConsoleOutput (Type: System.Collections.ArrayList)
#>
	Param ([int]$Seconds)
		
	#--------------------------------------------------------------------------
	#TODO: Add initialization script here (Load modules and check requirements)
	
	
	#--------------------------------------------------------------------------
	
	if((Call-MainForm_psf($Seconds)) -eq 'OK')
	{
		
	}
	
	$global:ExitCode = 0 #Set the exit code for the Packager
}

function Call-MainForm_psf
{
	Param ([int]$Seconds)

	#----------------------------------------------
	#region Import the Assemblies
	#----------------------------------------------
	[void][reflection.assembly]::Load('System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089')
	[void][reflection.assembly]::Load('System.Data, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089')
	[void][reflection.assembly]::Load('System.Drawing, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a')
	#endregion Import Assemblies

	#----------------------------------------------
	#region Generated Form Objects
	#----------------------------------------------
	[System.Windows.Forms.Application]::EnableVisualStyles()
	$MainForm = New-Object 'System.Windows.Forms.Form'
	$panel2 = New-Object 'System.Windows.Forms.Panel'
	$ButtonCancel = New-Object 'System.Windows.Forms.Button'
	$ButtonLogoffNow = New-Object 'System.Windows.Forms.Button'
	$panel1 = New-Object 'System.Windows.Forms.Panel'
	$labelITSystemsMaintenance = New-Object 'System.Windows.Forms.Label'
	$labelSecondsLeftToLogoff = New-Object 'System.Windows.Forms.Label'
	$labelTime = New-Object 'System.Windows.Forms.Label'
	$labelDescription = New-Object 'System.Windows.Forms.Label'
	$timerUpdate = New-Object 'System.Windows.Forms.Timer'
	$InitialFormWindowState = New-Object 'System.Windows.Forms.FormWindowState'
	$progressBar1 = New-Object 'System.Windows.Forms.ProgressBar'
	#endregion Generated Form Objects

	#----------------------------------------------
	# User Generated Script
	#----------------------------------------------
	$TotalTime = $Seconds #in seconds
	
	$MainForm_Load={
		#TODO: Initialize Form Controls here
		$labelTime.Text = "{0:D2}" -f $TotalTime #$TotalTime
		#Add TotalTime to current time
		$script:StartTime = (Get-Date).AddSeconds($TotalTime)
		#Start the timer and progress bar
		$timerUpdate.Start()
		$progressBar1.PerformStep()
	}
	
	
	$timerUpdate_Tick={
		# Define countdown timer
		[TimeSpan]$span = $script:StartTime - (Get-Date)
		#Update the display
		$labelTime.Text = "{0:N0}" -f $span.TotalSeconds
		$timerUpdate.Start()
		if ($span.TotalSeconds -le 0)
		{
			$timerUpdate.Stop()
			shutdown.exe /l /f
		}
		$progressBar1.PerformStep()
	}
	
	$ButtonLogoffNow_Click = {
		# Logoff the computer immediately
		shutdown.exe /l /f
	}
	
	$ButtonCancel_Click={
		$MainForm.Add_Closing({$_.Cancel = $false}) # Re-enable closing the form
		$MainForm.Close()
	}
	
	$labelITSystemsMaintenance_Click={
		#TODO: Place custom script here
		
	}
	
	$panel2_Paint=[System.Windows.Forms.PaintEventHandler]{
	#Event Argument: $_ = [System.Windows.Forms.PaintEventArgs]
		#TODO: Place custom script here
		
	}
	
	$labelTime_Click={
		#TODO: Place custom script here
		
	}
		# --End User Generated Script--
	#----------------------------------------------
	#region Generated Events
	#----------------------------------------------
	
	$Form_StateCorrection_Load=
	{
		#Correct the initial state of the form to prevent the .Net maximized form issue
		$MainForm.WindowState = $InitialFormWindowState
	}
	
	$Form_StoreValues_Closing=
	{
		#Store the control values
	}

	
	$Form_Cleanup_FormClosed=
	{
		#Remove all event handlers from the controls
		try
		{
			$ButtonCancel.remove_Click($buttonCancel_Click)
			$ButtonLogoffNow.remove_Click($ButtonLogoffNow_Click)
			$panel2.remove_Paint($panel2_Paint)
			$labelITSystemsMaintenance.remove_Click($labelITSystemsMaintenance_Click)
			$labelTime.remove_Click($labelTime_Click)
			$MainForm.remove_Load($MainForm_Load)
			$timerUpdate.remove_Tick($timerUpdate_Tick)
			$MainForm.remove_Load($Form_StateCorrection_Load)
			$MainForm.remove_Closing($Form_StoreValues_Closing)
			$MainForm.remove_FormClosed($Form_Cleanup_FormClosed)
		}
		catch [Exception]
		{ }
	}
	#endregion Generated Events

	#----------------------------------------------
	#region Generated Form Code
	#----------------------------------------------
	$MainForm.SuspendLayout()
	$panel2.SuspendLayout()
	$panel1.SuspendLayout()
	#
	# MainForm
	#
	$MainForm.Controls.Add($panel2)
	$MainForm.Controls.Add($panel1)
	$MainForm.Controls.Add($labelSecondsLeftToLogoff)
	$MainForm.Controls.Add($labelTime)
	$MainForm.Controls.Add($labelDescription)
	$MainForm.Controls.Add($progressBar1)
	$MainForm.AutoScaleDimensions = '6, 13'
	$MainForm.AutoScaleMode = 'Font'
	$MainForm.BackColor = 'White'
	$MainForm.ClientSize = '373, 400'
	$MainForm.MaximizeBox = $False
	$MainForm.MinimizeBox = $False
	$MainForm.Add_Closing({$_.Cancel = $true}) # Disable closing the form using the X button
	$MainForm.WindowState = 'Normal'
	$MainForm.Name = 'MainForm'
	$MainForm.ShowIcon = $False
	$MainForm.ShowInTaskbar = $False
	$MainForm.StartPosition = 'CenterScreen'
	$MainForm.Text = 'Citrix VDA Autologon Process'
	$MainForm.TopMost = $True
	$MainForm.add_Load($MainForm_Load)
	#
	# panel2
	#
	$panel2.Controls.Add($ButtonCancel)
	$panel2.Controls.Add($ButtonLogoffNow)
	$panel2.BackColor = 'ScrollBar'
	$panel2.Location = '0, 326'
	$panel2.Name = 'panel2'
	$panel2.Size = '378, 80'
	$panel2.TabIndex = 9
	$panel2.add_Paint($panel2_Paint)
	#
	# ButtonCancel
	#
	$ButtonCancel.Location = '250, 17'
	$ButtonCancel.Name = 'ButtonCancel'
	$ButtonCancel.Size = '77, 45'
	$ButtonCancel.TabIndex = 7
	$ButtonCancel.Text = 'Cancel'
	$ButtonCancel.UseVisualStyleBackColor = $True
	$ButtonCancel.add_Click($buttonCancel_Click)
	#
	# ButtonLogoffNow
	#
	$ButtonLogoffNow.Font = 'Microsoft Sans Serif, 8.25pt, style=Bold'
	$ButtonLogoffNow.ForeColor = 'DarkRed'
	$ButtonLogoffNow.Location = '42, 17'
	$ButtonLogoffNow.Name = 'ButtonLogoffNow'
	$ButtonLogoffNow.Size = '91, 45'
	$ButtonLogoffNow.TabIndex = 0
	$ButtonLogoffNow.Text = 'Logoff Now'
	$ButtonLogoffNow.UseVisualStyleBackColor = $True
	$ButtonLogoffNow.add_Click($ButtonLogoffNow_Click)
	#
	# panel1
	#
	$panel1.Controls.Add($labelITSystemsMaintenance)
	$panel1.BackColor = '0, 114, 198'
	$panel1.Location = '0, 0'
	$panel1.Name = 'panel1'
	$panel1.Size = '375, 67'
	$panel1.TabIndex = 8
	#
	# labelITSystemsMaintenance
	#
	$labelITSystemsMaintenance.Font = 'Microsoft Sans Serif, 14.25pt'
	$labelITSystemsMaintenance.ForeColor = 'White'
	$labelITSystemsMaintenance.Location = '11, 18'
	$labelITSystemsMaintenance.Name = 'labelITSystemsMaintenance'
	$labelITSystemsMaintenance.Size = '310, 23'
	$labelITSystemsMaintenance.TabIndex = 1
	$labelITSystemsMaintenance.Text = 'Citrix VDA Autologon Logoff Timer'
	$labelITSystemsMaintenance.TextAlign = 'MiddleLeft'
	$labelITSystemsMaintenance.add_Click($labelITSystemsMaintenance_Click)
	#
	# labelSecondsLeftToLogoff
	#
	$labelSecondsLeftToLogoff.AutoSize = $True
	$labelSecondsLeftToLogoff.Font = 'Microsoft Sans Serif, 9pt, style=Bold'
	$labelSecondsLeftToLogoff.Location = '87, 283'
	$labelSecondsLeftToLogoff.Name = 'labelSecondsLeftToLogoff'
	$labelSecondsLeftToLogoff.Size = '155, 15'
	$labelSecondsLeftToLogoff.TabIndex = 5
	$labelSecondsLeftToLogoff.Text = 'Seconds left to logoff :'
	#
	# labelTime
	#
	$labelTime.AutoSize = $True
	$labelTime.Font = 'Microsoft Sans Serif, 9pt, style=Bold'
	$labelTime.ForeColor = '192, 0, 0'
	$labelTime.Location = '237, 283'
	$labelTime.Name = 'labelTime'
	$labelTime.Size = '43, 15'
	$labelTime.TabIndex = 3
	$labelTime.Text = '00:60'
	$labelTime.TextAlign = 'MiddleCenter'
	$labelTime.add_Click($labelTime_Click)
	#
	# labelDescription
	#
	$labelDescription.Font = 'Microsoft Sans Serif, 9pt'
	$labelDescription.Location = '12, 76'
	$labelDescription.Name = 'labelDescription'
	$labelDescription.Size = '350, 204'
	$labelDescription.TabIndex = 2
	$labelDescription.Text = 'When a Citrix VDA restarts as either part of scheduled reboots, or when non-persistent desktops reboot to reset, the first logon is generally always the longest. So we use an autologon account to prime the VDA when it restarts. This script is designed to logoff the auto logged on session when the countdown reaches 0.

If using Power Management with non-persistent desktops, you must ensure this script runs before the Citrix Desktop Service (BrokerAgent) service starts, or you will end up in a reboot loop.

If you do not wish to logoff at this time, please click on the cancel button below.'
	#
	# progressBar1
	#
	$progressBar1.Location = '12, 306'
	$progressBar1.Size = '350, 15'
	$progressBar1.Name = 'progressBar1'
	$progressBar1.Minimum = 0
	$progressBar1.Maximum = $TotalTime
	$progressBar1.Step = 1
	$progressBar1.Value = 0
	$progressBar1.Style = 'continuous'
	#
	# timerUpdate
	#
	$timerUpdate.Interval = 1000 # 1 second
	$timerUpdate.add_Tick($timerUpdate_Tick)
	$panel1.ResumeLayout()
	$panel2.ResumeLayout()
	$MainForm.ResumeLayout()
	#endregion Generated Form Code

	#----------------------------------------------

	#Save the initial state of the form
	$InitialFormWindowState = $MainForm.WindowState
	#Init the OnLoad event to correct the initial state of the form
	$MainForm.add_Load($Form_StateCorrection_Load)
	#Clean up the control events
	$MainForm.add_FormClosed($Form_Cleanup_FormClosed)
	#Store the control values when form is closing
	$MainForm.add_Closing($Form_StoreValues_Closing)
	#Show the Form
	return $MainForm.ShowDialog()

}

#Start the application
Main ($Seconds)

Enjoy!

Jeremy Saunders

Jeremy Saunders

Delivering customer success through tech: IT Infrastructure | Citrix | End User Computing | Platform Engineering | DevOps | Full Stack Developer | Technical Architect | Improvisor | Aspiring Comedian | Midlife Adventurer at J House Consulting
Jeremy Saunders is the Problem Terminator; the MacGyver of IT. Views and Intellectual Property (IP) published on this site belong to Jeremy. Please refer to the About page for more information about Jeremy.