Monday, June 5, 2017

PowerShell - Force Idle User Logoff After X Hours

I came across a customer who wanted to log users off of their machines after an idle period.  After getting a few more details, it turns out they really just wanted the ability to prevent users from staying logged in forever.

Since they were already using Windows' security feature of locking the screen on idle (https://technet.microsoft.com/en-us/library/jj966265(v=ws.11).aspx), this became a rather trivial task.

The logic works on the following design-parameters:
  • Run continuously throughout users' logon sessions.
  • Logoff users if they become idle for roughly 8 hours.
To accomplish this, the following logic was employed:

  • Detect an "idle" event
    • In this case, we're using the system's own idle-detection, resulting in a screen lock, which creates a system-level event that we can hook and respond to upon occurrence.
  • Respond to each event type
    • On screen-lock, we need to create a countdown timer that performs work (the logoff) if the countdown completes.
    • On screen-unlock, we need to be able to destroy/reset the timer so that it can handle being locked/unlocked multiple times throughout a work day and still fire once a user locks their screen to go home for the day.
To run the script, it needs to be executed in some way at user-logon.  That can be accomplished very easily in one of two standard ways:
  1. A Logon Script.  Group Policy or Active Directory User Account Properties (deprecated)
  2. A Scheduled Task.  The trigger for the scheduled task should be, 'At user logon'
A few acknowledgements I'd like to add:
  • A smart user will be able to figure out what this script does and because it runs under their user context, can kill this process at-will.  There are a few options in this realm:
    • Log the relevant events (see below) -- Logging has been added.
    • Create a Windows service to perform this function which would require them to at least be an Administrator before they could kill the process

Change Log:
  1. Edited script to pop up a message to the user if their last session had been forcefully logged off by this script.

Script:
######################################################################### ## Title: Force-LogoffAfterX.ps1 ## Author: Cameron Wilson (thepip3r) ## Create-Date: 2017-06-05 ## Description: Logs off users after a given timeframe once a screen ## lock occurs. This essentially becomes an idle-logoff. ## PowerShell V: 2.0+ ## Environment: Intended to be run infinitely during the user's logon session. ## Must run under the logged-on users' context. Expects ## to be used in tandem with idle screen lock security feature. ######################################################################### $ScriptName = (Split-Path -Leaf $MyInvocation.MyCommand.Path) ## Define Logoff Hours Interval $global:LogoffHours = 8 $global:LogPath = "$($env:TEMP)\$($env:COMPUTERNAME)_$($ScriptName).log" ## Stage the global objects required for the multiple disparate event tracking $global:Timer = New-Object Timers.Timer $global:Timer.Interval = ($global:LogoffHours*60*60*1000) #$global:Timer.Interval = (20000) ## 20 seconds for testing $global:Job = $null function global:ScriptLog ([string]$msg) { Write-Host $msg "$(Get-Date -UFormat ""%Y%m%d_%H%M%S"") - $($msg)" | Out-File -Append $global:LogPath } ## Define the event handler with a ScriptBlock $EventHandle = { param( [Microsoft.Win32.SessionSwitchReason]$EventReason ) ## Handle each event, as required. ## On SessionLock, create the a timer object that if it's "elapsed" period occurs, execute the force-logoff if ($EventReason -eq [Microsoft.Win32.SessionSwitchReason]::SessionLock) { ScriptLog "A SessionLock event has occurred." $global:Job = Register-ObjectEvent -InputObject $global:Timer -SourceIdentifier "LockedScreenTimeoutWorker" -EventName Elapsed -Action { ScriptLog "Idle timer object fired after $($global:Timer.Interval) milliseconds. User will be forcibly logged off." (Get-WmiObject Win32_OperatingSystem -EnableAllPrivileges).win32shutdown(4) } $global:Timer.Start() ## On SessionUnlock, we need to kill the logoff timer and reset the timer in case the screen re-locks } elseif ($EventReason -eq [Microsoft.Win32.SessionSwitchReason]::SessionUnlock) { ScriptLog "A SessionUnlock event has occurred." Unregister-Event -SourceIdentifier $global:Job.Name -Force $global:Timer.Stop() } else { ## Unhandled sessionswitch event } } if ([System.IO.File]::Exists($global:LogPath)) { $log = [System.IO.File]::ReadAllLines($global:LogPath) if ($log[-1] -match 'User will be forcibly logged off\.') { $t = $log[-1].Split(' - ') [System.Windows.Forms.MessageBox]::Show("Your previous session was logged off due to inactivity on/at: $($t[0])") } } try { ## Create the initial even subscript for the SystemEvents objects to watch for the different "SessionSwitch" events. $SystemEvent = [Microsoft.Win32.SystemEvents] $lstw = Register-ObjectEvent -InputObject $SystemEvent -SourceIdentifier "LockedScreenTimeoutWatcher" -EventName "SessionSwitch" -Action { $EventHandle.Invoke($args[1].Reason) } -ErrorAction Stop ScriptLog "Successfully created the 'SessionSwitch' SystemEvent hook." } catch { ScriptLog "An error occurred trying to register the 'SessionSwitch', SystemEvent hook: $_" } while (1) {}

No comments:

Post a Comment