Reporting Who Made License Assignments

Who Performed an Azure AD License Assignment?

After writing about how to detect underused (and expensive) licenses assigned to Azure AD accounts, I was asked if it was possible to report who assigned a license to accounts. It’s a good question that stumped me for a moment. There’s no obvious off-the-shelf indication of who assigned licenses to accounts in any Microsoft 365 administrative interface.

Azure AD Audit Data

License assignment is an Azure AD activity. It’s therefore possible to find information about these actions in the Azure AD audit log by searching for “Change user license” events. Unfortunately, these events only note that some sort of license assignment occurred. It doesn’t tell you what happened to licenses in terms of additions, removals, or disabling service plans in licenses. For that information, you need to find a matching “Update user” event where the license assignment detail is captured in the Modified Properties tab (Figure 1).

Azure AD license assignment details in the audit log
Figure 1: Azure AD license assignment details in the audit log

Unfortunately, the Get-MgAuditLogDirectoryAudit cmdlet doesn’t report the same level of detail about license assignments, so the Azure AD audit log isn’t a good source for reporting.

License Assignment Records in the Unified Audit Log

Azure AD is a source for the Office 365 (unified) audit log and the information ingested into the Office 365 audit log is more comprehensive albeit formatted in such a way that the data isn’t easy to fetch. However, we can find enough data to write a PowerShell script to create a basic report that contains enough information to at least give administrators some insight into who assigns licenses.

To create the report, the script:

  • Ran the Search-UnifiedAuditLog cmdlet to retrieve audit records for the Change user license and Update User actions.
  • Create separate arrays for both types of event.
  • For each Change user license event, see if there’s a matching Update user record. If one is found, extract the license assignment information from the record.
  • Report what’s been found.

Here’s the script to prove that the concept works:

# Azure AD license assignment script
$StartDate = (Get-Date).AddDays(-90)
$EndDate = (Get-Date).AddDays(1)
Write-Host "Searching for license assignment audit records"
[array]$Records = Search-UnifiedAuditLog -StartDate $StartDate -EndDate $EndDate -Formatted -ResultSize 5000 -Operations "Change user license", "Update User" -SessionCommand ReturnLargeSet
If (!($Records)) { Write-Host "No audit records found... exiting... " ; break}

Write-Host ("Processing {0} records" -f $Records.count)
$Records = $Records | Sort-Object {$_.CreationDate -as [datetime]} -Descending
[array]$LicenseUpdates = $Records | Where-Object {$_.Operations -eq "Change user license."}
[array]$UserUpdates = $Records | Where-Object {$_.Operations -eq "Update user."}

$Report = [System.Collections.Generic.List[Object]]::new()

ForEach ($L in $LicenseUpdates) {
  $NewLicenses = $Null; $OldLicenses = $Null; $OldSkuNames = $Null; $NewSkuNames = $Null
  $AuditData = $L.AuditData | ConvertFrom-Json
  $CreationDate = Get-Date($L.CreationDate) -format s
  [array]$Detail = $UserUpdates | Where-Object {$_.CreationDate -eq $CreationDate -and $_.UserIds -eq $L.UserIds}
  If ($Detail) { # Found a user update record
     [int]$i = 0
     $LicenseData = $Detail[0].AuditData | ConvertFrom-Json
     [array]$OldLicenses = $LicenseData.ModifiedProperties | Where {$_.Name -eq 'AssignedLicense'} | Select-Object -ExpandProperty OldValue | Convertfrom-Json
     If ($OldLicenses) {
        [array]$OldSkuNames = $Null
        ForEach ($OSku in $OldLicenses) {
           $OldSkuName = $OldLicenses[$i].Substring(($OldLicenses[$i].IndexOf("=")+1), ($OldLicenses[$i].IndexOf(",")-$OldLicenses[$i].IndexOf("="))-1)
           $OldSkuNames += $OldSkuName
      $OldSkuNames = $OldSkuNames -join ", "
    [array]$NewLicenses = $LicenseData.ModifiedProperties | Where {$_.Name -eq 'AssignedLicense'} | Select-Object -ExpandProperty NewValue | Convertfrom-Json
    If ($NewLicenses) {
        $i = 0
        [array]$NewSkuNames = $Null
        ForEach ($N in $NewLicenses) {
           $NewSkuName = $NewLicenses[$i].Substring(($NewLicenses[$i].IndexOf("=")+1), ($NewLicenses[$i].IndexOf(",")-$NewLicenses[$i].IndexOf("="))-1)
           $NewSkuNames += $NewSkuName
      $NewSkuNames = $NewSkuNames -join ", "

  } # end if
  $ReportLine   = [PSCustomObject] @{ 
     Operation      = $AuditData.Operation
     Timestamp      = Get-Date($AuditData.CreationTime) -format g
     'Assigned by'  = $AuditData.UserId
     'Assigned to'  = $AuditData.ObjectId 
     'Old SKU'      = $OldSkuNames
     'New SKU'      = $NewSkuNames
     'New licenses' = $NewLicenses
     'Old licenses' = $OldLicenses

$Report = $Report | Sort-Object {$_.TimeStamp -as [datetime]} 
$Report | Out-GridView

The output is sparse (Figure 2) but I reckon it is sufficient to understand what happens when a license assignment occurred. Events without any license detail appear to be when an administrator removes a license from an account or a service plan from a license.

Azure AD license assignment data extracted from the Office 365 audit log
Figure 2: License assignment data extracted from the Office 365 audit log

I didn’t bother attempting to parse out the license detail. The information returned by Azure AD includes all the licenses assigned to an account, so you’d end up with something like this for an account with three licenses. Splitting the individual licenses and disabled service plans out from this information is an exercise for the reader.


Principal Proved

In any case, the answer to the question is that it’s possible to track and report Azure AD license assignments by using the audit log to extract events relating to these actions and parsing the information in the events. The resulting output might not be pretty (but could be cleaned up), but it’s enough to prove the principal.

Learn how to exploit the data available to Microsoft 365 tenant administrators through the Office 365 for IT Pros eBook. We love figuring out how things work.

9 Replies to “Reporting Who Made License Assignments”

  1. Search-UnifiedAuditLog cmdlet only available for me after connecting to exchange online. However, that doesn’t pull any information for license assigment only the changes made on exchange. Any suggestions?

    1. Search-UnifiedAuditLog is a cmdlet in the Exchange Online management module, hence why you must connect to EXO before you can run it.

      Azure AD sends information about license assignments (all assignments) to the log. It tracks more than changes made on Exchange.

  2. Hi! I am testing this script but when I execute it is returning time out after some time executing. 🙁

      1. My timeou is here
        [array]$Records = Search-UnifiedAuditLog -StartDate $StartDate -EndDate $EndDate -Formatted -ResultSize 5000 -Operations “Change user license”, “Update User”
        Write-ErrorMessage : The operation has timed out

      2. Add -SessionCommand ReturnLargeSet to the command and see if it works. There have been some problems with the cmdlet recently and this parameter can help. Remember to sort the returned items afterwards. I’ve updated the code to reflect these chamges.

    1. The code ran and found license update events for me. If you mean the old and new license data, Microsoft seems to have changed the format of the captured data since I wrote the script. It’s just a matter of extracting the right data from the $AuditData variable. I don’t have time to look at it now…

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.