Update-ClientSecrets.ps1
<#
.AUTHOR Nik Chikersal .SYNOPSIS This function is used to Renew Secrets for Azure App Registrations .EXAMPLE Update-ClientSecrets -MailboxSender Someone@Domain.com -TeamsWebHookURL https://outlook.office.com/webhook/12345 This example shows how to Renew Client secrets for Azure App Registrations and send a Teams Message if a failure occurs .NOTES Ensure this Script is being run in an Azure Automation Account with PWSH 7.2+, using an MSI with the proper RBAC Permissions. #> function Update-ClientSecrets { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string]$MailboxSender, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string]$KeyVaultName, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string]$TeamsWebHookURL ) try { Connect-AzAccount -Identity | Out-Null } catch { Write-Warning $Global:Error.Exception.Message[0] } if ($env:Username -or $env:USER -ne "ContainerUser") { Write-Warning "This Script must be run in an Azure Automation Account (Run on Azure)" Start-Sleep -Seconds 10 Exit 1 } $BearerToken = Get-GraphAccessToken -UseMSI [hashtable]$AppSplatArgs = @{ Headers = @{Authorization = "Bearer $($BearerToken)"} Uri = 'https://graph.microsoft.com/v1.0/applications' Method = 'GET' } Connect-MgGraph -Identity -NoWelcome | Out-Null (Invoke-RestMethod @AppSplatArgs).Value $Results = [System.Collections.ArrayList]::new() $Date = Get-Date $Tomorrow = $Date.AddDays(1).ToString().Split(" ")[0] $Today = $Date.ToString().Split(" ")[0] $Time = Get-Date -Format hh:mm (Invoke-RestMethod @AppSplatArgs).Value | Where-Object {$_.PasswordCredentials.Enddatetime.count -gt "0"} | ForEach-Object { $AppReg = $_ $AppReg.PasswordCredentials.Enddatetime | ForEach-Object { $_.ToString("M/dd/yyy") | ForEach-Object { $SecretExpiryDate = $_ [hashtable]$AppOwnerSplatArgs = @{ Headers = @{Authorization = "Bearer $($BearerToken)"} Uri = "https://graph.microsoft.com/v1.0/applications/$($AppReg.ID)/Owners" Method = 'GET' } (Invoke-RestMethod @AppownerSplatArgs).Value | ForEach-Object { $AppRegOwnerSMTP = $_ $CustomObject = [PSCustomObject]@{ AppName = $AppReg.DisplayName SecretName = $AppReg.PasswordCredentials.DisplayName SecretExpiryDate = $SecretExpiryDate SecretKeyID = $AppReg.PasswordCredentials.KeyID AppID = $AppReg.ID AppOwner = $AppRegOwnerSMTP.mail } [void]$Results.Add($CustomObject) } } } } $ExpiringSecrets = [System.Collections.ArrayList]::new() $Results | Where-Object {$_.SecretExpiryDate.Equals($Tomorrow)} | ForEach-Object { $Expiring = $_ $Object = [PSCustomObject][Ordered]@{ AppName = $Expiring.AppName SecretName = $Expiring.SecretName SecretExpiryDate = $Expiring.SecretExpiryDate SecretKeyID = $Expiring.SecretKeyID AppID = $Expiring.AppID AppOwner = $Expiring.AppOwner } $ExpiringSecrets.Add($Object) } if ( -not [string]::IsNullOrEmpty($ExpiringSecrets)) { Write-Output "The following Secrets are expiring soon:" $ExpiringSecrets | Format-Table -AutoSize } else { Write-Warning "There are no App Registrations with Secrets close to Expiry" Exit 1 } $ExpiringSecrets | ForEach-Object { $SecretToRemove = $_ [Array]$SecretToRemove.SecretKeyID | ForEach-Object { $SecretKeyID = $_ $RemoveSecretParams = @{ KeyId = $SecretKeyID } try { Write-Output "Removing Secret: $SecretKeyID from $($SecretToRemove.AppName)" Start-Sleep -Seconds 10 [hashtable]$SecretRemovalArgs = @{ ApplicationId = $SecretToRemove.AppID BodyParameter = $RemoveSecretParams ErrorAction = 'STOP' } Remove-MgApplicationPassword @SecretRemovalArgs } catch { Write-Output "Failed to remove secret $($Error[0].Exception.Message)" } } } $RenewedSecretsResultsArray = [System.Collections.ArrayList]::new() $ExpiringSecrets | ForEach-Object { $SecretToRenew = $_ $SecretToRenew.SecretName | ForEach-Object { $SecretName = $_ $TrimmedOldSecret = [System.Text.RegularExpressions.Regex]::Replace($SecretName, ": Renewed.*", "") $RenewSecretParams = @{ passwordCredential = @{ DisplayName = "$($TrimmedOldSecret): Renewed $Today - $Time" EndDateTime = (Get-Date).AddMonths(3) } } try { [hashtable]$SecretRenewalArgs = @{ ApplicationId = $SecretToRenew.AppID BodyParameter = $RenewSecretParams } Start-Sleep -Seconds 14 Write-Output "Renewing Secrets for $($Expiring.AppName): $($Expiring.AppID)" $Result = Add-MgApplicationPassword @SecretRenewalArgs [void]$RenewedSecretsResultsArray.Add($Result) } catch { Write-Output "There was an Error renewing the Secret for $($Expiring.AppName)" [PSCustomObject][Ordered]@{ Failure = $Error.Exception.Message AdditionalDetails = $Error.FullyQualifiedErrorId ErrorID = $Error.Errors $ErrorDetails = $Error.ErrorDetails } } } } try { Connect-AzAccount -Identity | Out-Null $RenewedSecretsResultsArray | ForEach-Object { $KeyVaultSecret = $_ $SecretNamePrefix = $KeyVaultSecret.DisplayName.Split(" ").Replace(" ", "").Replace(":", "")[0] $SecretType = "-" + $KeyVaultSecret.DisplayName.Replace(":", "").Split(" ")[1] $ConstructedSecretName = $SecretToRenew.AppName + "-" + $SecretNamePrefix + $SecretType $EncryptedSecret = ConvertTo-SecureString -String $KeyVaultSecret.SecretText -AsPlainText -Force [hashtable]$KeyVaultArgs = @{ VaultName = $KeyVaultName Name = $ConstructedSecretName SecretValue = $EncryptedSecret } Set-AzKeyVaultSecret @KeyVaultArgs | ForEach-Object { Write-Output "" Write-Output "" Write-Output "Creating Secret $($_.Name) in $($KeyVaultArgs.VaultName)" } } } catch { Write-Output "There was an Error renewing the Secret for $($Expiring.AppName)" [PSCustomObject][Ordered]@{ Failure = $Error.Exception.Message AdditionalDetails = $Error.FullyQualifiedErrorId ErrorID = $Error.Errors $ErrorDetails = $Error.ErrorDetails } $TeamsError = $Expiring.AppName | Out-String $JsonBody = [PSCustomObject][Ordered]@{ "@type" = "MessageCard" "@context" = "http://schema.org/extensions" "summary" = "One or More Application Registrations have failed to Renew Secrets" "themeColor" = "0078D7" "title" = "One or More Application Registrations have failed to Renew Secrets" "text" = "Application Registration Failed to Renew One or More Secrets: App Reg Name: $($TeamsError)" } $TeamMessageBody = ConvertTo-Json $JsonBody -Depth 100 [hashtable]$WebhookArgs = @{ "URI" = $TeamsWebHookURL "Method" = 'POST' "Body" = $TeamMessageBody "ContentType" = 'application/json' } Invoke-RestMethod @WebhookArgs -ErrorAction SilentlyContinue } $EmailTop = [string]"<h2>One or More App Registration Secrets are Expiring</h2> <br>" $EmailBody = $ExpiringSecrets | Select-Object AppName, SecretExpiryDate, @{N='Secrets'; E={($_.SecretName -join ', ')}} | ConvertTo-Html -Fragment | Out-String -Width 10 $EmailBody = $EmailTop + $EmailBody $ExpiringSecrets | Where-Object {$_.AppOwner -ne $null} | ForEach-Object { $AppOwnerPrimarySMTP = $_.AppOwner $Subject = "Alert: One or More App Registration Secrets are Expiring" try { Send-GraphEmail -MailboxSender $MailboxSender -MailboxRecipient $AppOwnerPrimarySMTP -Subject $Subject -EmailBody $EmailBody -UseMSI } catch { Write-Output "$($Error[0].Exception.Message)" } } } |