posts - 25, comments - 24, 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 (4) | Filed Under [ C# ]

Tuesday, September 22, 2009

SQL Server: DateTime and Indexes

A nice post about handling DateTime fields and indexing.

http://itknowledgeexchange.techtarget.com/sql-server/dates-can-easily-be-the-hardest-datatype-to-work-with/

posted @ Tuesday, September 22, 2009 12:52 PM | Feedback (0) |

Tips n Tricks - Visual Studio

1. Disable HTML Navigation bar.

Tools -> Options -> Text Editor -> HTML -> Unckeck Navigation Bar option

2. Auto insert quotes when typing.

Tools -> Options -> Text Editor -> HTML -> Format -> Check "Insert attribute value quotes when typing"

3. Format HTML on paste

Tools -> Options -> Text Editor -> HTML -> Miscellaneous -> Format HTML on Paste

posted @ Tuesday, September 22, 2009 7:37 AM | Feedback (0) |

Auto-Start in .NET 4.0

Here Scott talks about the new feature Auto-Start included in .NET 4.0 and IIS 7. This can prove really helpful in avoiding the initial delay experienced by the first visitor of a web ASP.NET application.

Here's the post.

posted @ Tuesday, September 22, 2009 7:25 AM | Feedback (0) |

Wednesday, August 19, 2009

Windows Authentication using Forms Authentication

Here's a good article for doing Windows Authentication using Forms Authentication.

Windows Authentication using Form Authentication

posted @ Wednesday, August 19, 2009 3:58 PM | Feedback (0) |

SQL Azure Database Preview released

Microsoft has released a trial version of SQL Azure DB. Get more details here

posted @ Wednesday, August 19, 2009 3:54 PM | Feedback (0) |

Friday, August 14, 2009

SQL Server: Disaster Recovery...some tips

Again a nice arctile by Don Jones, where he talks about different SQL Server backup options and disaster recovery plans...a good read.

SQL Server Disaster Recovery by Don Jones

posted @ Friday, August 14, 2009 10:35 AM | Feedback (0) |

SQL Server: Optimizing Indexes

A good article about types of SQL Server Indexes and how to optimize them when they go bad...

Optimizing SQL Server Indexes by Don Jones

posted @ Friday, August 14, 2009 10:26 AM | Feedback (0) |

Thursday, July 16, 2009

Remove Projects in Visual Studio recent project list

As such, you cannot remove the projects that are listed in the Visual Studio recent projects list on the Start page. One option is the find the projects which you want to remove in the location specified by the shortcuts, then either delete them or rename them and come back to VS and click the project name, you will get a message box asking you whether to remove the entry from the list.

Another option is to edit the registry entry (Warning! All registry edits should be done at your own risk).

I found a nice blog post explaing all of this, I am just pasting what I may need in future here:

Visual Studio Recent Projects
Visual Studio .Net keeps it's list of projects in the registry. Depending on which version depends on which location the list is stored.
·         Visual Studio 2002 - 7.0
·         Visual Studio 2003 - 7.1
·         Visual Studio 2005 - 8.0
·         Visual Studio 2008 - 9.0
[HKEY_CURRENT_USER\Software\Microsoft\VisualStudio\7.0\ProjectMRUList]
[HKEY_CURRENT_USER\Software\Microsoft\VisualStudio\7.1\ProjectMRUList]
[HKEY_CURRENT_USER\Software\Microsoft\VisualStudio\8.0\ProjectMRUList]
[HKEY_CURRENT_USER\Software\Microsoft\VisualStudio\9.0\ProjectMRUList]
 

 

 

posted @ Thursday, July 16, 2009 10:30 AM | Feedback (0) |

Open / Create Website menu item missing from Visual Studio 2008 Start Page

I recently installed a third party control and it had an option to add the control to my Visual Studio toolbox. When it installed the control to my toolbox, I noticed that the "Open / Create Website" option on the Start Page was missing. I tried everything from repairing the install to uninstalling and installing VS 2008 all over again. Nothing worked, but then a saw a comment on resetting the VS settings.

For some, it may not be an option but I had only a few custom settings which I could change again.

This is what I did and it worked:

1. Start VS IDE.
2. Tools -> Import and Export Settings
3. Select "Reset all settings", click Next
4. If you want to save the settings, go ahead, I did not save them. Then click Next
5. Select "Web Development Settings" and click Finish.

You should now get the Website option on your start page.

posted @ Thursday, July 16, 2009 10:21 AM | Feedback (0) |

Powered by: