Disclaimer: I do not accept responsibility for any issues arising from scripts being run without adequate understanding. It is the user's responsibility to review and assess any code before execution. More information

PowerShell : One-Liner to Web Application: Building an AD Password Expiry Checker

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:


When you enter a valid username you will then after a short wait, be returned the password expiry date and time:


The target for this website was a user called "wifi.surfer" a made up testing account, this account should report as having a short password length as you can see below we get 24 hours with this account and the password:







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:

  1. Security: IIS automatically blocks web access to any folder named App_Data
  2. 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

  1. 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.
  2. Assembly References: Not all .NET assemblies are available by default in web applications
  3. Trust Levels: PowerShell execution requires Full trust, which needed to be configured in web.config
  4. Application vs. Virtual Directory: The application had to be properly configured in IIS, not just dropped into wwwroot
  5. 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>
Previous Post Next Post

نموذج الاتصال