Skip to main content

Notify client app secret expiration with automation

Requirements: Create an App Registration with Application.Read.All permissions and a client secret.

With this automation all client secrets and their expiration dates are evaluated via Graph API. A message is posted in a team channel when a client secret is about to expire.

Use case

This automation helps large organizations with many Microsoft Entra ID App Registrations to keep track of expiration dates of their Client Secrets. If this automation is carried out regularly, interruptions in operation and expired client secrets can be noticed at an early stage. To run such scripts on a regular basis, an Azure Runbook is a good choice.

Teams webhook notification

To send a notification to a Teams Channel you can use a webhook. To create a webhook for a channel you can rightclick on a channel and then choose "Connectors".

image.png

There you can then configure a new incoming webhook:

image.png

You have to name the webhook first. This name will be displayed as the sender of the message. You can upload an image which will then be used as a profile picture of the sender. After that you can click "create". This will return an URL which then can be used to send a payload message.

PowerShell script

This PowerShell script needs an App Registration and the according Application.Read.All Graph API permission. This script uses the three Azure Automation variables "$tenantid", "$clientid" and "$clientsecret" as well as the perviously created "$WebhookURI", which should be added before.

$tenantId=Get-AutomationVariable -Name "<nameoftenantidrunbookvariable>"
$ClientId=Get-AutomationVariable -Name "<nameofclientidrunbookvariable>"
$CredentialObject=Get-AutomationPSCredential -Name '<nameofclientsecretrunbookcredentials>'
$ClientSecret = $CredentialObject.GetNetworkcredential().password

$WebhookURI = "<yourwebhookuri>"

$NotifyOnDays = @(1,2,3,4,5,10,15,20,30)
$DeleteSecretIfExpiredForDays = 30

$Body = @{
"tenant" = $TenantId
"client_id" = $ClientId
"scope" = "https://graph.microsoft.com/.default"
"client_secret" = $ClientSecret
"grant_type" = "client_credentials"
}

$Params = @{
"Uri" = "https://login.microsoftonline.com/$TenantId/oauth2/v2.0/token"
"Method" = "Post"
"Body" = $Body
"ContentType" = "application/x-www-form-urlencoded"
}

$AuthResponse = Invoke-RestMethod @Params

$Header = @{
    "Authorization" = "Bearer $($AuthResponse.access_token)"
}

function Send-ToTeams {
    Param(
        $SecretOB
    )

    $Body = @{
"@context" = "https://schema.org/extensions"
"@type" = "MessageCard"
"themeColor" = "880808"
"title" = "Secret of App Registration $($SecretOB.AppName) expires in $($SecretOB.ExpiresInXDays)!"
    "text" = "<ul><li>App: $($SecretOB.AppName)</li><li>AppId: $($SecretOB.AppID)</li><li>SecretId: $($SecretOB.SecretID)</li><li>Expirationdate: $($SecretOB.ExpiryDate) (in $($SecretOB.ExpiresInXDays) days)</li></ul>"
}

    $JsonBody = $Body | ConvertTo-JSON

    Invoke-RestMethod -Method Post -Body $JsonBody -Uri $WebhookURI -Headers @{"content-type" = "application/json; charset=UTF-8"} | out-null
}


$AllApps = @()

$Response = Invoke-RestMethod -Method Get -Uri "https://graph.microsoft.com/v1.0/applications" -Headers $Header
$AllApps = $Response.value
 
while ($Response.'@odata.nextLink') {
    $Response = Invoke-RestMethod -Method Get -Uri $Response.'@odata.nextLink' -Headers $Header
    $AllApps += $Response.value
}
 
Write-Output "$($Allapps.Count) Apps were discovered"


$AllSecrets = @()

foreach ($App in $AllApps) {
    foreach ($AppSecret in $App.passwordCredentials) {
        $ExpiresInXDays = $(New-TimeSpan -Start $(Get-Date) -End $(Get-Date $AppSecret.endDateTime)).TotalDays
        switch ($ExpiresInXDays) {
            { $_ -lt 30 } {$Status = "Expires Soon" }
            { $_ -lt 0 } {$Status = "Expired" }
            Default {$Status = "Ok" }
        }

        $SecretOB = [PSCustomObject]@{
            AppID = $App.AppID
            AppName = $App.DisplayName
            SecretID = $AppSecret.KeyID
            ExpiryDate = $AppSecret.endDateTime
            ExpiresInXDays = [math]::Round($ExpiresInXDays)
            Status = $Status
        }

        $AllSecrets += $SecretOB

        if ($SecretOB.ExpiresInXDays -in $NotifyOnDays) {
            [array]$AppOwners = $(Invoke-RestMethod -Method get -Uri "https://graph.microsoft.com/v1.0/applications/$($App.ID)/owners" -Headers $Header).value

            Write-Warning "Secret $($SecretOB.SecretID) on App $($SecretOB.AppID) expires in $($SecretOB.ExpiresInXDays) days."

            if ($AppOwners) {
                foreach ($AdminOwner in $AppOwners | Where-Object { $_.userPrincipalName -like "A_" }) {
                    $SAM = $AdminOwner.userPrincipalName.Split("@")[0].replace("A_","")
                    $RegularUser = $(Invoke-RestMethod -Method GET -uri "https://graph.microsoft.com/v1.0/users?`$filter=startswith(MailNickName, '$SAM')" -Headers $Header).Value
    
                    $AppOwners += $RegularUser
                }
                if ([array]$AppOwners.Mail) {
                    Send-ToTeams -SecretOB $SecretOB
                } else {
                    Send-ToTeams -SecretOB $SecretOB
                }
            } else {
                Send-ToTeams -SecretOB $SecretOB
            }
        } elseif ($SecretOB.ExpiresInXDays -lt -$DeleteSecretIfExpiredForDays) {
            if ($($App.passwordCredentials | Where-Object { $_.endDateTime -gt $(get-date) }).Count -ge 1) {
                Write-Output "Secret $($SecretOB.SecretID) on App $($SecretOB.AppID) expired $(-$SecretOB.ExpiresInXDays) Days ago and can be deleted. Client has at least one Secret that does not expire whithin the next $DeleteSecretIfExpiredForDays days."
            } else {
                Write-Output "Secret $($SecretOB.SecretID) on App $($SecretOB.AppID) expired $(-$SecretOB.ExpiresInXDays) Days ago and can be deleted"
            }

            $SecretDeletionBody = @"
            {
                "keyId": "$($SecretOB.SecretID)"
            }
"@
            #Invoke-RestMethod -Method Post -Uri "https://graph.microsoft.com/v1.0/applications/$($SecretOB.AppID)/removePassword" -Body $SecretDeletionBody -Headers $Header
        }
    }
}

If you want to automatically delete old app registrations that have expired for more than a defined number of days ($DeleteSecretIfExpiredForDays) and no longer have any active secrets, you can show the time 118. This will permanently delete all app registrations that have expired client secrets.