posts - 26, comments - 34, trackbacks - 0

My Links

News

Archives

Post Categories

Friday, October 30, 2009

Automated password expiration notice for Active Directory users

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 @ Friday, October 30, 2009 8:44 AM | Feedback (6) | Filed Under [ C# ]

Powered by: