<# ============================================================================================= Name: Send Microsoft Entra App Credentials Expiry Notifications Version: 1.1 Website: o365reports.com Script Highlights: ~~~~~~~~~~~~~~~~~ 1. Sends app credential expiry notifications to specific users. 2. Sends notifications for expiring certificates alone, client secrets alone, or both. 3. Exports a list of apps with expiring credentials within the specified days in CSV format. 4. Allows sending emails on behalf of other users. 5. Automatically install the Microsoft Graph PowerShell module (if not installed already) upon your confirmation. 6. The script can be executed with an MFA-enabled account too. 7. It can be executed with certificate-based authentication (CBA) too. 8. The script is scheduler-friendly. Change Log ~~~~~~~~~~ V1.0 (Apr 29, 2025) - File created V1.1 (Jun 14, 2025) - Minor code improvements. For detailed Script execution: https://o365reports.com/2025/04/29/send-entra-app-credential-expiry-notifications ============================================================================================ #> Param ( [Parameter(Mandatory = $True)] [int]$SoonToExpireInDays, [Parameter(Mandatory = $True)] [string]$Recipients, [string]$FromAddress, [Switch]$ClientSecretsOnly, [Switch]$CertificatesOnly, [Switch]$StoreReportLocally, [string]$TenantId, [string]$ClientId, [string]$CertificateThumbprint ) $CSVFilePath ="$(Get-Location)\AppCertsAndSecretsExpiryNotificationSummary_$((Get-Date -format yyyy-MMM-dd-ddd` hh-mm` tt).ToString()).csv" # Check if Microsoft Graph module is installed $MsGraphModule = Get-Module Microsoft.Graph -ListAvailable if ($MsGraphModule -eq $null) { Write-Host "`nImportant: Microsoft Graph module is unavailable. It is mandatory to have this module installed in the system to run the script successfully." $confirm = Read-Host "Are you sure you want to install Microsoft Graph module? [Y] Yes [N] No" if ($confirm -match "[yY]") { Write-Host "Installing Microsoft Graph module..." Install-Module Microsoft.Graph -Scope CurrentUser -AllowClobber Write-Host "Microsoft Graph module is installed in the machine successfully" -ForegroundColor Magenta } else { Write-Host "Exiting. `nNote: Microsoft Graph module must be available in your system to run the script" -ForegroundColor Red Exit } } Write-Host "`nConnecting to Microsoft Graph..." if (($TenantId -ne "") -and ($ClientId -ne "") -and ($CertificateThumbprint -ne "")) { # Use certificate-based authentication if TenantId, ClientId, and CertificateThumbprint are provided Connect-MgGraph -TenantId $TenantId -AppId $ClientId -CertificateThumbprint $CertificateThumbprint -NoWelcome } else { # Use delegated permissions (Scopes) if credentials are not provided Connect-MgGraph -Scopes "Application.Read.All", "Mail.Send.Shared", "User.Read.All" -NoWelcome } # Verify connection if ((Get-MgContext) -ne $null) { if ((Get-MgContext).Account -ne $null) { $LoggedInAccount = (Get-MgContext).Account if([string]::IsNullOrEmpty($FromAddress)) { $FromAddress = $LoggedInAccount } Write-Host "Connected to Microsoft Graph PowerShell using account: $($LoggedInAccount)" } else { Write-Host "Connected to Microsoft Graph PowerShell using certificate-based authentication." if ([string]::IsNullOrEmpty($FromAddress)) { Write-Host "`nError: FromAddress is required when using certificate-based authentication." -ForegroundColor Red Exit } } } else { Write-Host "Failed to connect to Microsoft Graph." -ForegroundColor Red Exit } # Function to Send Email function SendEmail { $EmailAddresses = ($Recipients -split ",").Trim() $toRecipients = @() foreach ($Email in $EmailAddresses) { $toRecipients += @{ emailAddress = @{ address = $Email } } } $Script:TableContent += "" $TableStyle = "" $MailContent = "$($TableStyle)

Hello Admin,

These application credentials are soon to expire:

$($Script:TableContent)

To prevent authentication failures and service disruptions, please renew the expiring secret or certificate via the App registrations in Microsoft Entra admin center.

If you have any questions, feel free to contact IT support.

Best regards,
IT Admin Team

