Geeks With Blogs
Mayank Sharma
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" ?>
           - <config>
                <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       
        }
    }
}
Posted on Friday, October 30, 2009 8:44 AM C# | Back to top


Comments on this post: Automated password expiration notice for Active Directory users

# re: Automated password expiration notice for Active Directory users
Requesting Gravatar...
Would it be possible to get a copy of the completed application? I'm not savvy enough to build it from the notes :-)
Left by Miklosch Sander on Nov 02, 2009 3:48 PM

# re: Automated password expiration notice for Active Directory users
Requesting Gravatar...
Hi, this is I really need, but when I run this in my pc, I cannt to take yhe value of entry.Properties["pwdLastSet"].Value, entry.Properties["displayname"].Value , entry.Properties["mail"].Value they what I have to do.
Sorry to my english.
Left by Danira on Nov 13, 2009 1:45 PM

# re: Automated password expiration notice for Active Directory users
Requesting Gravatar...
@Danira - What error message do you get?
Left by Max on Nov 13, 2009 2:45 PM

# re: Automated password expiration notice for Active Directory users
Requesting Gravatar...
I used Microsoft Visual C# 2008 Express Edition to put this together and I needed to add the System.DirectoryServices in addition to the ActiveDS Library type. Not sure whether it would be the same with another IDE but might be worth noting in the instructions.

I am not familiar with c# but I want to run this only on one OU, instead of the whole domain, can I manually set the sDomain string to the ldap path to that OU to do this?

Thanks. This will be a great help.
Left by David on Nov 24, 2009 2:37 PM

# re: Automated password expiration notice for Active Directory users
Requesting Gravatar...
Thanks so much for the only working example I could find in 12 hours of googling for how to get a value out of the pwdLastSet property.
Left by Ken Zook on Dec 29, 2009 9:47 AM

# re: Automated password expiration notice for Active Directory users
Requesting Gravatar...
I am using a VB Script in my environements. I have posted the same script on my blog.You may interested in that. I found VBscript is much simpler than other rich laguages like C++ to implement.

http://techontip.wordpress.com/2009/12/27/sending-active-directory-domain-password-expiration-notification-to-end-users/
Left by BRAJESH on Jan 13, 2010 4:14 AM

# re: Automated password expiration notice for Active Directory users
Requesting Gravatar...
Am using LDAP active directory in my console application to fetch mailid.
Application was scheduled on daily basis . In job, previously i have used same domain username as used for LDAP search . Now have changed Another domain username for scheduled job . After got changes LDAP throws error as "Logon failure: unknown user name or bad password" . Is there any pblm in using different domain for scheduled job ,whether ldap will take runing application username?? Could you please suggest me any solution..

thanks in advance
Left by Jeevitha on Dec 29, 2010 5:39 AM

# Automated password expiration notice for Active Directory users
Requesting Gravatar...
ADAudit Plus is a valuable security tool that will help you be compliant with all the IT regulatory acts. With this tool, you can monitor user activity such as logon, file access, etc. A configurable alert system warns you of potential threats.
Left by johnrockfellerz on May 20, 2011 2:48 AM

# re: Automated password expiration notice for Active Directory users
Requesting Gravatar...
Why do you set,

internal enum AdsUserFlags

At the end of the script?
Left by Mark on Oct 31, 2011 4:57 AM

Your comment:
 (will show your gravatar)


Copyright © bullpit | Powered by: GeeksWithBlogs.net