AzureAD App registrations – the “application” permission + credentials combination security nightmare

When talking about Azure AD security, we tend to put less focus on service principals/app registrations*. But when we take into consideration that these principals can have assigned API permissions and “static” credentials (certificate or password) and that these credentials in the wrong hands can cause serious damage, we may change our attitude.
* While “App registrations” and “service principals” are different entites (link) they can be used interchangeably (link)

TL;DR
– Follow best practices for securing service principals: Conditional Access for workload identites, review AAD roles and API permissions of SPs, review SP sign-in logs, pioritize key credential usage over password credentials
– Explore the AzureADToolkit to gain insights on application credentials and API permissions
– Try out my script to start reviewing apps with Application type API permissions

Imaginary example: an IT admin created an app registration which is used in a PowerShell script for some repetitive tasks. The app was granted Directory.ReadWrite.All API permission (Application type, admin consent granted) on Microsoft Graph and a client secret was generated for the app – and this secret is saved as plain text in a script, along with the tenant id and app id. Something like this:

If this script gets into the wrong hands… what a nightmare! 😱

What to do with these app registrations?
Follow security best practices:
– Apply Conditional Access to workload identities (link)
– Review sign-in logs (service principal sign-ins)
– Implement a credential rotation process (especially when key/password credentials are/were accessible for a leaver)
– Review service principals with AzureAD role granted (in preview)

– Prefer key credentials over password credentials (link), don’t store password credentials hardcoded if possible
– Review API permissions for App Registrations
– Identify, investigate and remediate risky workload identities (link)

AzureADToolkit
The last page linked is referencing a very cool toolkit, the AzureADToolkit which can easily identify service principals that have credentials.

Install-Module AzureADToolkit
Connect-AADToolkit
Get-AADToolkitApplicationCredentials | Out-GridView
Sample result of Get-AADToolkitApplicationCredentials

The other useful cmdlet in the toolkit (Build-AzureADAppConsentGrantReport) returns all service principals that have admin consented permissions (each entry contains the resource displayname and the permission* ie.: Microsoft Graph, User.Read)

*sometimes it’s unable to return all the info, in my case the following application has Exchange.ManageAsApp permission, but this property is empty

The two commands combined are probably able to display information for app registrations with admin constented API permissions that have credentials… but, to be honest, I already prepared a script to gather this info when I found that toolkit 🙃

Report script

The following script will return those app registrations that have active (non-expired) credentials, with admin consent on application type API permissions (delegated permissions are intentionally filtered out because those are tied to the authenticated users’ delegated permissions)

Connect-MgGraph

#List apps with cert or password credentials
$apps = Get-MgApplication -all | ? {($_.KeyCredentials -ne $null) -or ($_.PasswordCredentials -ne $null)}
# filter apps with expired credentials
$apps_activeCred = foreach ($app in $apps){ if ((($app.KeyCredentials.EndDateTime | sort -Descending | select -First 1) -gt (get-date)) -or (($app.PasswordCredentials.EndDateTime | sort -Descending | select -First 1) -gt (get-date))){$app}}

function Get-ServicePrincipalRoleAssignmentReadable ($appId){
#query apps that have application permissions with admin consent
$roleAssignments = Get-MgServicePrincipalAppRoleAssignment -ServicePrincipalId (Get-MgServicePrincipal -Filter "appId eq '$($appid)'").id
#match permission entries with resource name and permission name
foreach ($roleAssignment in $roleAssignments){(Get-MgServicePrincipal -ServicePrincipalId $roleAssignment.ResourceId) | select @{'L'="Value";'E'={"$($_.DisplayName)/"+($_.AppRoles | ? {$_.id -eq $roleAssignment.approleid}).value}} }
}

$report = foreach ($app in $apps_activeCred){
[pscustomobject]@{ 
    Name = $app.DisplayName
    AppId = $app.AppId
    LatestKeyExpiration = $app.KeyCredentials.enddatetime | sort -Descending | select -First 1
    LatesPasswordCredential = $app.PasswordCredentials.enddatetime | sort -Descending | select -First 1
    APIPermissions = (Get-ServicePrincipalRoleAssignmentReadable -appId $app.AppId).value

    }
} 
#filter out apps with no application type permissions
$report | ? {$_.apipermissions} | Out-GridView
Sample result of the script
Comments are closed.