Skip to main content

Password Changes

Password Changes

Now that we have everything deployed, let's look toward the future. You should plan to update the password for your autologin users at some point. This probably wouldn't be much of a hassle if you only have a few of them, but for the sake of argument, let's assume you have at least 100 such devices and accounts. Changing the password in the GPO is easy enough, but do you really want to manually reset the password for every account? Of course not! Let's do things the fun way and write a script to handle the tedious manual tasks for us!

Considerations and Planning

Before we write our script, let's look at what we are trying to accomplish and what a successful autologin password change would look like. We need to:

  • Update the password in the GPO
  • Reset account passwords using the password in the GPO
  • Add descriptions to AD objects (users and computers) for easier troubleshooting and documentation

Additional considerations might include:

  • Autologin accounts and computers may be segmented into different groups with different policies based on use case and environment
  • You may not be able to update all accounts at once, so you might need different AD groups, GPOs, and/or OUs for the new password. This allows devices using the old password and devices using the new password to coexist during the transition
  • How frequently should the autologin password(s) be updated?

With all of that in mind, let's lay out a scenario. Let's say we have 100 autologin users and computers that we can't update all at once because they are in constant use, but we can do them in smaller batches of 25 at a time. We have decided that the password(s) need to be updated twice a year.

We won't complicate the scenario by adding segmented use cases because it shouldn't significantly change how we approach the problem.

Here's the plan:

  • Copy the exiting GPO and call the copy Windows 10 Autologin 2024 H1. Please avoid using names like "Windows 10 Autologin NEW" because the word "NEW" becomes ambiguous and unhelpful over time. Including 2024 H1 in the name makes it clear that this policy is intended for the first half of the year for 2024
  • Create a corresponding group named Autologin Computers 2024 H1 that we can move computers to as we run through batches. We'll tie this and the Autologin Users group to the new GPO. This change will be transparent to our autologin users, so we do not need to create a new group for them
  • Write and run a PowerShell script to automatically handle manual changes

Implementation

Active Directory Prep Work

We're not going to script this part. We absolutely could, but there is much less value in doing so because we only need to perform these particular tasks once every 6 months per our scenario.

Create the new Autologin Computers 2024 H1 group in AD

image.png

Make a copy of the Windows 10 Autologin GPO and name the copy Windows 10 Autologin 2024 H1. Select Preserve the existing permissions in the Copy GPO box.

image.png

image.png

Open the GPO and go to Computer Configuration\Preferences\Windows Settings\Registry\ and change the DefaultPassword to reflect the new password.

image.png

Replace the Autologin Computers group with the new Autologin Computers 2024 H1 group in the Security Filtering section of the GPO.

image.png

image.png

User Update Script

Update-AutologinUsers.ps1 (click to expand)
<#
	.SYNOPSIS
	Updates autologin account on Windows PCs
 
	.DESCRIPTION
	Brings autologin user accounts in line with the specified group policy object (GPO) in Active Directory (AD)
	
	This is accomplished by doing the following:
	- Resetting the account password in AD using the password found in the GPO
	- Optionally moving the associated computer to a new/different AD OU
	- Optionally moving the associated computer to a new/different AD group
	
	Usage Notes
	- The script always requires an argument for the GPO and target computers
	- You can't use both the MoveComputer and UpdateComputerGroup switches at the same time
	
	Most values are not hardcoded. This provides flexibility and avoids leakage of sensitive information.
	
	Author	: Newb
	Date	: 04/23/2024
 
	.EXAMPLE
	Update-AutologinUsers.ps1 -GPOName 'Dummy GPO' -TargetComputers 'DummyPC'
	- Resets the autologin user account's password for the target computer using the password found in the specified GPO
	- Updates user and computer object descriptions in AD
	
	.EXAMPLE
	Update-AutologinUsers.ps1 -GPOName 'Dummy GPO' -TargetComputers (Get-Content .\Dummy PC List)
	- Same as the previous example but the target computers are stored in a text file
	
	.EXAMPLE
	Update-AutologinUsers.ps1 -GPOName 'Dummy GPO' -TargetComputers 'DummyPC' -UpdateComputerGroup -OldGroup 'Old Dummy Group' -NewGroup 'New Dummy Group'
	- Resets the autologin user account's password for the target computer using the password found in the specified GPO
	- Updates user and computer object descriptions in AD
	- Removes computer from old AD group
	- Adds computer to new AD group
 
	.EXAMPLE
	Update-AutologinUsers.ps1 -GPOName 'Dummy GPO' -TargetComputers 'DummyPC' -MoveComputer -ComputerDestinationPath 'OU=Dummy OU,DC=dummy,DC=com'
	- Resets the autologin user account's password for the target computer using the password found in the specified GPO
	- Updates user and computer object descriptions in AD
	- Moves computer to a new AD OU
