---
title: "App Secret Expiration notifications"
canonical: "https://docs.cuesystem.com/space/M3SG/1825669147/App%20Secret%20Expiration%20notifications"
format: markdown
---
Optional addition for CUE Solution setup is setting up notifications. Currenty Microsoft doesn’t have this functionality so we suplement this with a PowerShell script run everyday from our CUE Solution server.

1. We suggest you create a new separate Microsoft App with permissions Application.Read.All in your M365 tenant at [entra.microsoft.com](https://entra.microsoft.com)
2. Than you complete the parametres in this script:

```
<#
.SYNOPSIS
    Checks Registered App Secret\cert Expiry date

.DESCRIPTION
    Connects to MS Graph with a registered app token
    gets all apps where the secret\cert is expiring in the next x days
    sends an email report to a user with the names and dates and details of the apps affected

.INPUTS
    Fill in the required parameters, see Example below

.NOTES
     required app permissions:
     - Application.Read.All

    #>
    
###PLEASE FILL OUT ALL OF THESE PARAMETRES
	
#App Registration Perms App ID
$CredappID = '<YOUR_APP_ID>'

#App Registration Secret value
$CredAppSecret = '<YOUR_APP_SECRET>'

#Tenant ID
$TenantId = '<YOUR_TENANT_ID>'

#Days to check ahead for expiring credentials
$DaysBeforeExpiry = 30

#Email subject line
$EmailSubject = "App Registration Secrets Expiring in $DaysBeforeExpiry days"

#Email recipients to send email to, comma separated list. Handled in foreach loop
$RecipientEmailAddresses = '<EMAIL_RECIEVER>'#,'mark@domain.com','james@domain.com'

#Account whitelisted to send email from in Exchange
$FromAccount = '<YOUR_EMAIL_SENDER>'
$FromAccountPassword = '<YOUR_EMAIL_SENDER_PASSWORD>'

#SMTP Relay endpoint to send mail through
$SMTPServer = '<SMTP_SERVER>'
$SMTPPort = '<SMTP_SERVER_PORT>'

### ALL PARAMETRES ARE FILLED OUT. No editing from this point.

#function to write logs
function Write-Log { 
    [CmdletBinding()] 
    Param 
    ( 
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] 
        [ValidateNotNullOrEmpty()] 
        [Alias("LogContent")] 
        [string]$Message, 
 
        [Parameter(Mandatory = $false)] 
        [Alias('LogPath')] 
        [string]$Path = '.\PowerShellLog.log', 
         
        [Parameter(Mandatory = $false)] 
        [ValidateSet("Error", "Warn", "Info")] 
        [string]$Level = "Info", 
         
        [Parameter(Mandatory = $false)]
        [switch]$NoClobber,

        [Parameter(Mandatory = $false)]
        [switch]$WriteVerbose,

        [Parameter(Mandatory = $false)]
        [switch]$WriteHost,

        [Parameter(Mandatory = $false)]
        [string]$WriteHostForegroundColor
    ) 
 
    Begin { 
        # Set VerbosePreference to Continue so that verbose messages are displayed. 
        $VerbosePreference = 'Continue' 
    } 
    Process { 

        # If the file already exists and NoClobber was specified, do not write to the log. 
        if ((Test-Path $Path) -AND $NoClobber) { 
            Write-Error "Log file $Path already exists, and you specified NoClobber. Either delete the file or specify a different name." 
            Return 
        } 
 
        # If attempting to write to a log file in a folder/path that doesn't exist create the file including the path. 
        elseif (!(Test-Path $Path)) { 
            Write-Verbose "Creating $Path." 
            $NewLogFile = New-Item $Path -Force -ItemType File 
        } 
 
        else { 
            # Nothing to see here yet. 
        } 
 
        # Format Date for our Log File 
        $FormattedDate = Get-Date -Format "yyyy-MM-dd HH:mm:ss" 


        [pscustomobject]@{
            Time     = $FormattedDate
            Severity = $Level
            Message  = $Message
        } | Export-Csv -Path $Path -Append -NoTypeInformation

        if ($WriteVerbose) {
            Write-Verbose -Message $Message
        }

        if ($WriteHost) {
            Write-Host $Message -ForegroundColor $WriteHostForegroundColor
        }
         
    } 
    End { 
    } 
}

#Function to retrieve token
function Get-AccessToken {
    
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string] $AppId,

        [Parameter(Mandatory = $false)]
        [string] $AppSecret = $null,

        [Parameter(Mandatory = $false)]
        [string] $Scope,

        [Parameter(Mandatory = $true)]
        [string] $TenantId,

        [Parameter(Mandatory = $false)]
        [string] $Url,

        [Parameter(Mandatory = $false)]
        [string] $Thumbprint = $null

    )

   #if cert
   if($Thumbprint.length -gt 0){

        #get cert from local store
        $Certificate = Get-Item Cert:\CurrentUser\My\$Thumbprint #thumbprint of the cert
        $Scope = "https://graph.microsoft.com/.default"

        # Create base64 hash of certificate
        $CertificateBase64Hash = [System.Convert]::ToBase64String($Certificate.GetCertHash())

        # Create JWT timestamp for expiration
        $StartDate = (Get-Date "1970-01-01T00:00:00Z" ).ToUniversalTime()
        $JWTExpirationTimeSpan = (New-TimeSpan -Start $StartDate -End (Get-Date).ToUniversalTime().AddDays(1)).TotalSeconds
        $JWTExpiration = [math]::Round($JWTExpirationTimeSpan, 0)

        # Create JWT validity start timestamp
        $NotBeforeExpirationTimeSpan = (New-TimeSpan -Start $StartDate -End ((Get-Date).ToUniversalTime())).TotalSeconds
        $NotBefore = [math]::Round($NotBeforeExpirationTimeSpan, 0)

        # Create JWT header
        $JWTHeader = @{
            alg = "RS256"
            typ = "JWT"
            # Use the CertificateBase64Hash and replace/strip to match web encoding of base64
            x5t = $CertificateBase64Hash -replace '\+', '-' -replace '/', '_' -replace '='
        }

        # Create JWT payload
        $JWTPayLoad = @{
            # What endpoint is allowed to use this JWT
            aud = "https://login.microsoftonline.com/$TenantId/oauth2/token"

            # Expiration timestamp
            exp = $JWTExpiration

            # Issuer = your application
            iss = $AppId

            # JWT ID: random guid
            jti = [guid]::NewGuid()

            # Not to be used before
            nbf = $NotBefore

            # JWT Subject
            sub = $AppId
        }

        # Convert header and payload to base64
        $JWTHeaderToByte = [System.Text.Encoding]::UTF8.GetBytes(($JWTHeader | ConvertTo-Json))
        $EncodedHeader = [System.Convert]::ToBase64String($JWTHeaderToByte)

        $JWTPayLoadToByte = [System.Text.Encoding]::UTF8.GetBytes(($JWTPayload | ConvertTo-Json))
        $EncodedPayload = [System.Convert]::ToBase64String($JWTPayLoadToByte)

        # Join header and Payload with "." to create a valid (unsigned) JWT
        $JWT = $EncodedHeader + "." + $EncodedPayload

        # Get the private key object of your certificate
        # $PrivateKey = $Certificate.PrivateKey
        $PrivateKey = ([System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($Certificate))

        # Define RSA signature and hashing algorithm
        $RSAPadding = [Security.Cryptography.RSASignaturePadding]::Pkcs1
        $HashAlgorithm = [Security.Cryptography.HashAlgorithmName]::SHA256

        # Create a signature of the JWT
        $Signature = [Convert]::ToBase64String(
            $PrivateKey.SignData([System.Text.Encoding]::UTF8.GetBytes($JWT), $HashAlgorithm, $RSAPadding)
        ) -replace '\+', '-' -replace '/', '_' -replace '='

        # Join the signature to the JWT with "."
        $JWT = $JWT + "." + $Signature

        # Create a hash with body parameters
        $Body = @{
            client_id             = $AppId
            client_assertion      = $JWT
            client_assertion_type = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
            scope                 = $Scope
            grant_type            = "client_credentials"
        }

        $Url = "https://login.microsoftonline.com/$TenantId/oauth2/v2.0/token"

        # Use the self-generated JWT as Authorization
        $Header = @{
            Authorization = "Bearer $JWT"
        }

        # Splat the parameters for Invoke-Restmethod for cleaner code
        $PostSplat = @{
            ContentType = 'application/x-www-form-urlencoded'
            Method      = 'POST'
            Body        = $Body
            Uri         = $Url
            Headers     = $Header
        }

        $Request = Invoke-RestMethod @PostSplat
        return $request.access_token 
   }

   #if Secret
   if($AppSecret.length -gt 0){
        
        $Scope = "https://graph.microsoft.com/.default"
        $Url = "https://login.microsoftonline.com/$TenantId/oauth2/v2.0/token"

        # Add System.Web for urlencode
        Add-Type -AssemblyName System.Web

        # Create body
        $Body = @{
            client_id = $AppId
            client_secret = $AppSecret
            scope = $Scope
            grant_type = 'client_credentials'
        }

        # Splat the parameters for Invoke-Restmethod for cleaner code
        $PostSplat = @{
            ContentType = 'application/x-www-form-urlencoded'
            Method = 'POST'
            # Create string by joining bodylist with '&'
            Body = $Body
            Uri = $Url
        }
        # Request the token!
        $Request = Invoke-RestMethod @PostSplat
        return $Request.access_token
    }

}

#function to send email
Function Send-Mail {
    param (
        [string]$SMTPServer,
        [string]$SMTPPort,
        [string]$Username,
        [string]$Password,
        [string]$to,
        [string]$subject,
        [string]$body
    )

    $message = New-Object System.Net.Mail.MailMessage
    $message.subject = $subject
    $message.body = $body
    $message.IsBodyHtml = $true
    $message.to.add($to)
    $message.from = $Username

    $smtp = New-Object System.Net.Mail.SmtpClient($SMTPServer, $SMTPPort);
    $smtp.EnableSSL = $true
    $smtp.Credentials = New-Object System.Net.NetworkCredential($Username, $Password);
    $smtp.send($message)
}

#logpath
#Prefix log file with yyyyMM
$CurrentMonth = Get-Date -Format yyyyMM

#path to log file
$LogPath = "$PSScriptRoot\LogFiles\$CurrentMonth-Check-RegAppSecretExpiry.log"

#Retrieve tokens
try{
    $CredAccesstoken = Get-AccessToken -AppId $CredappID -AppSecret $CredAppSecret -TenantId $TenantId
}catch{
    Write-Log -Level Error -Message "Error retrieving one or more Access tokens $($_.exception)" -Path $LogPath
}

#Create Cred App Header
$CredHeader = @{
    "Authorization"="Bearer $CredAccesstoken" 
    "Content-type"="application/json"
}

#Request data + Paging
$Appslist = @()

try{
    $Apps = Invoke-RestMethod -Method Get -Uri "https://graph.microsoft.com/v1.0/applications?$select=id,keyCredentials,passwordCredentials" -Headers $CredHeader -ErrorAction Stop
    $Appslist += $apps.value
}catch{
    Write-Log -Level Error -Message "Error with Graph request: $($_.exception)" -Path $LogPath
}

if($apps.'@odata.nextLink' -ne $null){
    while($apps.'@odata.nextLink' -ne $null){
        $apps = Invoke-RestMethod -Method Get -Uri $apps.'@odata.nextLink' -Headers $CredHeader -ErrorAction Stop
        $Appslist += $apps.value
    }
}

#Get any apps where the secret is good
$OKlist = @()
foreach($app in $Appslist){
    
    foreach($key in $app.passwordCredentials){
        if((get-date $key.endDateTime) -gt (get-date).AddDays(0)){
            #Secret good
            $obj = New-Object PSobject
            $Obj | Add-Member -MemberType NoteProperty -Name "AppName" -Value $app.displayName
            $Obj | Add-Member -MemberType NoteProperty -Name "KeyType" -Value "Secret"
            $Obj | Add-Member -MemberType NoteProperty -Name "KeyName" -Value $Key.displayName
            $Obj | Add-Member -MemberType NoteProperty -Name "Expiry" -Value $Key.endDateTime
            $OKlist += $Obj
        }
    }

    foreach($key in $app.keyCredentials){
        if((get-date $key.endDateTime) -gt (get-date).AddDays(0)){
            #Cert good
            $obj = New-Object PSobject
            $Obj | Add-Member -MemberType NoteProperty -Name "AppName" -Value $app.displayName
            $Obj | Add-Member -MemberType NoteProperty -Name "KeyType" -Value "Certificate"
            $Obj | Add-Member -MemberType NoteProperty -Name "KeyName" -Value $Key.displayName
            $Obj | Add-Member -MemberType NoteProperty -Name "Expiry" -Value $Key.endDateTime
            $OKlist += $Obj
        }
    }
}

#Get any apps where the secret expiry is soon
$expiringlist = @()
foreach($app in $Appslist){
    
    $xdaysfromtoday = (get-date).AddDays($DaysBeforeExpiry)
    #expiry lt 30 days from now and gt now or already expired
    #(get-date $key.endDateTime) -lt $xdaysfromtoday -and (get-date $key.endDateTime) -gt (get-date) -or ((get-date $key.endDateTime) -lt (get-date))

    foreach($key in $app.passwordCredentials ){
        if((get-date $key.endDateTime) -lt $xdaysfromtoday -and (get-date $key.endDateTime) -gt (get-date) -or ((get-date $key.endDateTime) -lt (get-date))){
            #Secret expiring
            $obj = New-Object PSobject
            $Obj | Add-Member -MemberType NoteProperty -Name "AppName" -Value $app.displayName
            $Obj | Add-Member -MemberType NoteProperty -Name "KeyType" -Value "Secret"
            $Obj | Add-Member -MemberType NoteProperty -Name "KeyName" -Value $Key.displayName
            $Obj | Add-Member -MemberType NoteProperty -Name "Expiry" -Value $Key.endDateTime
            $expiringlist += $Obj
        }
    }

    foreach($key in $app.keyCredentials){
        if((get-date $key.endDateTime) -lt $xdaysfromtoday -and (get-date $key.endDateTime) -gt (get-date) -or ((get-date $key.endDateTime) -lt (get-date))){
            #Cert expiring
            $obj = New-Object PSobject
            $Obj | Add-Member -MemberType NoteProperty -Name "AppName" -Value $app.displayName
            $Obj | Add-Member -MemberType NoteProperty -Name "KeyType" -Value "Certificate"
            $Obj | Add-Member -MemberType NoteProperty -Name "KeyName" -Value $Key.displayName
            $Obj | Add-Member -MemberType NoteProperty -Name "Expiry" -Value $Key.endDateTime
            $expiringlist += $Obj
        }
    }
}

#Get unique lists
$uniqueExpiring = $expiringlist | Select AppName -Unique
$uniqueOK = $oklist | Select AppName -Unique

#Apps that have no active secret or Cert
$concerns = $uniqueExpiring | ?{$_.Appname -notin $uniqueOK.appname}
$concernlist = $expiringlist | ?{$_.appname -in $concerns.Appname}

#Email styling
$tablestyle = "

    <style>
        table {
          background-color: #FFFFFF;
          border-collapse: collapse;
          border-width: 2px;
          border-color: #DDDDDD;
          border-style: solid;
          color: #000000;
          font-family: `"Segoe UI`", Arial, sans-serif;
        }

        td {
          border-width: 2px;
          border-color: #DDDDDD;
          border-style: solid;
          padding: 3px;
          white-space: nowrap;
        }

        th {
          background-color: #DDDDDD;

        }

    </style>
