When passwords are set to expire after a certain number of days in Active Directory, the remote users suffer because they do not get a notification like the local users do that their password is going to expire.
Eventually, it becomes too late for them to change their passwords and they get locked out. I found this out recently and did not believe that there was no built in support for this. I started researching and indeed, there was no built in support. The solution was to email the users, either through a script or manually before their passwords expire.
There are simple VB Scripts out there, which do this kind of stuff.
I created a C# Console Application which emails users if their password is going to expire, and also emails the report to the administrator. You can schedule it run every day as a Windows Scheduled Task. It will start emailing users when 15 (default) days are left for their passwords to expire.
There are a few things you will need:
1. config.xml file
2. EmailBody.txt file
3. A reference to Active DS Type Library
4. SettingsProvider.cs class
config.xml settings:
AdministratorEmail: To send reports
MailServer: The mail server used to send emails
Countdown: Used as a threshold to start emailing users
<?xml version="1.0" encoding="utf-8" ?>
<AdministratorEmail>Administrator@YourCompany.com</AdministratorEmail>
<MailServer>YourMailServer</MailServer>
<Countdown>15</Countdown>
</config>
The SettingsProvider.cs class is used to read values from the config.xml file.
SettingsProvider.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml;
using System.IO;
namespace PasswordNotification
{
class SettingsProvider
{
private const string CONFIG_FILE = "config.xml";
private static XmlDocument _xmlDoc = null;
private static string ConfigPath
{
get { return Directory.GetCurrentDirectory() + @"\" + CONFIG_FILE; }
}
public static void Save(string key, string value)
{
XmlNode node = _xmlDoc.SelectSingleNode("./config/" + key);
if (node != null)
{
node.InnerText = value;
Save();
}
else
{
// XML node doesnt exist thus create a new one
Get(key, value);
}
}
///<summary>
/// Gets a value of the settings. If the setting does not exist, defaultValue is returned.
///</summary>
public static string Get(string key, string defaultValue)
{
if (_xmlDoc == null)
{
_xmlDoc = new XmlDocument();
// Load config.xml
string fullPath = SettingsProvider.ConfigPath;
if (!File.Exists(fullPath))
{
// Xml declaration
XmlDeclaration declaration = _xmlDoc.CreateXmlDeclaration("1.0", null, null);
_xmlDoc.AppendChild(declaration);
// Root node <config>
XmlElement rootNode = _xmlDoc.CreateElement("config");
_xmlDoc.AppendChild(rootNode);
Save();
}
else
{
_xmlDoc.Load(fullPath);
}
}
XmlNode node = _xmlDoc.SelectSingleNode("./config/" + key);
if (node != null)
{
return node.InnerText;
}
else
{
// Add default value
XmlElement newSetting = _xmlDoc.CreateElement(key);
newSetting.InnerText = defaultValue;
_xmlDoc.ChildNodes[1].AppendChild(newSetting);
Save();
return defaultValue;
}
}
public static void Save()
{
if (_xmlDoc != null)
{
_xmlDoc.Save(SettingsProvider.ConfigPath);
}
}
}
}
Then there’s a text file called EmailBody.txt which stores a template email. It contains parameters like [USERNAME] and [DAYCOUNT] which I use to plug in custom values in code.
EmailBody.txt
<html><title></title><head></head><body style="font-size:0.7em;color:#666666;">
[USERNAME]
Your password is going to expire in [DAYCOUNT] days.
Please change your password to avoid account lockout.
The steps to change your password are as follows:
1) Log in to your machine
2) (If you are remote) Connect to YOURCOMPANY using the VPN Client
3) Once connected, press Ctrl / Alt / Del
4) From the menu, select Change Password
5) Enter your old password, then your new password twice.
6) Press OK
Administrator.
</body></html>
And here’s the main code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.DirectoryServices;
using ActiveDs;
using System.DirectoryServices.ActiveDirectory;
using System.IO;
using System.Configuration;
using System.Net.Mail;
using System.Resources;
using System.Reflection;
using System.Security.Principal;
namespace PasswordNotification
{
class Program
{
public static string GetAdministratorEmail()
{
return SettingsProvider.Get("AdministratorEmail", "");
}
public static string GetMailServer()
{
return SettingsProvider.Get("MailServer", "");
}
public static string GetCountdown()
{
return SettingsProvider.Get("Countdown", "15");
}
static void Main(string[] args)
{
try
{
GetPasswordExpirationSendEmailNotifications();
}
catch (System.Exception ex)
{
SendEmail("<AdminEmail>", "<AdminEmail>", "Exception: Password Notification", ex.Message, MailPriority.High);
}
}
// This method checks for flags like PasswordDoesNotExpire
private static bool CheckAdsFlag(AdsUserFlags flagToCheck, DirectoryEntry user)
{
AdsUserFlags userFlags = (AdsUserFlags)user.Properties["userAccountControl"].Value;
return userFlags.ToString().Contains(flagToCheck.ToString()); }
// This method gets the password expiration and sends email to
// the users and a report to the Administrator
private static void GetPasswordExpirationSendEmailNotifications()
{
string adminEmail = GetAdministratorEmail();
// Get the DOMAIN
//ActiveDs.ADSystemInfoClass objUserInformation = new ActiveDs.ADSystemInfoClass();
//string sDomain = objUserInformation.DomainDNSName;
string sDomain = Environment.UserDomainName;
// Since maxPwdAge is the Domain policy, it has to be retrieved for the
// domain
TimeSpan maxPwdAge = TimeSpan.MinValue;
int maxPwdAgeDays = 0;
using (DirectoryEntry domain = new DirectoryEntry("LDAP://" + sDomain))
{
DirectorySearcher ds = new DirectorySearcher(
domain,
"(objectClass=*)",
null,
SearchScope.Base
);
SearchResult sr = ds.FindOne();
if (sr.Properties.Contains("maxPwdAge"))
{
maxPwdAge = TimeSpan.FromTicks((long)sr.Properties["maxPwdAge"][0]);
maxPwdAgeDays = maxPwdAge.Days;
}
// maxPwdAge is in negative, make it positive
if (maxPwdAgeDays < 0)
{
maxPwdAgeDays *= -1;
}
}
//Define the filter for your LDAP query
string filter = "(&(objectCategory=person)(objectClass=user))";
DirectorySearcher search = new DirectorySearcher(filter);
StringBuilder sb = new StringBuilder();
sb.Append("<u>User Password Expiration Report</u><br/><br/>");
int count = 0;
bool sendReport = false;
// Read file EmailBody.txt
string filePath = String.Format("{0}\\EmailBody.txt", Directory.GetCurrentDirectory());
string fileContents = TextFileReader(filePath);
foreach (SearchResult result in search.FindAll())
{
// Do work with data returned for each address entry
DirectoryEntry entry = result.GetDirectoryEntry();
// ActiveDs.LargeInteger needs to be used to manipulate pwdLastSet value
LargeInteger liAcctPwdChange = entry.Properties["pwdLastSet"].Value as LargeInteger;
string mailValue = "";
if (entry.Properties["mail"].Value != null)
{
mailValue = entry.Properties["mail"].Value.ToString();
}
// Skip the user if there is no email id
if (string.IsNullOrEmpty(mailValue))
{
continue;
}
// Skip the user if PasswordDoesNotExpire is set
if(CheckAdsFlag(AdsUserFlags.PasswordDoesNotExpire, entry))
{
continue;
}
// Manipulate LargeInteger liAcctPwdChange
long dateAcctPwdChange = (((long)(liAcctPwdChange.HighPart) << 32) + (long)liAcctPwdChange.LowPart);
// Convert FileTime to DateTime and get what today's date is.
DateTime dtNow = DateTime.Now;
// Add maxPwdAgeDays to dtAcctPwdChange
DateTime dtAcctPwdChange = DateTime.FromFileTime(dateAcctPwdChange).AddDays(maxPwdAgeDays);
// Calculate the difference between the date the pasword was changed, and
// what day it is now.
TimeSpan timeRemaining;
timeRemaining = dtAcctPwdChange - dtNow;
// Send email to users if their password is going to expire
int countdown = Convert.ToInt32(GetCountdown());
int daysRemaining = timeRemaining.Days;
if (daysRemaining <= countdown)
{
sendReport = true;
count++;
string displayName = "";
if (entry.Properties["displayname"].Value != null)
{
displayName = entry.Properties["displayname"].Value.ToString();
}
// DisplayName, Email, DaysRemaining
sb.AppendFormat("{0}. {1} {2}: {3} days remaining.{4}", count.ToString(), displayName, mailValue, daysRemaining.ToString(), "<br/>");
string toEmail = mailValue;
string subject = String.Format("Password Expiration in {0} days", daysRemaining.ToString());
string body = fileContents.Replace("[DAYCOUNT]", daysRemaining.ToString());
body = body.Replace("[USERNAME]", displayName);
body = body.Replace("\r\n", "<br />");
Console.Write(String.Format("Sending email to {0}...", toEmail));
SendEmail(adminEmail, toEmail, subject, body, MailPriority.High);
Console.WriteLine("Done.");
}
}
// Only send email report to Administrator if required
if (sendReport)
{
Console.Write(String.Format("Sending email to {0}...", adminEmail));
SendEmail(adminEmail, adminEmail, "User Password Expiration Report", sb.ToString(), MailPriority.Normal);
Console.WriteLine("Done.");
}
}
private static void SendEmail(string fromEmail, string toEmail, string subject, string body, MailPriority priority)
{
string mailServer = GetMailServer();
//create the mail message
MailMessage mail = new MailMessage();
//set the addresses
mail.From = new MailAddress(fromEmail);
mail.To.Add(toEmail);
//set the content
mail.Subject = subject;
mail.Body = body;
mail.IsBodyHtml = true;
//send the message
SmtpClient smtp = new SmtpClient(mailServer);
smtp.Send(mail);
}
private static string TextFileReader(string filePath)
{
string fileContents = "";
// create reader & open file
using (TextReader tr = new StreamReader(filePath))
{
// read a line of text
fileContents = tr.ReadToEnd();
}
return fileContents;
}
[Flags]
internal enum AdsUserFlags
{
Script = 1, // 0x1
AccountDisabled = 2, // 0x2
HomeDirectoryRequired = 8, // 0x8
AccountLockedOut = 16, // 0x10
PasswordNotRequired = 32, // 0x20
PasswordCannotChange = 64, // 0x40
EncryptedTextPasswordAllowed = 128, // 0x80
TempDuplicateAccount = 256, // 0x100
NormalAccount = 512, // 0x200
InterDomainTrustAccount = 2048, // 0x800
WorkstationTrustAccount = 4096, // 0x1000
ServerTrustAccount = 8192, // 0x2000
PasswordDoesNotExpire = 65536, // 0x10000
MnsLogonAccount = 131072, // 0x20000
SmartCardRequired = 262144, // 0x40000
TrustedForDelegation = 524288, // 0x80000
AccountNotDelegated = 1048576, // 0x100000
UseDesKeyOnly = 2097152, // 0x200000
DontRequirePreauth = 4194304, // 0x400000
PasswordExpired = 8388608, // 0x800000
TrustedToAuthenticateForDelegation = 16777216, // 0x1000000
NoAuthDataRequired = 33554432 // 0x2000000
}
}
}