#>

########---------- Define Script Scope Parameters -----------------------------########
#
#
#

[CmdletBinding(DefaultParameterSetName='None')]param
(
	[Parameter(Mandatory)][String]$GPOName, # GPO from which to pull the autologin account password
	[Parameter(Mandatory)][String[]]$TargetComputers, # Can be a single string or an array of strings
	
	[Parameter(ParameterSetName='MoveComputer')][Switch]$MoveComputer, # Set if you want to move the computer to a new AD OU
	[Parameter(ParameterSetName='MoveComputer',Mandatory=$True)][String]$ComputerDestinationPath, # Distinguished name for new OU. Only used if MoveComputer switch is set
	
	[Parameter(ParameterSetName='UpdateComputerGroup')][Switch]$UpdateComputerGroup, # Set if you want to change the AD group the computer belongs to
	[Parameter(ParameterSetName='UpdateComputerGroup',Mandatory=$True)][String]$OldGroup, # AD Group to remove computer from
	[Parameter(ParameterSetName='UpdateComputerGroup',Mandatory=$True)][String]$NewGroup # AD group to add computer to
)

#
#
#
########---------- Define Script Scope Variables ------------------------------########
#
#
#

$Date = Get-Date -Format "yyyy-MM-dd" # For logs
$RegPath = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon\' # Where autologin values live
$Results = [ordered]@{} # Dictionary for tracking progress
$Tasks = # Functions to run through
@(
'Move-Computer',
'Update-ComputerGroup',
'Reset-UserPassword',
'Update-UserDescription',
'Update-ComputerDescription'
)

#
#
#
########---------- Define Script Scope Functions ------------------------------########
#
#
#

##----- Work Functions: Functions doing actual work

function Move-Computer # Move computer to new AD OU
{
	## Notes
	# Function doesn't run unless the MoveComputer switch is set
	
	if ($MoveComputer) # If MoveComputer switch is on
	{
		## Define Function Scope Variables
		# $Computer and $ComputerDestinationPath inherited from parent scope(s)
		$ComputerCurrentDN = (Get-ADComputer -Identity $Computer -ErrorAction Stop).DistinguishedName # Computer's distinguished name
		$ComputerCurrentPath = $ComputerCurrentDN.Substring($ComputerCurrentDN.IndexOf(",")+1) # LDAP path to computer
		
		## Actions
		if ($ComputerDestinationPath -eq $ComputerCurrentPath)
		{
			return "$Computer is already at $ComputerDestinationPath"
		}
		else
		{
			Move-ADObject -Identity $ComputerCurrentDN -TargetPath $ComputerDestinationPath -ErrorAction Stop
			return 'Completed'
		}
	}
	else
	{
		return 'Not indicated'
	}
}

function Update-ComputerGroup # Removes from old group, adds to new group
{
	## Notes
	# Function doesn't run unless the UpdateComputerGroup switch is set
	
	if ($UpdateComputerGroup) # If UpdateComputerGroup switch is on
	{
		## Define Function Scope Variables
		# $Computer, $OldGroup, and $NewGroup inherited from paarent scope(s)
		$ComputerCurrentDN = (Get-ADComputer -Identity $Computer -ErrorAction Stop).DistinguishedName # Computer's distinguished name
		$IsAlreadyInGroup = (Get-ADGroupMember -Identity $NewGroup).Name -match $Computer
	
		## Actions
		if ($IsAlreadyInGroup)
		{
			return "$Computer is already a member of $NewGroup"
		}
		else
		{
			try # Remove-ADGroupMember throws an exception if the group doesn't exist
			{
				Remove-ADGroupMember -Identity $OldGroup -Members $ComputerCurrentDN -Confirm:$False
			}
			catch # We don't need to stop the script over this, so just warn the user
			{
				Write-Warning $_.exception.message
			}
			
			Add-ADGroupMember -Identity $NewGroup -members $ComputerCurrentDN
			return 'Completed'
		}
	}
	else
	{
		return 'Not Indicated'
	}
}

