App registrations are everywhere in Microsoft 365 environments — powering automation, integrations, third-party tools, and internal workloads. These apps often rely on client secrets or certificates for authentication.
When these credentials expire unexpectedly, it can break:
To prevent outages, administrators should proactively monitor expiring app credentials. This Graph PowerShell script scans all app registrations for secrets/certs expiring within a chosen window (default 30 days), exports a report, and automatically emails it to administrators and stakeholders.
Try the M365Corner Microsoft 365 Reporting Tool — your DIY pack with 20+ out-of-the-box M365 reports for Users, Groups, and Teams.
$SenderUPN = "admin@yourtenant.onmicrosoft.com"
$Recipients = @(
"admin@yourtenant.onmicrosoft.com",
"securityteam@yourtenant.onmicrosoft.com"
)
$DaysToCheck = 30
$Now = Get-Date
$Threshold = $Now.AddDays($DaysToCheck)
Connect-MgGraph -Scopes "Application.Read.All","Directory.Read.All","Mail.Send"
$Apps = Get-MgApplication -All -Property Id,AppId,DisplayName,PasswordCredentials,KeyCredentials
$ExpiringItems = @()
foreach ($app in $Apps) {
foreach ($secret in ($app.PasswordCredentials | Where-Object { $_.EndDateTime })) {
$end = [datetime]$secret.EndDateTime
if ($end -le $Threshold -and $end -ge $Now) {
$ExpiringItems += [PSCustomObject]@{
"App Name" = $app.DisplayName
"AppId" = $app.AppId
"Credential Type" = "Client Secret"
"Credential Name" = $secret.DisplayName
"Start Date" = $secret.StartDateTime
"End Date" = $secret.EndDateTime
"Days Remaining" = [int]([timespan]($end - $Now)).TotalDays
}
}
}
foreach ($cert in ($app.KeyCredentials | Where-Object { $_.EndDateTime })) {
$end = [datetime]$cert.EndDateTime
if ($end -le $Threshold -and $end -ge $Now) {
$ExpiringItems += [PSCustomObject]@{
"App Name" = $app.DisplayName
"AppId" = $app.AppId
"Credential Type" = "Certificate"
"Credential Name" = $cert.DisplayName
"Start Date" = $cert.StartDateTime
"End Date" = $cert.EndDateTime
"Days Remaining" = [int]([timespan]($end - $Now)).TotalDays
}
}
}
}
$ReportPath = "$env:TEMP\AppCredentials_ExpiringSoon.csv"
if ($ExpiringItems.Count -gt 0) {
$ExpiringItems |
Sort-Object "Days Remaining" |
Export-Csv -Path $ReportPath -NoTypeInformation -Encoding utf8
} else {
"No secrets or certificates are expiring in the next $DaysToCheck days." |
Set-Content -Path $ReportPath -Encoding utf8
}
$Bytes = [System.IO.File]::ReadAllBytes($ReportPath)
$Utf8Bom = New-Object System.Text.UTF8Encoding($true)
[System.IO.File]::WriteAllText($ReportPath, [System.Text.Encoding]::UTF8.GetString($Bytes), $Utf8Bom)
$Count = $ExpiringItems.Count
$Subject = "App Secrets/Certs Expiring in Next $DaysToCheck Days — $(Get-Date -Format 'yyyy-MM-dd')"
$Body = @"
Hello Team,<br><br>
Attached is the <b>App Credentials Expiration Report</b> for the next $DaysToCheck days.<br>
This includes expiring <b>client secrets</b> and <b>certificates</b> from Entra app registrations.<br><br>
Total expiring credentials found: <b>$Count</b><br><br>
Regards,<br>
Graph PowerShell Automation
"@
$AttachmentContent = [System.Convert]::ToBase64String([System.IO.File]::ReadAllBytes($ReportPath))
$Attachments = @(
@{
"@odata.type" = "#microsoft.graph.fileAttachment"
Name = "AppCredentials_ExpiringSoon.csv"
ContentBytes = $AttachmentContent
}
)
$ToRecipients = $Recipients | ForEach-Object {
@{ EmailAddress = @{ Address = $_ } }
}
$Message = @{
Message = @{
Subject = $Subject
Body = @{
ContentType = "HTML"
Content = $Body
}
ToRecipients = $ToRecipients
Attachments = $Attachments
}
SaveToSentItems = "true"
}
Send-MgUserMail -UserId $SenderUPN -BodyParameter $Message
Write-Host "App credentials expiring report emailed successfully." -ForegroundColor Green
The script signs in using delegated scopes: Application.Read.All (retrieves app registrations) , Directory.Read.All (retrieves directory objects) and Mail.Send (emails the report).
These permissions must have admin consent.
A configurable window is defined:
$DaysToCheck = 30
$Threshold = $Now.AddDays($DaysToCheck)
Only secrets/certs expiring between today and the next 30 days are reported.
The script pulls all apps including credential blocks: PasswordCredentials (client secrets) and KeyCredentials (certificates).
Each credential is checked: If EndDateTime <= threshold And EndDateTime >= now then it’s added to the report with: app name, AppId, credential type & name, start/end date, days remaining.
To avoid “corrupt CSV” issues in Excel: i) Export is done in UTF8, ii) Then rewritten with UTF8 BOM and, iii) If no records exist, a readable “no expiring items” line is still written. This guarantees a clean and readable attachment every time.
The CSV is Base64-encoded and sent using Send-MgUserMail to all recipients listed in $Recipients.
Here are strong extensions you can add:
| Error | Cause | Solution |
|---|---|---|
| Authorization_RequestDenied | Application/Directory permissions not granted. | Ensure these scopes are approved: Application.Read.All, Directory.Read.All and Mail.Send. |
| Script returns no rows | No credentials expiring within the window. | Valid result. Increase $DaysToCheck to verify wider coverage. |
| Email not delivered | Sender account lacks mailbox or Mail.Send scope. | Use a mailbox-enabled sender UPN. Reconnect with Mail.Send Scope. |
| CSV opens blank or garbled | Excel UTF8 parsing issues. | Already fixed by BOM export in the script. |
Expired app secrets or certificates are one of the most common causes of sudden automation and integration failures in Microsoft 365 tenants. This script provides a proactive way to detect expiring credentials before they cause outages.
By automatically generating and emailing a clean, Excel-friendly report, administrators can quickly renew credentials, notify app owners, and maintain stable service operations across the tenant.
© m365corner.com. All Rights Reserved. Design by HTML Codex