Unlock a locked-out on-premises Active Directory user, and optionally set a temporary password, from a domain controller or management server
Unlock a user's on-premises Active Directory account after too many wrong passwords, and optionally give them a temporary password they must change at next sign-in, by running a PowerShell action on a domain controller or a management server inside the customer's network. For MSPs and IT teams supporting customers with on-premises or hybrid Active Directory.
| Runs in | On the device (PowerShell, as SYSTEM), on a domain controller or a server with RSAT |
| Uses | None |
| Suggested approval | Anyone → Require approval from your admins group. The approver checks who is asking before approving |
| Category | Identity and access |
Test it with SuperIT before you rely on it
This is an example, not a ready-made action, and it hasn't been tested end to end, on a domain controller or on a member server with RSAT. Before you use it, work through it with SuperIT: ask SuperIT to review the script against your environment and requirements, and adjust it together. Then run it against a test account, as in Test it. Device actions can't use Test, so every run is real.
Why use an action instead of asking SuperIT
SuperIT has no access to on-premises Active Directory: its Microsoft 365 connection is read-only and cloud-only, and the cloud sandbox can't reach a private network. In a chat, SuperIT could only run Unlock-ADAccount on a device with a signed-in user session, which a domain controller rarely has, and each command would need an engineer's approval to run as SYSTEM. This action runs on a server inside the network as SYSTEM, isn't checked against command policies, works only on ordinary user accounts in the OUs you list, and its authorization policy decides who can run it and who approves.
To check whether an account is synced from on-premises Active Directory, or what a cloud-only account's status is in Microsoft 365, just ask SuperIT in chat.
When to use it
- "Jane is locked out of her PC at the Leeds office."
- "Unlock sam.lee's domain account and give him a temporary password."
- An end user asks "I'm locked out, can you unlock my account?" and an admin approves after calling them back.
What the action does
- Reads
username(the sAMAccountName, such asjdoe, or the UPN), thereset_passwordswitch andconfirm_reset. - If
reset_passwordis on, stops unlessconfirm_resetrepeats the username exactly. A password reset is never done by accident. - Loads the Active Directory PowerShell module and finds the domain's PDC emulator, the domain controller that handles lockouts.
- Searches only the OUs listed in the script, and finds exactly one user with that sAMAccountName or UPN there.
- Refuses the account if any of these is true:
- it's disabled;
- it has
adminCountset, which marks accounts that are, or were, in an AD protected group such as Domain Admins; - it's a member, directly or through nested groups, of a protected group: the built-in administrator and operator groups, Domain Admins, Enterprise Admins, Schema Admins, Group Policy Creator Owners, Key Admins, Cert Publishers, DnsAdmins, and the Exchange groups that can change domain permissions;
- its primary group isn't Domain Users;
- it has a service principal name, which usually means a service account;
- its name starts with
MSOL_,Sync_orAAD_, like the Microsoft Entra Connect sync accounts, or it'skrbtgt; - it's on the script's exclusion list.
- Unlocks the account if it's locked, on the PDC emulator.
- If
reset_passwordis on, sets a random 16-character temporary password and requires a change at next sign-in. - Prints the account's state before and after, and the temporary password if one was set.
These checks catch the common Tier 0 and service accounts, not all of them. An account can hold delegated rights over an OU, or over the domain, without being in any protected group. Keep accounts like that out of the OUs you list, or add them to the exclusion list.
What it doesn't do:
- It doesn't enable a disabled account. Disabling is usually deliberate, so do that in Active Directory yourself.
- It doesn't unlock or reset privileged, service or sync accounts, or any account outside the OUs you list. Do those by hand.
- It doesn't find what is locking the account out, such as an old password saved on a phone. Check the lockout events (4740) on the PDC emulator for that.
- It doesn't reset Microsoft 365 passwords for cloud-only accounts. See Reset a Microsoft 365 password.
- It doesn't send the temporary password to the user.
For accounts synced to Microsoft 365 with Microsoft Entra Connect and password hash sync, the new password reaches Microsoft 365 at the next sync. Check the user can sign in to Microsoft 365 before you tell them to use it there.
Before you begin
- A Windows server with the SuperIT RMM agent inside the customer's network, online:
- A domain controller. SYSTEM on a domain controller acts as that domain controller in Active Directory and can unlock and reset any account. This is the simpler choice.
- Or a management server with RSAT, with the Active Directory PowerShell module installed (
Install-WindowsFeature RSAT-AD-PowerShell). SYSTEM on a member server normally acts as the server's computer account, so delegate Read/write lockoutTime, Read/write pwdLastSet and Reset password on the users' OU to that computer account. This setup hasn't been tested with SuperIT, so test it with a test account before you rely on it.
- Windows PowerShell 5.1. The script runs in Windows PowerShell on the device, not PowerShell 7.
- Your user OUs and exclusions. Edit
Get-UnlockPolicyin the script: list the OUs that hold ordinary user accounts, and any account to exclude, such as a break-glass account or a service account without a service principal name. - Your role. Creating an action in a chat needs the Admin role, and so does setting it to run on a device. See Set an action to run on a device.
- No credential. The action uses the server's own identity. See Credentials.
- An authorization policy with approval. For example, one rule: Requester Anyone, Approver Require approval from
superit:admins. See Authorization policies and approvals.
Never use Anyone without approval
Scripts don't receive the identity of the person who asked, so the action can't check that an end user is unlocking their own account. With Anyone → No approval needed, any end user could unlock or reset any account the script allows. Keep an approver, and have them confirm the requester's identity, for example by calling them back.
Build it with SuperIT
Create an action called "Unlock an on-premises AD account".
This action must always run on a device: our domain controller DC01, not the cloud sandbox.
Runtime: PowerShell (Windows PowerShell 5.1 on the device, runs as SYSTEM). No top-level param() block; set $script:result.
Inputs: username (string, required - sAMAccountName or UPN), reset_password (boolean, optional), confirm_reset (string, optional).
If reset_password is true, stop unless confirm_reset equals the username.
Import the ActiveDirectory module (fail clearly if RSAT is missing). Use the domain's PDC emulator as -Server for every AD call.
Search only under these OUs: <your user OUs>. Find exactly one user by SamAccountName or UserPrincipalName.
Refuse: disabled accounts; adminCount = 1; members (including nested, via LDAP_MATCHING_RULE_IN_CHAIN) of the builtin admin and operator groups, Domain Admins, Enterprise Admins, Schema Admins, Group Policy Creator Owners, Key Admins, Enterprise Key Admins, Cert Publishers, DnsAdmins, Organization Management, Exchange Trusted Subsystem, Exchange Windows Permissions; primary group other than Domain Users; accounts with a servicePrincipalName; names starting MSOL_, Sync_ or AAD_, and krbtgt; and these accounts: <your exclusions>.
Unlock-ADAccount if LockedOut. If reset_password is confirmed: random 16-character password, Set-ADAccountPassword -Reset, then Set-ADUser -ChangePasswordAtLogon $true.
Print state before and after, and the temporary password in Stdout only, never in $result.
Authorization policy: Anyone, require approval from superit:admins.
SuperIT confirms the target, drafts the script for Windows PowerShell 5.1, validates it and creates the action as a draft. You review the script and publish it under Workflows → Actions.
Or build it yourself
Create the action under Workflows → Actions with the PowerShell runtime and these inputs, then set it to run on a device.
| Name | Type | Required | Example |
|---|---|---|---|
username |
string | Yes | jdoe |
reset_password |
boolean | No | false |
confirm_reset |
string | No | jdoe |
On a device, the script runs in Windows PowerShell as SYSTEM with $ErrorActionPreference = "Stop" already set, and SuperIT wraps it in try { }, so don't put a param() block at the top. Inputs arrive as strings in $ctx.inputs; booleans are "true" or "false". Invoke-AdAccountUnlock at the top lists the steps in order; each step is a small function below it, and the last line runs it. $result is set as $script:result, because a plain $result = inside a function stays local and never reaches the Result tab.
# Unlocks one ordinary, enabled on-premises AD user in the listed OUs on the PDC emulator and, if asked and
# confirmed, sets a temporary password they must change at next sign-in. Prints the state before and after.
function Invoke-AdAccountUnlock {
param($Username, $ResetPassword, $ConfirmReset)
$name = Assert-Inputs -Username $Username
$reset = Assert-ResetConfirmed -Name $name -ResetPassword $ResetPassword -ConfirmReset $ConfirmReset
$policy = Get-UnlockPolicy
Import-AdModule
$server = (Get-ADDomain).PDCEmulator
$user = Get-TargetAccount -Name $name -Server $server -SearchBases $policy.AllowedOus
Assert-AccountAllowed -User $user -Server $server -Policy $policy
Write-Output '=== AD ACCOUNT UNLOCK ==='
Write-Output "Account : $($user.SamAccountName) ($($user.UserPrincipalName))"
Write-Output "Server : $server"
Write-Output "Before : locked out $($user.LockedOut)"
$unlocked = Unlock-TargetAccount -User $user -Server $server
Write-Output $(if ($unlocked) { 'Unlocked.' } else { "The account wasn't locked out." })
$password = $null
if ($reset) { $password = Reset-TargetPassword -User $user -Server $server }
$after = Get-ADUser -Identity $user.DistinguishedName -Server $server -Properties LockedOut
Write-Output "After : locked out $($after.LockedOut)"
if ($password) {
Write-Output "Temporary password (must be changed at next sign-in): $password"
}
# $result is read after the script ends, so set it at script scope. The password stays out of it.
$script:result = @{ account = $user.SamAccountName; unlocked = $unlocked; locked_out = $after.LockedOut; password_reset = [bool]$password }
}
function Get-UnlockPolicy {
# Edit for each domain. Only accounts under AllowedOus are searched.
@{
AllowedOus = @('OU=Staff,DC=contoso,DC=local')
ExcludedAccounts = @('breakglass.admin', 'svc-scanner')
# Matched by SID so they're caught whatever language the domain uses: builtin Administrators,
# Account, Server, Print and Backup Operators.
ProtectedSids = @('S-1-5-32-544', 'S-1-5-32-548', 'S-1-5-32-549', 'S-1-5-32-550', 'S-1-5-32-551')
# Domain groups by RID: Domain Admins, Cert Publishers, Schema Admins, Enterprise Admins,
# Group Policy Creator Owners, Key Admins, Enterprise Key Admins.
ProtectedRids = @('512', '517', '518', '519', '520', '526', '527')
# Groups without a fixed RID that can still take over the domain.
ProtectedGroups = @('DnsAdmins', 'Organization Management', 'Exchange Trusted Subsystem', 'Exchange Windows Permissions')
}
}
function Assert-Inputs {
param($Username)
$name = "$Username".Trim()
# Also keeps quotes out of the -Filter string in Get-TargetAccount.
if ($name -notmatch '^[A-Za-z0-9._@-]{1,104}$') { throw "username must be a sAMAccountName or UPN, such as jdoe or [email protected]." }
$name
}
function Assert-ResetConfirmed {
param([string]$Name, $ResetPassword, $ConfirmReset)
if ($ResetPassword -ne 'true') { return $false }
if ("$ConfirmReset".Trim() -ne $Name) {
throw "To set a temporary password, enter the username again in confirm_reset. Nothing was changed."
}
$true
}
function Import-AdModule {
try {
Import-Module ActiveDirectory
} catch {
throw "The Active Directory PowerShell module isn't installed on $($env:COMPUTERNAME). Run this action on a domain controller or a server with RSAT-AD-PowerShell."
}
}
function Get-TargetAccount {
param([string]$Name, [string]$Server, [string[]]$SearchBases)
$found = @(foreach ($base in $SearchBases) {
Get-ADUser -Filter "SamAccountName -eq '$Name' -or UserPrincipalName -eq '$Name'" -SearchBase $base -Server $Server `
-Properties LockedOut, Enabled, AdminCount, UserPrincipalName, ServicePrincipalName, PrimaryGroupID
})
$found = @($found | Sort-Object -Property DistinguishedName -Unique)
if ($found.Count -eq 0) { throw "No Active Directory user called '$Name' in this action's user OUs." }
if ($found.Count -gt 1) { throw "More than one Active Directory user matches '$Name'. Use the sAMAccountName." }
$found[0]
}
function Assert-AccountAllowed {
param($User, [string]$Server, $Policy)
$sam = $User.SamAccountName
if (-not $User.Enabled) { throw "'$sam' is disabled. Enable it in Active Directory if that's intended." }
if ($User.AdminCount -eq 1) { throw "'$sam' is, or was, in an AD protected group. Unlock it by hand." }
if ($Policy.ExcludedAccounts -contains $sam) { throw "'$sam' is on this action's exclusion list. Unlock it by hand." }
if ($sam -match '^(MSOL_|Sync_|AAD_)' -or $sam -eq 'krbtgt') { throw "'$sam' looks like a sync or system account. Unlock it by hand." }
if (@($User.ServicePrincipalName).Count -gt 0) { throw "'$sam' has a service principal name, so it's probably a service account. Unlock it by hand." }
# 513 is Domain Users. Membership through the primary group doesn't show in the nested search below.
if ($User.PrimaryGroupID -ne 513) { throw "'$sam' has an unusual primary group. Unlock it by hand." }
$group = Find-ProtectedGroup -User $User -Server $Server -Policy $Policy
if ($group) { throw "'$sam' is a member, directly or through nested groups, of '$group'. Unlock it by hand." }
}
function Find-ProtectedGroup {
param($User, [string]$Server, $Policy)
# LDAP_MATCHING_RULE_IN_CHAIN (1.2.840.113556.1.4.1941) returns every group the user is in, including nested ones.
$dn = $User.DistinguishedName.Replace('\', '\5c').Replace('*', '\2a').Replace('(', '\28').Replace(')', '\29')
foreach ($g in @(Get-ADGroup -LDAPFilter "(member:1.2.840.113556.1.4.1941:=$dn)" -Server $Server)) {
$sid = "$($g.SID)"
$isDomainRid = $sid -like 'S-1-5-21-*' -and $Policy.ProtectedRids -contains ($sid -split '-')[-1]
if ($Policy.ProtectedSids -contains $sid -or $isDomainRid -or $Policy.ProtectedGroups -contains $g.Name) { return $g.Name }
}
$null
}
function Unlock-TargetAccount {
param($User, [string]$Server)
# Returns only a Boolean: the caller captures this function's output, so any message here would turn it into an array.
if (-not $User.LockedOut) { return $false }
try {
Unlock-ADAccount -Identity $User.DistinguishedName -Server $Server
} catch {
throw "Failed to unlock '$($User.SamAccountName)' on ${Server}: $_"
}
$true
}
function Reset-TargetPassword {
param($User, [string]$Server)
$password = New-TemporaryPassword
try {
Set-ADAccountPassword -Identity $User.DistinguishedName -Server $Server -Reset `
-NewPassword (ConvertTo-SecureString $password -AsPlainText -Force)
} catch {
throw "Failed to set a temporary password for '$($User.SamAccountName)' on ${Server}: $_"
}
try {
Set-ADUser -Identity $User.DistinguishedName -Server $Server -ChangePasswordAtLogon $true
} catch {
# The password has already changed, so print it before failing or nobody knows the account's password.
# Write-Host, not Write-Output: the caller captures this function's output and the throw would discard it.
Write-Host "Temporary password (NOT set to change at next sign-in): $password"
throw "Set a temporary password for '$($User.SamAccountName)' but couldn't require a change at next sign-in on ${Server}: $_. Tick 'User must change password at next logon' by hand."
}
$password
}
function New-TemporaryPassword {
# RNGCryptoServiceProvider, not RandomNumberGenerator.GetBytes(): the static method needs .NET Core.
$chars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789!#%*?'
$rng = New-Object System.Security.Cryptography.RNGCryptoServiceProvider
$bytes = New-Object byte[] 16
do {
$rng.GetBytes($bytes)
$password = -join ($bytes | ForEach-Object { $chars[$_ % $chars.Length] })
} until ($password -cmatch '[A-Z]' -and $password -cmatch '[a-z]' -and $password -match '\d')
$rng.Dispose()
$password
}
Invoke-AdAccountUnlock -Username $ctx.inputs.username -ResetPassword $ctx.inputs.reset_password -ConfirmReset $ctx.inputs.confirm_reset
The temporary password is printed to Stdout only, not put in $result. If your domain's password policy needs more than 16 characters or other character types, Set-ADAccountPassword fails with the policy's message; lengthen the password in New-TemporaryPassword.
The temporary password stays in run history
Stdout is kept with the run. Every Engineer and Admin in the team can open it, and so can the approver. If an engineer runs the action in a chat, the password also appears in that chat, which SuperIT reads. End users who request a run see a redacted view instead. The user must change the password at next sign-in, so pass it on promptly. If the run history might be seen by someone who shouldn't have the password, reset it again by hand.
Test it
You can't use Test on an action that runs on a device. (On a cloud-sandbox action, Test isn't a dry run either: it runs the script for real and skips approvals.) Every device run really changes Active Directory, so use a test account.
- Create a test user in one of the OUs listed in
Get-UnlockPolicy, and lock it by signing in with a wrong password until your lockout threshold is reached. - Publish the action under Workflows → Actions.
- Select Run, enter the test user's sAMAccountName, pick the domain controller under Run target, and select Run action. Approve the run.
- Check Stdout shows "locked out False" after the unlock, then sign in as the test user.
- Run it with
reset_passwordon andconfirm_resetleft blank, and confirm it fails with "Nothing was changed". - Run it again with
reset_passwordon and the username inconfirm_reset, and sign in with the temporary password. Windows should ask for a new password. - Run it with a test account that's in a nested group inside Domain Admins, one with a service principal name, and one outside your listed OUs, and confirm each run is refused.
Use it
- In a chat. Ask "Unlock jdoe's domain account" with the domain controller selected. SuperIT asks whether to run on the device and fills in
username. Ask SuperIT to make the domain controller the action's default device, so engineers don't have to pick it each time. - From end users. If your policy has an Anyone rule, an end user can ask SuperIT "I'm locked out of my account", and the run waits for an admin's approval. Before approving, check the run is for your domain controller and confirm the requester's identity. End users who request a run don't see its output, so they never see a temporary password: the approver passes it on after checking the user's identity.
- From the app. Open the action, select Run, enter the username, pick the server under Run target, and select Run action.
- Not from tickets automatically. Don't switch on Run automatically for this action. A run started from a ticket's Recommended actions panel goes to the action's default device or your team's designated device.
- For clients. Don't publish one copy of this action to every client. Published copies are read-only, and the OU list and exclusions are written into the script, so a copy can't be adjusted for each client's directory. Build a separate action for each client's directory instead, with that client's OUs and exclusions in
Get-UnlockPolicy, in the team that runs it.
Always pick the device. If no device is chosen, SuperIT runs a device action on the action's default device or your team's designated device. On a device without the Active Directory module, the run fails without changing anything.
Variations
- Unlock only: remove
reset_password,confirm_resetandReset-TargetPasswordfor a copy that engineers can run with No approval needed. Unlocking alone doesn't give anyone access they didn't have. Keep the account checks. - Show the lockout source: read event 4740 for the user from the PDC emulator's Security log with
Get-WinEventand print the Caller Computer Name. - Several domains: add a
domainselect input and pass the matching domain controller to-Server, with an OU list per domain.