function Reset-UserPassword # Reset password... duh
{
	# $User and $GPOPassword inherited from parent scope(s)
	Set-ADAccountPassword -Identity $User -Reset -NewPassword (ConvertTo-SecureString -AsPlainText $GPOPassword -Force) -ErrorAction Stop
	return 'Completed'
}

function Update-UserDescription # Add associated computer to account description
{
	# $User and $Computer inherited from parent scope(s)
	Set-ADUser -Identity $User -Description "Autologin account for $Computer" -ErrorAction Stop
	return 'Completed'
}

function Update-ComputerDescription # Add associated account to computer description
{
	# $User and $Computer inherited from parent scope(s)
	Set-ADComputer -Identity $Computer -Description "Autologin Account: $User" -ErrorAction Stop
	return 'Completed'
}

##----- Support Functions: Functions that provide input data, reporting, etc

function Get-GPOPassword # Get autologin password from GPO
{
	# $GPOName and $RegPath inherited from parent scope(s)
	(Get-GPPrefRegistryValue -Name $GPOName -Context Computer -Key $RegPath.Replace(':', '') -ValueName 'DefaultPassword' -ErrorAction Stop).Value
}

function Get-Users # Get autologin usernames from target computers
{
	# $TargetComputers and $RegPath inherited from parent scope(s)
	$Users = Invoke-Command -ComputerName $TargetComputers {(Get-ItemProperty -Path $Using:RegPath -Name DefaultUserName).DefaultUserName}
	return $Users | Sort-Object -Property PSComputerName # Sort results
}

function Fail-RemainingTasks # Adds status to update tasks that were not executed
{
	## Define Function Scope Parameters
	param ([Parameter(Mandatory)]$Message)
	
	## Define Function Scope Variables
	# $Results, $Computer, $Tasks, and $Task inherited from parents scope(s)
	# $RemainingTasks gets an index range
	$RemainingTasks = ($Tasks.IndexOf($Task)+1)..($Tasks.GetUpperBound(0))
	
	foreach ($Task in $Tasks[$RemainingTasks])
	{
		$script:Results[$Computer] += @{$Task = $Message}
	}
}

function Print-Summary # Output results summary for all tasks run
{
	Write-Host -BackgroundColor White -ForegroundColor Black "`nResults Summary`n"
	foreach ($Computer in $Results.Keys)
	{
		Write-Host -BackgroundColor DarkBlue -ForegroundColor White "$($Computer)"
		foreach ($Record in $Results.$Computer)
		{
			$Record | Format-Table -Autosize
		}
	}
}

#
#
#
########---------- Run Script -------------------------------------------------########
#
#
#

# Start log
Start-Transcript -Append -Path .\Update-Autologins-$Date.log

# Collect data
$GPOPassword = Get-GPOPassword
$Users = Get-Users

:UserLoop foreach ($User in $Users)
{
	$Computer = $User.PSComputerName
	
	try # Confirm user exists. 
	{
		Get-ADUser -Identity $User -ErrorAction Stop | Out-Null
	}
	catch # Skip tasks for this user if it doesn't exist in AD
	{
		Write-Host -ForegroundColor Red "$($Computer): $($_.exception.message)"
		Fail-RemainingTasks -Message $_.exception.message
		continue
	}
	
	Write-Host -BackgroundColor DarkBlue "`n Applying changes to user $User on $Computer"
	:TaskLoop foreach ($Task in $Tasks)
	{		
		try
		{
			$Results[$Computer] += @{$Task = & $Task} # Call task and add output to $Results
			Write-Host -ForegroundColor Green "$($Computer): $Task - $($Results.$Computer.$Task)"
		}
		catch # Skip remaining tasks in current loop if exception occurs
		{
			Write-Host -ForegroundColor Red "$($Computer): $Task - $($_.exception.message)"
			$Results[$Computer] += @{$Task = $_.exception.message}
			Fail-RemainingTasks -Message "Process failed at $Task"
			Write-Host "$($Computer): Skipping reboot because of unsuccessful task(s)"
			continue UserLoop # Point back to top of user loop
		}
	}
	
	Write-Host "$($Computer): Rebooting"
	Invoke-Command -ComputerName $Computer {Restart-Computer -Force}
}

Print-Summary

# End log
Stop-Transcript

I'll do my very best not to overexplain the above script, but I do want to briefly run through what it does and how it works.

