If you have NPS servers in your organisation that are good at handling 802.1x (PEAP) requests or Windows based authentication or certificate based authentication (EAP) then you need a way of keeping your eye on the failures to ensure smooth operating then you need to review the Security log for Event ID 6273 (which is a failure request)
This means would it not be nice to query these events and then have them presented in a easy to read format like a website, well that is the purpose of this article.
When you get these events we need to parse those events, first you need to know how those events will be presented, this Powershell will show you how this data will be presented to the script:
$scriptBlock = {
$event = Get-WinEvent -FilterHashtable @{
LogName = 'Security'
ID = 6273
StartTime = (Get-Date).AddHours(-2)
} -ErrorAction Stop | Select-Object -First 1
Write-Host "Properties Count: $($event.Properties.Count)"
# Inspect each property
for ($i = 0; $i -lt $event.Properties.Count; $i++) {
Write-Host "Property[$i]: $($event.Properties[$i].Value)"
}
}
Invoke-Command -ComputerName "nps-a.bear.local" -ScriptBlock $scriptBlock
That should then look something like this, in this example I have 26 properties:
Properties Count: 26
Property[0]: S-1-5-21-142122122-1548130249-1115540648-15957
Property[1]: nps.user
Property[2]: BEAR
Property[3]: BEAR/nps.user
Property[4]: S-1-0-0
Property[5]: -
Property[6]: -
Property[7]: F8-2B-28-7F-1A-84:BEAR
Property[8]: DA-51-79-44-26-62
Property[9]: 10.80.441.99
Property[10]: -
Property[11]: F8-9E-28-7F-3D-84:vap0
Property[12]: Wireless - IEEE 802.11
Property[13]: 3
Property[14]: Unifi-Controller1
Property[15]: 10.80.441.22
Property[16]: Secure Wireless Connections
Property[17]: Wifi Traffic
Property[18]: Windows
Property[19]: nps-a.bear.local
Property[20]: PEAP
Property[21]: -
Property[22]: 45394531453734433541323942424346
Property[23]: 23
Property[24]: An error occurred during the Network Policy Server use of the Extensible Authentication Protocol (EAP). Check EAP log files for EAP errors.
Property[25]: Accounting information was written to the local log file.
This property ID will be used in the script later on, but we need to get which field maps to which column in the HTML report before we can then script a automated report.
Produce the html report
Note : This report will only look back for the last 2 hours, if you wish to increase this value you can amend this value in the script below:
StartTime = (Get-Date).AddHours(-2)
StartTime = (Get-Date).AddDays(-2)
This will query all the defined NPS servers for valid events and then produce a html report, as who does not love a good graphical report, this will look like this:
Script : NPSFailureQuery.ps1
# Script Variables
$NPSServers = @(
"nps-a.bear.local",
"nps.-b.bear.local"
)
$scriptPath = Split-Path -Parent $MyInvocation.MyCommand.Path
$ReportName = "nps-report.html"
$currentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent()
Write-Host "Script started at: $(Get-Date)" -ForegroundColor Green
Write-Host "Current user: $($currentUser.Name)" -ForegroundColor Green
Write-Host "Querying servers: $($NPSServers -join ', ')" -ForegroundColor Green
Write-Host "Output will be saved to: $scriptPath\$ReportName" -ForegroundColor Green
Write-Host "`nGathering data..." -ForegroundColor Yellow
$style = @"
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html { font-size: 62.5%; }
body {
font-family: Arial, sans-serif;
font-size: 1.4rem;
line-height: 1.6;
color: #333333;
background-color: #f5f5f5;
padding: 2rem;
}
.container {
max-width: 98%;
margin: 0 auto;
background: #ffffff;
padding: 2rem;
border-radius: 0.4rem;
box-shadow: 0 0.1rem 0.3rem rgba(0, 0, 0, 0.1);
}
.header { margin-bottom: 2rem; padding-bottom: 1rem; border-bottom: 0.1rem solid #eee; }
h1 { font-size: 2.4rem; color: #2c3e50; margin-bottom: 1rem; }
.timestamp { color: #7f8c8d; font-size: 1.2rem; }
/* Health Cards */
.health-cards {
display: flex;
gap: 2rem;
margin-bottom: 2rem;
}
.card {
flex: 1;
padding: 1.5rem;
border-radius: 0.4rem;
background: #ffffff;
box-shadow: 0 0.2rem 0.4rem rgba(0, 0, 0, 0.1);
}
.card-title { font-size: 1.4rem; color: #7f8c8d; margin-bottom: 0.5rem; }
.card-value { font-size: 2.4rem; font-weight: bold; color: #2c3e50; }
.card-green { border-left: 4px solid #2ecc71; }
.card-yellow { border-left: 4px solid #f1c40f; }
.card-red { border-left: 4px solid #e74c3c; }
/* Table Styles */
.table-container { width: 100%; }
table {
width: 100%;
table-layout: fixed;
border-collapse: collapse;
font-size: 1.2rem;
}
th, td {
padding: 1rem;
text-align: left;
border-bottom: 0.1rem solid #eee;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
th {
background-color: #f8f9fa;
color: #2c3e50;
font-weight: bold;
}
td:hover {
white-space: normal;
overflow: visible;
}
tr:hover { background-color: #f9f9f9; }
.failure-code { color: #e74c3c; }
/* Column Widths */
th:nth-child(1), td:nth-child(1) { width: 12%; } /* Time */
th:nth-child(2), td:nth-child(2) { width: 10%; } /* Server */
th:nth-child(3), td:nth-child(3) { width: 15%; } /* Device/User */
th:nth-child(4), td:nth-child(4) { width: 8%; } /* Failure Code */
th:nth-child(5), td:nth-child(5) { width: 25%; } /* Failure Reason */
th:nth-child(6), td:nth-child(6) { width: 12%; } /* CRP Policy */
th:nth-child(7), td:nth-child(7) { width: 10%; } /* Network Policy */
th:nth-child(8), td:nth-child(8) { width: 8%; } /* Auth Type */
</style>
"@
$results = @()
$totalEventsProcessed = 0
foreach ($server in $NPSServers) {
try {
Write-Host "`nConnecting to server: $server" -ForegroundColor Cyan
$scriptBlock = {
param($ServerName)
Write-Output "Querying events on $ServerName..."
Get-WinEvent -FilterHashtable @{
LogName = 'Security'
ID = 6273
StartTime = (Get-Date).AddHours(-2)
} -ErrorAction Stop | ForEach-Object {
$event = $_
[PSCustomObject]@{
TimeCreated = $event.TimeCreated
DeviceUser = $event.Properties[3].Value # Full Account Name
FailureCode = $event.Properties[23].Value # Error Code
FailureReason = "$($event.Properties[23].Value) - $($event.Properties[24].Value)" # Error Code + Reason
CRPPolicy = $event.Properties[16].Value # Connection Request Policy
NetworkPolicy = $event.Properties[17].Value # Network Policy
AuthType = $event.Properties[20].Value # Auth Type (PEAP)
}
}
}
$events = Invoke-Command -ComputerName $server -ScriptBlock $scriptBlock
$totalEventsProcessed += $events.Count
Write-Host "Successfully retrieved $($events.Count) events from $server" -ForegroundColor Green
foreach ($event in $events) {
$data = @{
Server = $server
TimeCreated = $event.TimeCreated
DeviceUser = $event.DeviceUser
FailureCode = $event.FailureCode
FailureReason = $event.FailureReason
CRPPolicy = $event.CRPPolicy
NetworkPolicy = $event.NetworkPolicy
AuthType = $event.AuthType
}
$results += [PSCustomObject]$data
}
}
catch {
Write-Host "Error querying server $server" -ForegroundColor Red
Write-Host $_.Exception.Message -ForegroundColor Red
}
}
Write-Host "`nGenerating report..." -ForegroundColor Yellow
$totalFailures = $results.Count
$last24Hours = $results | Where-Object { $_.TimeCreated -gt (Get-Date).AddHours(-24) }
$last24HoursCount = $last24Hours.Count
$uniqueUsers = ($results | Select-Object -ExpandProperty DeviceUser -Unique).Count
Write-Host "Total failures processed: $totalFailures" -ForegroundColor Cyan
Write-Host "Failures in last 24 hours: $last24HoursCount" -ForegroundColor Cyan
Write-Host "Unique users affected: $uniqueUsers" -ForegroundColor Cyan
$tableRows = $results | Sort-Object TimeCreated -Descending | ForEach-Object {
@"
<tr>
<td>$($_.TimeCreated)</td>
<td>$($_.Server)</td>
<td>$($_.DeviceUser)</td>
<td class="failure-code">$($_.FailureCode)</td>
<td>$($_.FailureReason)</td>
<td>$($_.CRPPolicy)</td>
<td>$($_.NetworkPolicy)</td>
<td>$($_.AuthType)</td>
</tr>
"@
}
$html = @"
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>NPS Authentication Failures Report</title>
$style
</head>
<body>
<div class="container">
<div class="header">
<h1>NPS Authentication Failures Report</h1>
<div class="timestamp">Report generated: $(Get-Date -Format "yyyy-MM-dd HH:mm:ss")</div>
<div class="timestamp">Generated by: $($currentUser.Name)</div>
</div>
<div class="health-cards">
<div class="card card-red">
<div class="card-title">Total Failures</div>
<div class="card-value">$totalFailures</div>
</div>
<div class="card card-yellow">
<div class="card-title">Last 2 Hours</div>
<div class="card-value">$totalFailures</div>
</div>
<div class="card card-green">
<div class="card-title">Unique Users</div>
<div class="card-value">$uniqueUsers</div>
</div>
</div>
<div class="table-container">
<table>
<thead>
<tr>
<th>Time</th>
<th>Server</th>
<th>Device/User</th>
<th>Failure Code</th>
<th>Failure Reason</th>
<th>CRP Policy</th>
<th>Network Policy</th>
<th>Auth Type</th>
</tr>
</thead>
<tbody>
$tableRows
</tbody>
</table>
</div>
</div>
</body>
</html>
"@
$ReportPath = Join-Path $scriptPath $ReportName
$html | Out-File $ReportPath -Encoding UTF8
Write-Host "`nReport generated successfully at: $ReportPath" -ForegroundColor Green
Write-Host "Script completed at: $(Get-Date)" -ForegroundColor Green
When run this will produce the report called nps-report.html which you can see highlighted below:
However that drew my attention to these alerts, here we can see alerts saying that "user must change their password" this is an action point for the user and that user is trying to join corporate services with an expired password:
User Password change report
Note : This script will look for the last six hours, amend as required with the variable in bold as below in the script.
Script : NPSPasswordChange.ps1
"nps-a.bear.local",
"nps.-b.bear.local"
)
Extract UPN for further remediation
If you wish to engage the users in this situation then first you need to extract the UPN from the samAccountName, when you have this you can send them an e-mail to start with, so first lets get the UPN.
This file will contain the UPN of the user affected, now we can move on to sending them an email (remember that email on Outlook for mobile devices will work as this uses a token) you can also send them a notification via InTune if that indeed is your MDM solution.
- Looks in the upn_report.txt for the users UPN
- Retrieves the managed device(s) from Intune
- Prepares the Notification message
- Send Notification to the selected devices
Script : intune-Notification.ps1