Update-AADToolkitApplicationCredentials.ps1
<#
.Synopsis Helper utility to remove or roll over the client secrets and certificates of a given application or service principals in Azure Active Directory. Run Get-AADToolkitApplicationCredentials to get a summary of all the applications and service principals .Description This interactive function allows a user to select an application or service principle and manage it's credentials. .Example Update-AADToolkitApplicationCredentials #> function Update-AADToolkitApplicationCredentials { function Show-AppInfo ($appInfo){ $appDisplay = $appInfo | Format-List -Property objectId, appId | Out-String $appCreds = $appInfo.creds | Format-Table -Property id, keyId, startDateTime, endDateTime, expired, credentialtype, description | Out-String Write-Host $appDisplay.Trim() Write-Host Write-Host $appCreds.Trim() } function Show-Menu { Param($appInfo, [string[]]$MenuItems, [string] $Title ) $header = $null if (![string]::IsNullOrWhiteSpace($Title)) { $len = [math]::Max(($MenuItems | Measure-Object -Maximum -Property Length).Maximum, $Title.Length) $header = '{0}{1}{2}' -f $Title, [Environment]::NewLine, ('-' * $len) } # possible choices: didits 1 to 9, characters A to Z $choices = (49..57) + (65..90) | ForEach-Object { [char]$_ } $i = 0 $items = ($MenuItems | ForEach-Object { '[{0}] {1}' -f $choices[$i++], $_ }) -join [Environment]::NewLine # display the menu and return the chosen option while ($true) { Clear-Host if ($header) { Write-Host $header -ForegroundColor Yellow } Show-AppInfo $appInfo Write-Host Write-Host "What do you want to do?" -ForegroundColor Yellow Write-Host $items $answer = (Read-Host -Prompt 'Please make your choice').ToUpper() $index = $choices.IndexOf($answer[0]) if ($index -ge 0 -and $index -lt $MenuItems.Count) { return $MenuItems[$index] } else { Write-Warning "Invalid choice.. Please try again." Start-Sleep -Seconds 2 } } } function Get-CredentialInfo ($id, $cred, $credentialType) { $expired = "No" if(Get-IsExpired -date $cred.endDateTime){ $expired = "Yes" } [pscustomobject]@{ Id = $id CredentialType = $credentialType KeyId = $cred.keyId Hint = $cred.hint Description = $cred.displayName StartDateTime = $cred.startDateTime EndDateTime = $cred.endDateTime KeyType = $cred.type Usage = $cred.usage Expired = $expired } } function Invoke-CredentialRollover ($appInfo, $id){ $rolloverKey = $appInfo.Creds | Where-Object {$_.Id -eq $id} switch ($rolloverKey.CredentialType) { $credentialTypePassword { Add-Password -objectId $appInfo.objectId $appInfo.objectType Remove-Password -objectId $appInfo.objectId -objectType $appInfo.objectType -keyId $rolloverKey.keyId Write-Host "Secret rolled over successfully. Copy the 'SecretText' shown above and update your application to use the new secret." -ForegroundColor Yellow } $credentialTypeKey { $certFilePath = Read-Host -Prompt 'Enter the path to the certificate file' if($certFilePath.StartsWith('"') -and $certFilePath.EndsWith('"')){ #Remove the double-quotes that Windows adds in 'Copy as path' $certFilePath = $certFilePath.Substring(1, $certFilePath.Length -2) } if((Test-Path $certFilePath)){ $appInfo.keyCredentials = $appInfo.keyCredentials | Where-Object {$_.keyId -ne $rolloverKey.keyId} #Remove the keyId that is being rolled over Add-Key -objectId $appInfo.objectId $appInfo.objectType -certFilePath $certFilePath Write-Host "Certificate rolled over successfully." -ForegroundColor Yellow } else { Write-Error "Invalid certificate file path." -ErrorAction Stop } } } } function Get-IsExpired($date){ return (Get-Date).Subtract($date) -gt 0 } function Get-Passwords($appInfo) { return $appInfo.Creds | Where-Object {$_.CredentialType -eq $credentialTypePassword} } function Get-Certificates($appInfo) { return $appInfo.Creds | Where-Object {$_.CredentialType -eq $credentialTypeKey} } function Get-ExpiredCredentials($appInfo) { return $appInfo.Creds | Where-Object {(Get-IsExpired -date $_.endDateTime)} } function Get-ExpiredPasswords($appInfo) { return $appInfo.Creds | Where-Object {(Get-IsExpired -date $_.endDateTime) -and $_.CredentialType -eq $credentialTypePassword} } function Get-ExpiredCertificates($appInfo) { return $appInfo.Creds | Where-Object {(Get-IsExpired -date $_.endDateTime) -and $_.CredentialType -eq $credentialTypeKey} } function Remove-AppCredentials($appInfo, $selection) { switch ($selection) { $menuRemoveAll { $credsToRemove = $appInfo.Creds } $menuRemoveSecrets { $credsToRemove = Get-Passwords -appInfo $appInfo } $menuRemoveCerts { $credsToRemove = Get-Certificates -appInfo $appInfo } $menuRemoveExpiredAll { $credsToRemove = Get-ExpiredCredentials -appInfo $appInfo } $menuRemoveExpiredSecrets{ $credsToRemove = Get-ExpiredPasswords -appInfo $appInfo } $menuRemoveExpiredCerts { $credsToRemove = Get-ExpiredCertificates -appInfo $appInfo } } # Remove passwords foreach($cred in $credsToRemove){ if($cred.CredentialType -eq $credentialTypePassword){ Remove-Password -objectId $appInfo.objectId -objectType $appInfo.objectType -keyId $cred.keyId } } # Remove keys Remove-Keys -appInfo $appInfo -credsToRemove $credsToRemove } function Remove-Keys ($appInfo, $credsToRemove) { $keyCredsToRemove = $credsToRemove | Where-Object {$_.CredentialType -eq $credentialTypeKey} foreach($cred in $keyCredsToRemove) { Write-Host ("Removing certificate ({0}) from {1} ({2})" -f $cred.keyId, $appInfo.ObjectType, $appInfo.ObjectId) $appInfo.keyCredentials = $appInfo.keyCredentials | Where-Object {$_.keyId -ne $cred.keyId} } if($appInfo.keyCredentials.Length -eq 0){ # Convert null to an empty array to generate a Graph compatible json $appInfo.KeyCredentials = @() } $body = @{keyCredentials = $appInfo.KeyCredentials} | ConvertTo-Json $uri = '/{0}/{1}' -f $appInfo.GraphObjectType, $appInfo.objectId Invoke-AADTGraph -uri $uri -body $body -method PATCH } function Remove-Password($objectId, $objectType, $keyId){ Write-Host ("Removing client secret ({0}) from {1} ({2})" -f $keyId, $objectType, $objectId) switch ($objectType) { $objectTypeApplication { $graphObjectType = 'applications' } $objectTypeServicePrincipal { $graphObjectType = 'servicePrincipals' } } $uri = "/$graphObjectType/$objectId/removePassword" $body = @{keyId=$keyId} | ConvertTo-Json Invoke-AADTGraph -uri $uri -method POST -body $body } function Add-Password($objectId, $objectType){ Write-Host ("Rolling over client secret for {0} ({1})" -f $objectType, $objectId) switch ($objectType) { $objectTypeApplication { $graphObjectType = 'applications' } $objectTypeServicePrincipal { $graphObjectType = 'servicePrincipals' } } $uri = "/$graphObjectType/$objectId/addPassword" $body = @{passwordCredential = @{displayName="Rollover"; endDateTime=((Get-Date).AddYears(1).ToString('s'))} } | ConvertTo-Json Invoke-AADTGraph -uri $uri -method POST -body $body } function Add-Key($objectId, $objectType, $certFilePath){ Write-Host ("Rolling over certificate for {0} ({1})" -f $objectType, $objectId) switch ($objectType) { $objectTypeApplication { $graphObjectType = 'applications' } $objectTypeServicePrincipal { $graphObjectType = 'servicePrincipals' } } $uri = "/$graphObjectType/$objectId/addKey" $cer = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($certFilePath) $bin = $cer.GetRawCertData() $base64Value = [System.Convert]::ToBase64String($bin) $bin = $cer.GetCertHash() $base64Thumbprint = [System.Convert]::ToBase64String($bin) $keyId = [System.Guid]::NewGuid().ToString() $newKeyCredential = @{ customKeyIdentifier = $base64Thumbprint displayName = 'Rollover' keyId = $keyId type = 'AsymmetricX509Cert' usage = 'Verify' key = $base64Value } $keyCreds = @($newKeyCredential) foreach($k in $appInfo.KeyCredentials){ #Convert dates to ISO8601. PowerShell Core does this correctly but PowerShell Windows needs a manual conversion like this. $k.startDateTime = $k.startDateTime.ToString("s") $k.endDateTime = $k.endDateTime.ToString("s") $keyCreds += $k } $appInfo.KeyCredentials = $keyCreds $body = @{keyCredentials = $appInfo.KeyCredentials} | ConvertTo-Json $uri = '/{0}/{1}' -f $appInfo.GraphObjectType, $appInfo.objectId Invoke-AADTGraph -uri $uri -body $body -method PATCH } function Get-GraphSearchResults($type, $searchObjectId){ $searchValue = "'$searchObjectId'" $uri = '/{0}?$filter=id eq {1} or id eq {1}' -f $type, $searchValue #Repear with or to avoid PowerShell Graph SDK throwing errors when there are no entries return Invoke-AADTGraph -uri $uri } function Get-GraphApp($searchObjectId){ $result = Get-GraphSearchResults -type $graphObjectTypeApplications -searchObjectId $searchObjectId if($result.value.length -eq 1){ $ObjectType = $objectTypeApplication $graphObjectType = $graphObjectTypeApplications $app = $result.value } else { $result = Get-GraphSearchResults -type $graphObjectTypeServicePrincipals -searchObjectId $searchObjectId if($result.value.length -eq 1){ $ObjectType = $objectTypeServicePrincipal $graphObjectType = $graphObjectTypeServicePrincipals $app = $result.value } else { Write-Error "Object Id not found." -ErrorAction Stop } } $creds = @() $index = 1 foreach($cred in $app.passwordCredentials) { $creds += Get-CredentialInfo -id $index -cred $cred -credentialType $credentialTypePassword $index++ } foreach($cred in $app.keyCredentials) { $creds += Get-CredentialInfo -id $index -cred $cred -credentialType $credentialTypeKey $index++ } [pscustomobject]@{ ObjectType = $objectType GraphObjectType = $graphObjectType Creds = $creds KeyCredentials = $app.keyCredentials #Until GraphAPI supports removing KeyCredentials, need to cache the KeyCredentials and use them with a PATCH ObjectId = $app.id DisplayName = $app.displayName AppId = $app.appId } } $searchObjectId = Read-Host -Prompt 'Enter the ObjectId of the Application or Service Principal' $objectTypeApplication = 'Application' $objectTypeServicePrincipal = 'Service Principal' $graphObjectTypeApplications = 'applications' $graphObjectTypeServicePrincipals = 'servicePrincipals' $credentialTypePassword = 'Client secret' $credentialTypeKey = 'Certificate' $menuRemoveAll = 'Remove all certificates and secrets for this object' $menuRemoveCerts = 'Remove all certificates for this object' $menuRemoveSecrets = 'Remove all secrets for this object' $menuRemoveExpiredAll = 'Remove expired certificates and secrets for this object' $menuRemoveExpiredCerts = 'Remove expired certificates for this object' $menuRemoveExpiredSecrets = 'Remove expired secrets for this object' $menuRolloverCred = 'Rollover a certificate or secret for this object' $menuQuit = 'Quit' if(![guid]::TryParse($searchObjectId, $([ref][guid]::Empty))) { Write-Error "Invalid object identifier format" -ErrorAction Stop } else { $appInfo = Get-GraphApp -searchObjectId $searchObjectId if(!$appInfo){ Write-Error "Application or ServicePrincipal with this ObjectId was not found" } ##TODO Filter to show options applicable for the selected app if($appInfo.Creds -and $appInfo.Creds.Length -gt 0) { $menu = @($menuRemoveAll) if(Get-Passwords -appInfo $appInfo){ $menu += $menuRemoveSecrets} if(Get-Certificates -appInfo $appInfo){ $menu += $menuRemoveCerts} if(Get-ExpiredCredentials -appInfo $appInfo){ $menu += $menuRemoveExpiredAll} if(Get-ExpiredPasswords -appInfo $appInfo){ $menu += $menuRemoveExpiredSecrets} if(Get-ExpiredCertificates -appInfo $appInfo){ $menu += $menuRemoveExpiredCerts} $menu += $menuRolloverCred, $menuQuit $title = "Manage credentials for {0}: {1} ({2})" -f $appInfo.ObjectType, $appInfo.DisplayName, $appInfo.ObjectId $selection = Show-Menu -appInfo $appInfo -MenuItems $menu -Title $title switch ($selection) { {$_ -in $menuRemoveAll, $menuRemoveCerts, $menuRemoveSecrets, $menuRemoveExpiredAll, $menuRemoveExpiredCerts, $menuRemoveExpiredSecrets}{ Remove-AppCredentials -appInfo $appInfo -selection $selection } $menuRolloverCred { $message = "Enter the Id of the client secret or certificate to be rolled over (1..{0})" -f $appInfo.Creds.Length $rolloverId = Read-Host -Prompt $message if($rolloverId -lt 1 -or $rolloverId -gt $appInfo.Creds.Length){ Write-Error "Invalid Id." -ErrorAction Stop } else { Invoke-CredentialRollover -appInfo $appInfo -id $rolloverId } } } } else { $message = "{0} does have any client secrets or certificates." -f $appInfo.ObjectType Write-Error $message } } } |