" $params = @{ message = @{ subject = "Entra App Registration Credentials Expiry Notification" body = @{ contentType = "HTML" content = $MailContent } toRecipients = $toRecipients } } Send-MgUserMail -UserId $FromAddress -BodyParameter $params } $ExportResult = $null $AppCount = 0 $Script:ProcessedCount = 0 $RequiredProperties=@('DisplayName','AppId','Id','KeyCredentials','PasswordCredentials','CreatedDateTime','SigninAudience') if(($CertificatesOnly.IsPresent) -or ($ClientSecretsOnly.IsPresent) -or ($SoonToExpireInDays -ne "")) { $SwitchPresent=$True } else { $SwitchPresent=$false } # Create an HTML table with data $Script:TableContent = "" $Script:TableContent += "" Get-MgApplication -All -Property $RequiredProperties | ForEach-Object { $AppCount++ $AppName=$_.DisplayName Write-Progress -Activity "`n Processed App registration: $AppCount - $AppName" $AppId=$_.Id $Secrets=$_.PasswordCredentials $Certificates=$_.KeyCredentials $AppCreationDate=$_.CreatedDateTime $SigninAudience=$_.SignInAudience $AppOwners=(Get-MgApplicationOwner -ApplicationId $AppId).AdditionalProperties.userPrincipalName $Owners=$AppOwners -join "," if($owners -eq "") { $Owners="-" } #Process through Secret keys if(!($CertificatesOnly.IsPresent) -or ($SwitchPresent -eq $false)) { foreach($Secret in $Secrets) { $CredentialType="Client Secret" $DisplayName=$Secret.DisplayName $Id=$Secret.KeyId $CreatedTime=$Secret.StartDateTime $ExpiryDate=$Secret.EndDateTime $ExpiryStatusCalculation=(New-TimeSpan -Start (Get-Date).Date -End $ExpiryDate).Days $FriendlyExpiryTime="Expires in $ExpiryStatusCalculation days" if (($ExpiryStatusCalculation -gt 0) -and ($ExpiryStatusCalculation -le $SoonToExpireInDays)) { $Script:ProcessedCount++ if ($StoreReportLocally.IsPresent) { $ExportResult = [PSCustomObject]@{'App Name'=$AppName;'App Owners'=$Owners;'App Creation Time'=$AppCreationDate;'Credential Type'=$CredentialType;'Credential Name'=$DisplayName;'Credential Id'=$Id;'Creation Time'=$CreatedTime;'Expiry Date'=$ExpiryDate;'Days to Expiry'=$ExpiryStatusCalculation;'Friendly Expiry Date'=$FriendlyExpiryTime;'App Id'=$AppId} $ExportResult | Export-Csv -Path $CSVFilePath -Notype -Append } $Script:TableContent += "" } } } #Process through Certificates if(!($ClientSecretsOnly.IsPresent) -or ($SwitchPresent -eq $false)){ foreach ($Certificate in $Certificates) { $CredentialType="Certificate" $DisplayName=$Certificate.DisplayName $Id=$Certificate.KeyId $CreatedTime=$Certificate.StartDateTime $ExpiryDate=$Certificate.EndDateTime $ExpiryStatusCalculation=(New-TimeSpan -Start (Get-Date).Date -End $ExpiryDate).Days $FriendlyExpiryTime="Expires in $ExpiryStatusCalculation days" if (($ExpiryStatusCalculation -gt 0) -and ($ExpiryStatusCalculation -le $SoonToExpireInDays)) { $Script:ProcessedCount++ if ($StoreReportLocally.IsPresent) { $ExportResult = [PSCustomObject]@{'App Name'=$AppName;'App Owners'=$Owners;'App Creation Time'=$AppCreationDate;'Credential Type'=$CredentialType;'Credential Name'=$DisplayName;'Credential Id'=$Id;'Creation Time'=$CreatedTime;'Expiry Date'=$ExpiryDate;'Days to Expiry'=$ExpiryStatusCalculation;'Friendly Expiry Date'=$FriendlyExpiryTime;'App Id'=$AppId} $ExportResult | Export-Csv -Path $CSVFilePath -Notype -Append } $Script:TableContent += "" } } } } if ($Script:ProcessedCount -ne 0) { SendEmail Write-Host `n"$Script:ProcessedCount app(s) credential is about to expire in $SoonToExpireInDays days." -ForegroundColor Yellow Write-Host `n"Email has been sent successfully." -ForegroundColor Yellow if ((Test-Path -Path $CSVFilePath) -eq "True") { Write-Host `n"The output file saved in: " -NoNewline -ForegroundColor Yellow Write-Host $CSVFilePath } } else{ Write-Host `n"No app(s) found with secrets or certificates expiring in the next $SoonToExpireInDays days." -ForegroundColor Yellow } # Disconnect from Microsoft Graph Disconnect-MgGraph | Out-Null Write-Host `n~~ Script prepared by AdminDroid Community ~~`n -ForegroundColor Green Write-Host "~~ Check out " -NoNewline -ForegroundColor Green; Write-Host "admindroid.com" -ForegroundColor Yellow -NoNewline; Write-Host " to get access to 1900+ Microsoft 365 reports. ~~" -ForegroundColor Green
App NameApp Creation TimeCredential TypeCredential NameCreation TimeExpiry DateFriendly Expiry Date
$($AppName)$($AppCreationDate)$($CredentialType)$($DisplayName)$($CreatedTime)$($ExpiryDate)$($FriendlyExpiryTime)
$($AppName)$($AppCreationDate)$($CredentialType)$($DisplayName)$($CreatedTime)$($ExpiryDate)$($FriendlyExpiryTime)