Check secret expiration for Azure AD Applications using Powershell
When you assign a client ID/secret to an Application Registration in Azure AD, the expiration for that secret is one of the input settings. It could expire in one or two years, or never.
Bad news is that Microsoft does not provide any support out of the box to notify sys admins when a secret is near to expire, so you have to build that feature for yourself unless no expiration is selected, which is never recommended.
You can still use the Graph API to query the applications, and check the expiration date for each secret. The following script does exactly that with Powershell.
param(
[string]$TenantId,
[string]$Thumbprint,
[string]$AppId,
[bool]$RunAsJob = $false,
[int]$Days = 30,
[string]$SmtpUser,
[string]$SmtpPassword
)
$ErrorActionPreference = "Stop"
$today = (Get-Date).ToUniversalTime()
$limitDate = $today.AddDays($Days)
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
function GenerateJWT (){
$cert = Get-Item Cert:\LocalMachine\My\$Thumbprint
$hash = $cert.GetCertHash()
$hashValue = [System.Convert]::ToBase64String($hash) -replace '\+','-' -replace '/','_' -replace '='
$exp = ([DateTimeOffset](Get-Date).AddHours(1).ToUniversalTime()).ToUnixTimeSeconds()
$nbf = ([DateTimeOffset](Get-Date).ToUniversalTime()).ToUnixTimeSeconds()
$jti = New-Guid
[hashtable]$header = @{alg = "RS256"; typ = "JWT"; x5t=$hashValue}
[hashtable]$payload = @{aud = "https://login.microsoftonline.com/$TenantId/oauth2/token"; iss = $AppId; sub=$AppId; jti = $jti; exp = $Exp; Nbf= $Nbf}
$headerjson = $header | ConvertTo-Json -Compress
$payloadjson = $payload | ConvertTo-Json -Compress
$headerjsonbase64 = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($headerjson)).Split('=')[0].Replace('+', '-').Replace('/', '_')
$payloadjsonbase64 = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($payloadjson)).Split('=')[0].Replace('+', '-').Replace('/', '_')
$toSign = [System.Text.Encoding]::UTF8.GetBytes($headerjsonbase64 + "." + $payloadjsonbase64)
$rsa = $cert.PrivateKey -as [System.Security.Cryptography.RSACryptoServiceProvider]
$signature = [Convert]::ToBase64String($rsa.SignData($toSign,[Security.Cryptography.HashAlgorithmName]::SHA256,[Security.Cryptography.RSASignaturePadding]::Pkcs1)) -replace '\+','-' -replace '/','_' -replace '='
$token = "$headerjsonbase64.$payloadjsonbase64.$signature"
return $token
}
if($RunAsJob)
{
$RequestToken = GenerateJWT
$AccessTokenResponse = Invoke-WebRequest -Method POST -ContentType "application/x-www-form-urlencoded" -Headers @{"accept"="application/json"} -Body "scope=https://graph.microsoft.com/.default&client_id=$AppId&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer&client_assertion=$RequestToken&grant_type=client_credentials" -Uri "https://login.microsoftonline.com/$TenantId/oauth2/v2.0/token"
$AccessTokenJsonResponse = ConvertFrom-Json $AccessTokenResponse.Content
$AccessToken = $AccessTokenJsonResponse.access_token
}
else {
if (!(Get-Module "MSAL.PS")) {
Import-Module "MSAL.PS"
}
$TokenResponse = Get-MsalToken -ClientId $AppId `
-TenantId $TenantId `
-Interactive `
-Scopes 'https://graph.microsoft.com/Application.Read.All'
$AccessToken = $TokenResponse.AccessToken
}
$GraphApiUrl="https://graph.microsoft.com/v1.0/applications";
$headers = @{}
$headers.Add("Accept","application/json")
$headers.Add("content-type","application/json")
$headers.Add("Authorization","bearer $AccessToken")
$GraphApiResponse=Invoke-RestMethod -Uri $GraphApiUrl -Headers $headers -Method GET
$Apps = @()
$Apps+=$GraphApiResponse.value
while($GraphApiResponse.'@odata.nextLink' -ne $null) {
$GraphApiResponse = Invoke-RestMethod -Uri $GraphApiResponse.'@odata.nextLink' -Headers $headers -Method Get
$Apps+=$GraphApiResponse.value
}
$ExpiredApps = @()
foreach($App in $Apps)
{
if($App.passwordCredentials -ne $null)
{
foreach($Credential in $App.passwordCredentials)
{
if($Credential.endDateTime) {
$credentialEndDate = [datetime]::Parse($Credential.endDateTime)
if($credentialEndDate -le $limitDate -OR $credentialEndDate -lt $today)
{
$ExpiredApps += @{
DisplayName = $App.DisplayName
Id = $App.Id
AppId = $App.AppId
Expiration = ($Credential.endDateTime)
KeyId = $Credential.KeyId
}
}
}
}
}
}
if($ExpiredApps.Count -EQ 0) {
Write-Output "No Apps found"
}
else {
Write-Output "Apps that will expire soon"
Write-Output $ExpiredApps.Count
Write-Output $ExpiredApps
}
You can use that script in any scheduled job and be notified via email about all the client secrets that are near to expire.