Backup AzureAD Conditional Access Policies v2 – Graph API

AzureAD Powershell is planned for deprecation (link) so I redesigned my Conditional Access Policy Backup solution originally posted here. This v2 edition uses an AzureAD app registration for unattended access (eg. scheduled script) and the Microsoft Graph API (but not the Microsoft Graph PowerShell module).

The idea and the logic is the same as in the previous version: Conditional Access Policies can be exported as JSON and if the policy in AzureAD differs from or latest version then we need an “incremental” backup.

TL;DR
– Create an app registration with Policy.Read.All Application permission (and grant admin consent) and a client secret (don’t forget to update the secret in the script before it expires)
– Copy the script below, fill the variables $tenantID, $appID,$appSecret (and $backupDir if you want it elsewhere)
– Schedule the script (based on my testing, SYSTEM/LOCAL SERVICE can’t be used for REST calls, so I’m using a user account for this purpose – to be fixed)
– If you need a “full backup” you can run the function Backup-AADCAs without the -ChangedOnly parameter

AzureAD App registration

In AzureAD go to App registrations and click on New registration:

Give the app a name and click on Register:

On the app registration page navigate to API permissions – Add a permission – Microsoft Graph – Application permissions

Add Policy.Read.All permission

You can remove the default User.Read permission, then grant admin consent for Policy.Read.All

Navigate to Certificates & secrets – New client secret – create a new secret (don’t forget to refresh the secret in the script before it expires)

Copy the client secret (use the copy button) and insert it in the $appSecret variable in the script

Head back to Overview, copy the tenant ID to $tenantID and application (client) ID to $appID

Schedule the scipt according to your needs.

Script

$backupDir = "C:\AAD_CA_Backup"

[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12

$tenantID = '' #tenantID
$appID = ''#appID
$appSecret = '' #appSecret
$scope = 'https://graph.microsoft.com/.default'
$oAuthUri = "https://login.microsoftonline.com/$TenantId/oauth2/v2.0/token"
$body = [Ordered] @{
    scope = "$scope"
    client_id = "$appId"
    client_secret = "$appSecret"
    grant_type = 'client_credentials'
}
$response = Invoke-RestMethod -Method Post -Uri $oAuthUri -Body $body -ErrorAction Stop
$aadToken = $response.access_token

#Conditional Access policies web request body
$url = 'https://graph.microsoft.com/v1.0/identity/conditionalAccess/policies'
$headers = @{ 
    'Content-Type' = 'application/json'
    'Accept' = 'application/json'
    'Authorization' = "Bearer $aadToken" 
}

function Import-AADCABackups {
    gci -File -Recurse $backupDir -Include *.json | % {
        [pscustomobject]@{
         ID = ($_.Name.Split("_"))[0]
         Version =[datetime]::ParseExact( ($_.BaseName.Split("_"))[1], 'yyyyMMddHHmm', $null)
         JSON = Get-Content $_.FullName #| ConvertFrom-Json
         Name = (Get-item $_.Directory).Name
         }
        }
}

function Backup-AADCAs {
    Param(
    [Parameter(Mandatory=$false)]
    [switch]$ChangedOnly
    )
    $import_CABackups = Import-AADCABackups

    $AAD_CAs = Invoke-WebRequest -Method Get -Uri $url -Headers $headers -ErrorAction Stop | ConvertFrom-Json | % {$_.value}
    $strDate = Get-date -Format yyyyMMddHHmm

    foreach ($CA in $AAD_CAs){
        #create backup directory if it does not exist
        if (!(Test-Path "$backupDir\$($CA.displayname)")){New-item -ItemType Directory -Path "$backupDir\$($CA.displayname)" >> $null }
        
        #load JSON
        $CA_JSON = $CA | ConvertTo-Json -Depth 6 -Compress
        
        #Export changes only  
        if ($ChangedOnly){
            $import_CABackup_latest_JSON = ($import_CABackups | where({$_.ID -eq $CA.id}) | sort version | select -Last 1).JSON
            #New CA
            if ($import_CABackup_latest_JSON -eq $null){
                Write-Host "New policy found: $($CA.DisplayName)" -ForegroundColor Green
                Out-File -InputObject $CA_JSON -Encoding utf8 -FilePath "$backupDir\$($CA.displayname)\$($ca.id)_$strdate.json"
                }
            #Difference found
            if (([bool]$import_CABackup_latest_JSON) -and ($import_CABackup_latest_JSON -ne $CA_JSON)){
                Write-Host "Found difference for $($CA.DisplayName)" -ForegroundColor Yellow
                Out-File -InputObject $CA_JSON -Encoding utf8 -FilePath "$backupDir\$($CA.displayname)\$($ca.id)_$strdate.json"
                }
            #No difference found
            if (([bool]$import_CABackup_latest_JSON) -and ($import_CABackup_latest_JSON -eq $CA_JSON)){
                Write-Host "No difference found for $($CA.DisplayName)" -ForegroundColor Cyan
                }

        #Export all
        }else{
            Out-File -InputObject $CA_JSON -Encoding utf8 -FilePath "$backupDir\$($CA.displayname)\$($ca.id)_$strdate.json"
         }
    }
    #Deleted CA
    $import_CABackups | ? {$_.id -notin $AAD_CAs.id} | % {
                Write-Host "Policy deleted in AzureAD: $($_.Name)" -ForegroundColor Red
                }

 }


Backup-AADCAs -ChangedOnly

Leave a Reply