I often find myself needing quick answers to common questions. One that comes up frequently is "when does this user's password expire?"
While Active Directory stores this information, it's not immediately obvious how to retrieve it. After developing a useful PowerShell one-liner for this task, I decided to take it a step further and build a web application that would be internally available to display this information.
The Starting Point: A Powerful One-Liner
It all began with this PowerShell command that I use regularly:
(Get-ADUser -Identity username -Properties msDS-UserPasswordExpiryTimeComputed)."msDS-UserPasswordExpiryTimeComputed" | ForEach-Object {[datetime]::FromFileTime($_)}
This command does something quite clever, Active Directory doesn't store password expiry as a simple date field. Instead, it calculates it dynamically based on the user's password policy and last password change.
The msDS-UserPasswordExpiryTimeComputed attribute gives us this calculated value, but it's stored as a Windows file time (the number of 100-nanosecond intervals since January 1, 1601).
The [datetime]::FromFileTime($_) portion converts this cryptic number into a readable date and time that humans can actually understand.
The Challenge: Making It Accessible
While this one-liner works perfectly, I realized it had some limitations:
- Not everyone on my team is comfortable with PowerShell
- It requires remembering the exact syntax
- You need to have the Active Directory module loaded
- Results aren't easily shareable or documented
I decided to create a web application that would make this functionality accessible to anyone on our network.
Visual Results
This is the simple to use main interface:
Building the Web Interface
I chose ASP.NET Web Forms for its simplicity and the interface needed to be clean and professional, so I focused on:
<div class="form-group">
<label for="txtUsername">Username (sAMAccountName)</label>
<asp:TextBox ID="txtUsername" runat="server" CssClass="form-control"
placeholder="Enter username (e.g., bear.user)" MaxLength="50"></asp:TextBox>
</div>
Simple input, clear labeling, and helpful placeholder text. No complexity, just function.
The Technical Challenge: Executing PowerShell from ASP.NET
This is where things got interesting. I initially tried using the System.Management.Automation namespace to run PowerShell directly within the web application, but ran into assembly reference issues on our server.
My solution was to use Process.Start to execute PowerShell as an external process:
private DateTime GetPasswordExpiryDate(string username)
{
try
{
string psCommand = "(Get-ADUser -Identity '" + username +
"' -Properties msDS-UserPasswordExpiryTimeComputed).'msDS-UserPasswordExpiryTimeComputed' | " +
"ForEach-Object {[datetime]::FromFileTime($_)}";
ProcessStartInfo startInfo = new ProcessStartInfo();
startInfo.FileName = "powershell.exe";
startInfo.Arguments = "-Command \"" + psCommand + "\"";
startInfo.RedirectStandardOutput = true;
startInfo.RedirectStandardError = true;
startInfo.UseShellExecute = false;
startInfo.CreateNoWindow = true;
using (Process process = Process.Start(startInfo))
{
process.WaitForExit(30000); // 30 second timeout
if (process.ExitCode == 0)
{
string output = process.StandardOutput.ReadToEnd().Trim();
DateTime result;
if (DateTime.TryParse(output, out result))
{
return result;
}
}
}
}
catch (Exception)
{
return DateTime.MinValue;
}
return DateTime.MinValue;
}
This approach executes the exact same PowerShell command I use manually, but captures the output programmatically. I added error handling, timeouts, and proper resource disposal to make it production-ready.
Security Considerations
Input validation was crucial. I couldn't just pass user input directly to PowerShell without sanitization:
private bool IsValidUsername(string username)
{
if (string.IsNullOrEmpty(username))
return false;
// Allow only safe characters
foreach (char c in username)
{
if (!char.IsLetterOrDigit(c) && c != '-' && c != '_' &&
c != '.' && c != '\\' && c != '@')
{
return false;
}
}
return true;
}
This prevents PowerShell injection attacks while still allowing legitimate username formats including domain\user syntax.
Making the Results User-Friendly
The raw output from PowerShell is just a date, but I wanted to provide more context:
TimeSpan timeUntilExpiry = expiryDate - DateTime.Now;
if (timeUntilExpiry.TotalDays < 0)
{
resultClass = "error";
title = "Password Expired";
message.AppendLine("<strong>Status:</strong> Expired " +
Math.Abs(timeUntilExpiry.Days).ToString() + " day(s) ago");
}
else if (timeUntilExpiry.TotalDays <= 7)
{
resultClass = "warning";
title = "Password Expiring Soon";
message.AppendLine("<strong>Days Remaining:</strong> " +
Math.Ceiling(timeUntilExpiry.TotalDays).ToString() + " day(s)");
}
The application now provides color-coded results: red for expired passwords, yellow for passwords expiring within a week, and green for passwords with plenty of time remaining.
Adding Audit Logging
One requirement that became important was tracking who was searching for what usernames. I needed to implement logging that would capture search requests without impacting performance. This turned out to be more involved than expected due to file system permissions.
The logging function captures essential audit information:
private void LogSearchRequest(string searchedUsername)
{
try
{
// Get the requesting user
string requestingUser = "Anonymous";
if (Request.IsAuthenticated && !string.IsNullOrEmpty(User.Identity.Name))
{
requestingUser = User.Identity.Name;
}
else if (!string.IsNullOrEmpty(Request.ServerVariables["LOGON_USER"]))
{
requestingUser = Request.ServerVariables["LOGON_USER"];
}
// Create log entry
string timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
string clientIP = Request.ServerVariables["REMOTE_ADDR"] ?? "Unknown";
string logEntry = timestamp + " | " +
requestingUser + " | " +
searchedUsername + " | " +
clientIP + Environment.NewLine;
// Write to log file
string logFilePath = Path.Combine(Server.MapPath("~/App_Data"), "PasswordSearchLog.txt");
File.AppendAllText(logFilePath, logEntry);
}
catch (Exception)
{
The App_Data Folder Setup
The default application pool identity doesn't have permissions to create folders or write files in the web directory. I needed to properly set up the App_Data folder with correct permissions.
The App_Data folder serves two important purposes:
- Security: IIS automatically blocks web access to any folder named App_Data
- Convention: It's the standard location for application data in ASP.NET applications
Setting up the folder and permissions required PowerShell commands run as administrator:
# Navigate to the application directory
cd "C:\inetpub\wwwroot\PasswordExpiryLookup"
# Create the App_Data folder
mkdir App_Data -Force
# Grant IIS_IUSRS modify permissions
icacls "App_Data" /grant "IIS_IUSRS:(M)"
# Verify permissions were set correctly
icacls "App_Data"# Create the log file manually
New-Item -Path "App_Data\PasswordSearchLog.txt" -ItemType File -Force
icacls "App_Data\PasswordSearchLog.txt" /grant "IIS_IUSRS:(F)"The IIS_IUSRS group is specifically designed for web applications running under application pool identities. This gives the application the minimum necessary permissions to write log files while maintaining security.
Deployment Lessons Learned
- Remeber to convert the virtual direction to an application is IIS - using the DefatulAppPool with the Intergrated model - this does not require any elevated permissions.
- Assembly References: Not all .NET assemblies are available by default in web applications
- Trust Levels: PowerShell execution requires Full trust, which needed to be configured in web.config
- Application vs. Virtual Directory: The application had to be properly configured in IIS, not just dropped into wwwroot
- jQuery Validation: Modern ASP.NET expects jQuery for validation, so I disabled unobtrusive validation mode
My final web.config included these key settings:
<appSettings>
<add key="vs:UnobtrusiveValidationMode" value="None" />
</appSettings>