Enable RDS Remote App Self-Service Password Reset and Notifications

One of the challenges introduced during the remote work revolution has been user password changes. When your worker population is directly connecting to your network on a regular basis, Active Directory password policies do the job nicely. VPN password resets can be difficult master, but it’s nothing some training can’t overcome.

What happens when the remote worker solution is based on Microsoft’s Remote Desktop Services? If your people are connecting to full remote desktops, the answer is easy. Teach them to press CTRL-ALT-END. If you publish remote applications instead of desktops, two issues present themselves pretty early after the rollout. It is possible to overcome the challenges using only native OS functions.

The first problem we’ll need to address is the actual method a user will employ to change their password. Lucky for us, the RDWeb feature of the Remote Desktop Services Role includes this functionality; it just has to be enabled. Log on to a server that hosts the RDWeb site. If you aren’t sure, log on to any member of the RDS farm, open an elevated PowerShell console, and run the one-liner below.

Get-RDServer -Role "RDS-WEB-ACCESS"

Open the IIS MMC on the RDWeb server. If you are load balancing, perform the following instructions on each RDWeb host to enable the hidden password reset page. First expand the chevrons until you get to RDWeb, then one more time and click Pages. Next double click “Application Settings” and set the “PasswordChangeEnabled” attribute to True. Restart IIS by opening an elevated PowerShell or CMD console and running IISRESET (add /Force to end sessions immediately).

Set the yellow attribute to true

Once completed, a hidden password reset page will be available at a variation of the URL you already use for RDWeb. The default for a US English server is https://yourdomain.com/RDWeb/Pages/en-US/password.aspx. All that is left is to address notification. There’s no prompt in a published application that effectively tells a user when their password is about to expire. We’ll use a PowerShell script to send an email with the relevant information like complexity rules and include a link to the reset page we just created.

Connectivity to SMTP is your responsibility. If you have an internal email relay server already established, then target the notification script at that by changing the $SMTPServer variable. You may also need to authenticate to your email server with a licensed account to send mail or configure an Office365 mail connector. The script can be modified to fit those situations but is not configured for it now.

Schedule the script below to run as a task from a server or workstation that is running RSAT for the appropriate version of Windows. Specifically, you’ll need the Active Directory PowerShell modules. Schedule the task with an appropriate service account, read-only access to Active Directory is required. Adjust the variables to fit your environment and needs.

#**Password_Expiration_RDS.ps1**
#Send password expiration password and link to reset for RDS Published Apps"
#Author: techbloggingfool.com

#Variables, populate with values for your environment and goals. 
$DaysToWarn = 7
$SupportTeam = "Support at XXX-XXX-XXXX"
$From = "Password AutoBot <noreply@yourdomainname.com>"
$Subject = "Reminder - Your Domain user account password will expire soon"
$SMTPServer = "Your Email Server's FQDN Goes between these quote marks"
$MailDomain = "Your email domain name goes here"
$RDWebResetURL = "The URL to your RDS password reset page goes here" 

function PreparePasswordPolicyMail ($ComplexityEnabled,$MaxPasswordAge,$MinPasswordAge,$MinPasswordLength,$PasswordHistoryCount)            
{            
    $verbosemailBody = "<p class=MsoNormal>&nbsp;</p><p class=MsoNormal>Below is a summary of the requirements for your new password:</p>`r`n<ul>`r`n"            
    $verbosemailBody += "<li class=MsoNormal>Your password must be changed every <b>" + $MaxPasswordAge + "</b> days.</li>`r`n"            
    If ($ComplexityEnabled) {
        $verbosemailBody += "<li class=MsoNormal>Your new password cannot contain any part of your name or username and must contain 3 of the 4 character types:<ul><li class=MsoNormal>Uppercase letters</li><li class=MsoNormal>Lowercase letters</li><li class=MsoNormal>Numbers</li><li class=MsoNormal>Symbols</li></ul>`r`n"
    }
    If ($MinPasswordLength -gt 0) {
        $verbosemailBody += "<li class=MsoNormal>Your new password must be at least <b>" + $MinPasswordLength + "</b> characters long.</li>`r`n"
    }
    If ($PasswordHistoryCount -gt 0) {
        $verbosemailBody += "<li class=MsoNormal>Your new password cannot be the same as the last <b>" + $PasswordHistoryCount + "</b> passwords that you have used.</li>`r`n"
    }
    If ($MinPasswordAge -eq 1) {
        $verbosemailBody += "<li class=MsoNormal>You must wait <b>" + $MinPasswordAge + "</b> days before you can change your password again.</li>`r`n"
    }
    If ($MinPasswordAge -gt 1) {
        $verbosemailBody += "<li class=MsoNormal>You must wait <b>" + $MinPasswordAge + "</b> days before you can change your password again.</li>`r`n"
    }
    $verbosemailBody += "</ul>`r`n"
    return $verbosemailBody            
}  

#HTML Email Header and Footer Formatting
$header = '<html>

<head>
<meta http-equiv=Content-Type content="text/html; charset=windows-1252">
<style>
<!--
 /* Font Definitions */
 @font-face
	{font-family:Wingdings;
	panose-1:5 0 0 0 0 0 0 0 0 0;}
@font-face
	{font-family:"Cambria Math";
	panose-1:2 4 5 3 5 4 6 3 2 4;}
@font-face
	{font-family:Calibri;
	panose-1:2 15 5 2 2 2 4 3 2 4;}
 /* Style Definitions */
 p.MsoNormal, li.MsoNormal, div.MsoNormal
	{margin:0in;
	font-size:11.0pt;
	font-family:"Calibri",sans-serif;}
@page WordSection1
	{size:8.5in 11.0in;
	margin:1.0in 1.0in 1.0in 1.0in;}
div.WordSection1
	{page:WordSection1;}
 /* List Definitions */
 ol
	{margin-bottom:0in;}
ul
	{margin-bottom:0in;}
-->
</style>

</head>

<body lang=EN-US style=''word-wrap:break-word''>

<div class=WordSection1>
'

$footer = "</div>

</body>

</html>
"
#Import AD Module and obtain data from it
Import-Module ActiveDirectory -Verbose:$false

$domainPolicy = Get-ADDefaultDomainPasswordPolicy            
$passwordexpirydefaultdomainpolicy = $domainPolicy.MaxPasswordAge.Days -ne 0            
            
if($passwordexpirydefaultdomainpolicy)            
{            
    $defaultdomainpolicyMaxPasswordAge = $domainPolicy.MaxPasswordAge.Days            
    if($verbose)            
    {            
        $defaultdomainpolicyverbosemailBody = PreparePasswordPolicyMail $PSOpolicy.ComplexityEnabled $PSOpolicy.MaxPasswordAge.Days $PSOpolicy.MinPasswordAge.Days $PSOpolicy.MinPasswordLength $PSOpolicy.PasswordHistoryCount            
    }            
} 

#Find accounts that are enabled and have expiring passwords
$users = Get-ADUser -filter {Enabled -eq $True -and PasswordNeverExpires -eq $False -and PasswordLastSet -gt 0} `
 -Properties "Name", "UserPrincipalName", "msDS-UserPasswordExpiryTimeComputed", "mS-DS-ConsistencyGuid" `
 | Where-Object {$_."ms-DS-ConsistencyGuid" -ne $null} | Select-Object -Property "Name", "UserPrincipalName", "SAMAccountName", `
 @{Name = "PasswordExpiry"; Expression = {[datetime]::FromFileTime($_."msDS-UserPasswordExpiryTimeComputed").ToLongDateString() + " " + [datetime]::FromFileTime($_."msDS-UserPasswordExpiryTimeComputed").ToLongTimeString() }}

If ($null -eq $Users) {
    Write-Error "No users found with the selected search criteria."
}
#check password expiration date and send email on match
foreach ($user in $users) {

    $DaysRemaining = (New-TimeSpan -Start $(Get-Date) -End $user.PasswordExpiry).Days

    if ($DaysRemaining -le $DaysToWarn) {

        $EmailBody = $header
        $EmailBody += "<p class=MsoNormal>Greetings $($user.Name),</p>`r`n"
        $EmailBody += "<p class=MsoNormal>&nbsp;</p><p class=MsoNormal>This is an automated password expiration warning.&nbsp; Your password will expire in <b>$DaysRemaining</b> days on <b>$($User.PasswordExpiry)</b>.</p>`r`n"

        $PSO= Get-ADUserResultantPasswordPolicy -Identity $user.SAMAccountName            
        if ($null -ne $PSO) {
            $EmailBody += PreparePasswordPolicyMail $PSO.ComplexityEnabled $PSO.MaxPasswordAge.Days $PSO.MinPasswordAge.Days $PSO.MinPasswordLength $PSO.PasswordHistoryCount            
        }
        else {
            $EmailBody += PreparePasswordPolicyMail $domainPolicy.ComplexityEnabled $domainPolicy.MaxPasswordAge.Days $domainPolicy.MinPasswordAge.Days $domainPolicy.MinPasswordLength $domainPolicy.PasswordHistoryCount            
        }
        
        $EmailBody += "<p class=MsoNormal>&nbsp;</p><p class=MsoNormal>Office workers, press the Ctrl+Alt+Delete keys on your keyboard and select ""Change a password"".</p>`r`n"
        $EmailBody += "<p class=MsoNormal><b>Note:</b> If you are a remote worker, first connect to the VPN.</p>`r`n"
        $EmailBody += "<p class=MsoNormal><b>Note:</b> Then go to ""$RDWebResetURL"" and reset your password.</p>`r`n"
        $EmailBody += "<p class=MsoNormal><b>Note:</b> Finally, reboot your computer and reconnect to the VPN with your new password.</p>`r`n"
        $EmailBody += "<p class=MsoNormal>&nbsp;</p><p class=MsoNormal>Please contact $SupportTeam if you need assistance changing your password.</p>`r`n"
        $EmailBody += "<p class=MsoNormal>&nbsp;</p><p class=MsoNormal>DO NOT REPLY TO THIS EMAIL. This is an unattended mailbox.</p>`r`n"
        $EmailBody += $footer
         
        $Recipient = $user.SAMAccountName + '@' + $MailDomain
        Send-MailMessage -To $Recipient -From $From -SmtpServer $SMTPServer -Subject $Subject -BodyAsHtml $EmailBody

        Write-Verbose "Server: $SMTPServer`r`nFrom: $From`r`nTo: $Recipient`r`nSubject: $Subject`r`nBody:`r`n$EmailBody"
    }
}

The task should be scheduled to run at least once per day. User’s will be able to reset their own passwords. The notification currently includes a line that suggests remote users should connect to the VPN, you may need to remove it if you utilize a different type of tunnel.

Improve Your Password Changing Practices

Back in the day, your network credentials were used to logon to your computer, get to your email server, and maybe to access some files or a printer. When you changed your password, if you changed it at all, it made sense to update it on the two or three systems that used it all at once and get it over with.

Now your password is synchronized to untold numbers of cloud platforms, on-premises application servers, VPNs, companion devices, and remote access solutions. Your password authenticates you to your computer and thanks to single sign on (SSO) it also logs you in to Zoom, Microsoft 365, your phone, and Salesforce. Single sign on usually involves an agent application that runs on your company directory servers and updates the other systems when a change is made to your account.

If you are like me you probably dread password change day and want to get it over with as quickly as possible. So you update your computer password when you are prompted and then preemptively logon to your other devices and apps and change them too. It seems prudent to update it everywhere, but our modern cloud connected networks are complicated. The agents that synchronize your password often encounter unresolvable conflicts between your company directory and the passwords that you manually updated. You may end up not being able to logon or lock-out your account.

I have a better experience and am less likely to end up calling the help desk when I wait for the sync agents to do their work. The next time that you are prompted to change your password try this. Only change it on the system that asked. Then wait for the other devices and software to require your new credentials. Some may take days, others may never ask.

Like the infamous CTRL+ALT+DEL, naming the alphanumeric strings we make up to authenticate our identities to our digital systems “passwords” was a mistake made long ago. It causes so much frustration to think of a single word that complies with the complexity requirements that many of us feel like we are losing at Scrabble. It’s no wonder that we forget them the next day. Here’s a tip, use phases like the lyrics from your favorite songs or quotes from movies. They are easier to remember and are actually more secure. Most password fields will allow at least 254 characters.