Script Structure and Explanation

If you open it up, you'll see plenty of comment-based help right at the top. The script itself is broken down into a few sections with parameters and variables at the top, functions in the middle, and the actual execution portion down at the bottom.

The execution portion of the script starts by collecting information. It gets the current autologin password from the GPO and gets usernames from each respective computer in a list of target computers. Once it has that information, it iterates through two main loops, one inside the other. The outer loops goes through each username while the inner loop goes through each of 5 tasks (two of which are optional). These tasks:

  • Move the computer to a new AD OU (optional)
  • Update the computer's AD group memberships (optional)
  • Reset the autologin account password
  • Update the user object description in AD
  • Update the computer object description in AD

Once all indicated tasks for a given user have been successfully completed, the computer is rebooted. If the script encounters an error for a given user, the inner loop starts over with the next user in line and the computer is not rebooted. Once the outer loop has iterated through all of the users in the list, it will print a summary of completed tasks, including any errors encountered. The script also creates a log file which includes the same output you see at the command line.

Script Execution

Now that we've done our prep work and understand how our update script works, let's run through our scenario. 

First, let's do a quick dry run to make sure the script works as expected. In the example below, you can see that only one of the computers has been targeted and that the script is pointing to the current GPO.

.\Update-AutologinUsers.ps1 -GPOName 'Windows 10 Autologin' -TargetComputers 'AUTOLOGINPC01'

The output will look like the below image. Output is divided into two sections, one for tracking current progress, and another for displaying the final Results Summary. Because we didn't specify a group change or OU move, those tasks are listed as Not Indicated.

image.png

If we list the files in our current directory, we'll see that our log file has been created.

image.png

Having had a successful test, let's move forward with the actual update. We'll target the new Windows 10 Autologin 2024 H1 GPO and move the target computers out of the old Autologin Computers group and into Autologin Computers 2024 H1. I have added the three target computers (pretend we have a full batch of 25) to a text file called computers.txt.

.\Update-AutologinUsers.ps1 -GPOName 'Windows 10 Autologin 2024 H1' -TargetComputers (Get-Content .\computers.txt) -UpdateComputerGroup -OldGroup 'Autologin Computers' -NewGroup 'Autologin Computers 2024 H1'

You should see similar output as with the test run. Here's our progress tracker

image.png

and our results summary

image.png

Let's confirm our success by looking at one of our computers. We can see that it's in the new AD group

image.png

and the password in the registry has been updated

image.png

If we check the rest of our computers, we'll see the same success!

Additional

Alternative Scenario

My preferred approach to managing autologin is to use AD groups, but you can certainly manage the same policies in a similar fashion using OUs instead. For example, we can set up two GPOs and OUs with these names:

  • Windows 10 Autologin H1
  • Windows 10 Autologin H2

Every 6 months, we just need to change the password for the unused GPO and move all of the computers to the vacant OU. To do this, just use the same script with the MoveComputer switch.

.\Update-AutologinUsers.ps1 -GPOName 'Windows 10 Autologin H1' -TargetComputers (Get-Content .\computers.txt) -MoveComputer -ComputerDestinationPath 'OU=Windows 10 Autologin H1,DC=newbadmin,DC=test'

The MoveComputer and UpdateComputerGroup switches are mutually exclusive.

Failure Scenarios

You won't always be running the script in ideal conditions. With that in mind, I have included some sample failure scenarios with corresponding expected output.

Computer Already in New Group

This isn't really a failure because the script will continue without any issues, but it is considered nonstandard output.

image.png

Username not in Registry

The script will throw an error for the affected computer and continue forward with all unaffected computers. Note the hostname of the affected computer at the bottom of the message.

image.png

Old Group Not in AD

This is another scenario where there is no impact to the completion of tasks, but you will see a warning message pop up in the output.

image.png

Autologin User Not in AD

This will cause all tasks for the affected computer to fail with the same error message.

image.png

Password in GPO Not Compliant with Password Policy

To demonstrate, I've added a value of badpassword to the GPO. When we run the script, the computer group change task runs, but the password reset fails with an expected error message. All subsequent tasks will fail with the message that the process failed at the password reset.

image.png

There are some additional protections that could be added to the script to prevent it from executing at all in this scenario, but my assumption is that the admin responsible for the GPO should be fully aware of current standards and that the new password has been tested and confirmed to be working ahead of time.