"

#Email body
$htmlbody = "
    $tablestyle

    <p>$($expiringlist.count) - App registrations with secrets\\certs that <b>are expired</b> or about to <b>expire in the next $DaysBeforeExpiry days</b>:</p>
    $($expiringlist | Sort-Object Expiry | Select AppName,KeyType, @{n='Expiry';e={get-date $_.Expiry -Format "dd/MM/yyyy HH:mm:ss"}} | ConvertTo-Html -Fragment)
 
    <p>$($($concernlist | Select AppName -Unique).count) Apps of concern - App that have <b>no valid credentials</b>:</p>
    $(($concernlist | Select AppName -Unique | ConvertTo-Html -Fragment) -replace "\*", 'AppName')

"

#Send email
foreach($email in $RecipientEmailAddresses){  
    try{
        Send-Mail -SMTPServer $SMTPServer -SMTPPort $SMTPPort -Username $FromAccount -Password $FromAccountPassword -to $email -subject $EmailSubject -body $htmlBody
    }catch{
        Write-Log -Level Error -Message "Error with Send Email request: $($_.exception)" -Path $LogPath
    }
}

```

3. Save this as a .ps1 file and copy onto your server.
4. On the server, open Task Scheduler and import this task
5. You can customize the tasks parametres / description and you are all set.