Report Entra App Secrets and Certificates Expiring Soon Using Graph PowerShell

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:

  • automated scripts
  • Azure integrations
  • Teams/SharePoint workflows
  • ITSM connectors
  • third-party enterprise apps

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.

🚀 Community Edition Released!

Try the M365Corner Microsoft 365 Reporting Tool — your DIY pack with 20+ out-of-the-box M365 reports for Users, Groups, and Teams.


i) The Script

$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

                                

ii) How the Script Works

  1. Connects to Microsoft Graph
  2. 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.

  3. Sets an Expiry Threshold
  4. A configurable window is defined:

    $DaysToCheck = 30
    $Threshold = $Now.AddDays($DaysToCheck)
                                    

    Only secrets/certs expiring between today and the next 30 days are reported.

  5. Retrieves All App Registrations
  6. The script pulls all apps including credential blocks: PasswordCredentials (client secrets) and KeyCredentials (certificates).

  7. Detects Expiring Secrets and Certificates
  8. 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.

  9. Exports an Excel-Safe CSV
  10. 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.

  11. Emails the Report
  12. The CSV is Base64-encoded and sent using Send-MgUserMail to all recipients listed in $Recipients.


iii) Further Enhancements

Here are strong extensions you can add:

  • Include Owners of Each App: Fetch app owners and their UPN/email. So stakeholders know who must renew credentials.
  • Add “Already Expired” Section: Include credentials that are already expired to prioritize remediation.
  • Auto-separate Secrets vs Certs: Send two separate CSVs for easier ownership and tracking.
  • Push Alerts to Teams: Send a Teams webhook notification when critical items are found.
  • Schedule Weekly Automation: Run via Task Scheduler / Azure Automation and receive a weekly health summary.

iv) Possible Errors & Solutions

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.


v) Conclusion

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.


Graph PowerShell Explorer Widget

20 Graph PowerShell cmdlets with easily accessible "working" examples.


Permission Required

Example:


                


                


                

© m365corner.com. All Rights Reserved. Design by HTML Codex