Scripts/Get-OAuthPermissions.ps1

function CacheObject ($Object) {
    if ($Object) {
        if (-not $script:ObjectByObjectClassId.ContainsKey($Object.ObjectType)) {
            $script:ObjectByObjectClassId[$Object.ObjectType] = @{}
        }
        $script:ObjectByObjectClassId[$Object.ObjectType][$Object.ObjectId] = $Object
        $script:ObjectByObjectId[$Object.ObjectId] = $Object
    }
}

# Function to retrieve an object from the cache (if it's there), or from Azure AD (if not).
function GetObjectByObjectId ($ObjectId) {
    if (-not $script:ObjectByObjectId.ContainsKey($ObjectId)) {
        Write-Verbose ("Querying Azure AD for object '{0}'" -f $ObjectId)
        try {
            $object = Get-AzureADObjectByObjectId -ObjectId $ObjectId
            CacheObject -Object $object
        } catch {
            Write-Verbose "Object not found."
        }
    }
    return $script:ObjectByObjectId[$ObjectId]
}

# Function to retrieve all OAuth2PermissionGrants, either by directly listing them (-FastMode)
# or by iterating over all ServicePrincipal objects. The latter is required if there are more than
# 999 OAuth2PermissionGrants in the tenant, due to a bug in Azure AD.
function GetOAuth2PermissionGrants ([switch]$FastMode) {
    if ($FastMode) {
        Get-AzureADOAuth2PermissionGrant -All $true
    } else {
        $script:ObjectByObjectClassId['ServicePrincipal'].GetEnumerator() | ForEach-Object { $i = 0 } {
            if ($ShowProgress) {
                Write-Progress -Activity "Retrieving delegated permissions..." `
                               -Status ("Checked {0}/{1} apps" -f $i++, $servicePrincipalCount) `
                               -PercentComplete (($i / $servicePrincipalCount) * 100)
            }

            $client = $_.Value
            Get-AzureADServicePrincipalOAuth2PermissionGrant -ObjectId $client.ObjectId
        }
    }
}

function GetAzureADServicePrincipal ($ObjectId) {
    Get-AzureADServicePrincipal -ObjectId $ObjectId | ForEach-Object {
        $Output = $_
        $script:homepage = $Output.Homepage
        $script:PublisherName = $Output.PublisherName
        $script:ReplyUrls = $Output.ReplyUrls
        $script:AppDisplayName = $Output.AppDisplayName
        $script:AppId = $Output.AppId
    }
}

function Get-OAuthPermissions
{
<#
.SYNOPSIS
Lists delegated permissions (OAuth2PermissionGrants) and application permissions (AppRoleAssignments).
Script inspired by: https://gist.github.com/psignoret/41793f8c6211d2df5051d77ca3728c09
 
.DESCRIPTION
Script to list all delegated permissions and application permissions in Azure AD
The output will be written to a CSV file.
 
.PARAMETER OutputDir
outputDir is the parameter specifying the output directory.
Default: Output\OAuthPermissions
 
.PARAMETER Encoding
Encoding is the parameter specifying the encoding of the CSV output file.
Default: UTF8
 
.EXAMPLE
Get-OAuthPermissions
Lists delegated permissions (OAuth2PermissionGrants) and application permissions (AppRoleAssignments).
 
#>


    [CmdletBinding()]
    param(
        [switch] $DelegatedPermissions,
        [switch] $ApplicationPermissions,
        [string[]] $UserProperties = @("DisplayName"),
        [string[]] $ServicePrincipalProperties = @("DisplayName"),
        [switch] $ShowProgress = $true,
        [int] $PrecacheSize = 999,
        [string] $OutputDir = "Output\OAuthPermissions",
        [string] $Encoding = "UTF8"
    )
        
    try {
        $tenant_details = Get-AzureADTenantDetail -ErrorAction stop
    } catch {
        write-logFile -Message "[INFO] Ensure you are connected to Azure by running the Connect-Azure command before executing this script" -Color "Yellow"
        Write-logFile -Message "[ERROR] An error occurred: $($_.Exception.Message)" -Color "Red"
        throw
    }

    write-logFile -Message "[INFO] Running Get-OAuthPermissions" -Color "Green"
    $date = Get-Date -Format "ddMMyyyyHHmmss" 
    
    if (!(test-path $OutputDir)) {
        New-Item -ItemType Directory -Force -Name $OutputDir > $null
        write-logFile -Message "[INFO] Creating the following directory: $OutputDir"
    }

    else {
        if (Test-Path -Path $OutputDir) {
            write-LogFile -Message "[INFO] Custom directory set to: $OutputDir"
        }
    
        else {
            write-Error "[Error] Custom directory invalid: $OutputDir exiting script" -ErrorAction Stop
            write-LogFile -Message "[Error] Custom directory invalid: $OutputDir exiting script"
        }
    }
    
    $report = @(
    Write-Verbose ("TenantId: {0}, InitialDomain: {1}" -f `
                    $tenant_details.ObjectId, `
                    ($tenant_details.VerifiedDomains | Where-Object { $_.Initial }).Name)

    # An in-memory cache of objects by {object ID} andy by {object class, object ID}
    $script:ObjectByObjectId = @{}
    $script:ObjectByObjectClassId = @{}

    $empty = @{} # Used later to avoid null checks

    # Get all ServicePrincipal objects and add to the cache
    Write-Verbose "Retrieving all ServicePrincipal objects..."
    Get-AzureADServicePrincipal -All $true | ForEach-Object {
        CacheObject -Object $_
    }
    $servicePrincipalCount = $script:ObjectByObjectClassId['ServicePrincipal'].Count

    if ($DelegatedPermissions -or (-not ($DelegatedPermissions -or $ApplicationPermissions))) {

        # Get one page of User objects and add to the cache
        Write-Verbose ("Retrieving up to {0} User objects..." -f $PrecacheSize)
        Get-AzureADUser -Top $PrecacheSize | Where-Object {
            CacheObject -Object $_
        }

        Write-Verbose "Testing for OAuth2PermissionGrants bug before querying..."
        $fastQueryMode = $false
        try {
            # There's a bug in Azure AD Graph which does not allow for directly listing
            # oauth2PermissionGrants if there are more than 999 of them. The following line will
            # trigger this bug (if it still exists) and throw an exception.
            $null = Get-AzureADOAuth2PermissionGrant -Top 999
            $fastQueryMode = $true
        } catch {
            if ($_.Exception.Message -and $_.Exception.Message.StartsWith("Unexpected end when deserializing array.")) {
                Write-Verbose ("Fast query for delegated permissions failed, using slow method...")
            } else {
                throw $_
            }
        }

        # Get all existing OAuth2 permission grants, get the client, resource and scope details
        Write-Verbose "Retrieving OAuth2PermissionGrants..."
        GetOAuth2PermissionGrants -FastMode:$fastQueryMode | ForEach-Object {
        #GetOAuth2PermissionGrants | ForEach-Object {
            $grant = $_
            GetAzureADServicePrincipal($grant.ClientId)
            if ($grant.Scope) {

                $grant.Scope.Split(" ") | Where-Object { $_ } | ForEach-Object {

                    $grantDetails =  [ordered]@{
                        "PermissionType" = "Delegated"
                        "AppId" = $script:AppId
                        "ClientObjectId" = $grant.ClientId
                        "ResourceObjectId" = $grant.ResourceId
                        "Permission" = $_
                        "ConsentType" = $grant.ConsentType
                        "PrincipalObjectId" = $grant.PrincipalId
                        "Homepage" = $script:homepage
                        "PublisherName" = $script:PublisherName
                        "ReplyUrls" = $null
                        "ExpiryTime" = $grant.ExpiryTime
                    }

                    if ($null -ne $ReplyUrls) {
                        $grantDetails["ReplyUrls"] = $script:ReplyUrls -join ', '
                    }

                    # Add properties for client and resource service principals
                    if ($ServicePrincipalProperties.Count -gt 0) {

                        $client = GetObjectByObjectId -ObjectId $grant.ClientId
                        $resource = GetObjectByObjectId -ObjectId $grant.ResourceId

                        $insertAtClient = 2
                        $insertAtResource = 3
                        foreach ($propertyName in $ServicePrincipalProperties) {
                            $grantDetails.Insert($insertAtClient++, "Client$propertyName", $client.$propertyName)
                            $insertAtResource++
                            $grantDetails.Insert($insertAtResource, "Resource$propertyName", $resource.$propertyName)
                            $insertAtResource ++
                        }
                    }

                    # Add properties for principal (will all be null if there's no principal)
                    if ($UserProperties.Count -gt 0) {

                        $principal = $empty
                        if ($grant.PrincipalId) {
                            $principal = GetObjectByObjectId -ObjectId $grant.PrincipalId
                        }

                        foreach ($propertyName in $UserProperties) {
                            $grantDetails["Principal$propertyName"] = $principal.$propertyName
                        }
                    }

                    New-Object PSObject -Property $grantDetails
                }
            }
        }
    }

    if ($ApplicationPermissions -or (-not ($DelegatedPermissions -or $ApplicationPermissions))) {

        # Iterate over all ServicePrincipal objects and get app permissions
        Write-Verbose "Retrieving AppRoleAssignments..."
        $script:ObjectByObjectClassId['ServicePrincipal'].GetEnumerator() | ForEach-Object { $i = 0 } {

            if ($ShowProgress) {
                Write-Progress -Activity "Retrieving application permissions..." `
                            -Status ("Checked {0}/{1} apps" -f $i++, $servicePrincipalCount) `
                            -PercentComplete (($i / $servicePrincipalCount) * 100)
            }

            $sp = $_.Value
            GetAzureADServicePrincipal($sp.ObjectId)

            Get-AzureADServiceAppRoleAssignedTo -ObjectId $sp.ObjectId -All $true `
            | Where-Object { $_.PrincipalType -eq "ServicePrincipal" } | ForEach-Object {
                $assignment = $_

                $resource = GetObjectByObjectId -ObjectId $assignment.ResourceId
                $appRole = $resource.AppRoles | Where-Object { $_.Id -eq $assignment.Id }

                $grantDetails =  [ordered]@{
                    "PermissionType" = "Application"
                    "AppId" = $null
                    "ClientObjectId" = $assignment.PrincipalId
                    "ResourceObjectId" = $assignment.ResourceId
                    "Permission" = $appRole.Value
                    "IsEnabled" = $null
                    "Description" = $null
                    "CreationTimestamp" = $null
                    "Homepage" = $script:homepage
                    "PublisherName" = $script:PublisherName
                    "ReplyUrls" = $null
                }

                 # Add the properties if they are available and not null or empty
                if ($null -ne $sp -and $sp.AppId) {
                    $grantDetails["AppId"] = $sp.AppId
                }

                if ($null -ne $ReplyUrls) {
                    $grantDetails["ReplyUrls"] = $script:ReplyUrls -join ', '
                }
            
                if ($null -ne $appRole -and $appRole.IsEnabled) {
                    $grantDetails["IsEnabled"] = $appRole.IsEnabled
                }
            
                if ($null -ne $appRole -and $appRole.Description) {
                    $grantDetails["Description"] = $appRole.Description
                }
            
                if ($null -ne $assignment -and $assignment.CreationTimestamp) {
                    $grantDetails["CreationTimestamp"] = $assignment.CreationTimestamp
                }

                # Add properties for client and resource service principals
                if ($ServicePrincipalProperties.Count -gt 0) {

                    $client = GetObjectByObjectId -ObjectId $assignment.PrincipalId                 

                    $insertAtClient = 2
                    $insertAtResource = 3
                    foreach ($propertyName in $ServicePrincipalProperties) {
                        $grantDetails.Insert($insertAtClient++, "Client$propertyName", $client.$propertyName)
                        $insertAtResource++
                        $grantDetails.Insert($insertAtResource, "Resource$propertyName", $resource.$propertyName)
                        $insertAtResource ++
                    }
                }
                
                New-Object PSObject -Property $grantDetails
            }
        }
    }
)
   
    $report | ConvertTo-Csv | Format-Table > $null
    $prop = $report.ForEach{ $_.PSObject.Properties.Name } | Select-Object -Unique
    $report | Select-Object $prop | Export-CSV -NoTypeInformation -Path "$OutputDir\$($date)-OAuthPermissions.csv" -Encoding $Encoding

    Write-LogFile -Message "Done, saving output to: $OutputDir\$($date)-OAuthPermissions.csv" -Color "Green"
}