PowerShell – nullify extension attributes for all users in AAD using Graph API

Extensions attributes in AAD are a great way to store any auxiliary information that does not belong in any other user properties. The only sad part, it’s only 15 of them and they are text fields. If you ever need to clear some or all of them, the script below will be a great help!

Benefits of the script:

  • makes changes in batches – only takes seconds to run;
  • dry or wet run depending on the switch (see the important note below).

This script uses an App-only access rather than Delegated access unlike my other script ( https://365basics.com/powershell-find-and-update-a-string-value-within-all-dynamic-group-membership-rules/ ). If you would like to learn more – https://learn.microsoft.com/en-us/graph/auth/auth-concepts

Important! This line (#116) of code is responsible for either dry or wet run of the script. I left it commented on purpose and by default.

# $UserAdjustments = Invoke-RestMethod @Parameters

Technically, you can adjust this script to clear and/or populate any user attributes.

# Variables (adjust to yours)
$TenantId = 'ab0595b5-8242-202a-8ad2-7c8c98346a1d'
$ClientId = '3f35cc15-0eb3-421c-b214-445b8f438d0e'
$ClientSecret = 'F_Q8Q~cdfyuGJJD234f2YqCdVAkh8q0l6i~Q2tyR'

# Start timer
$elapsed = [System.Diagnostics.Stopwatch]::StartNew()

# Body data needed for the token request
$Body = @{
    'tenant' = $TenantId
    'client_id' = $ClientId
    'scope' = 'https://graph.microsoft.com/.default'
    'client_secret' = $ClientSecret
    'grant_type' = 'client_credentials'
}

# Parameters for the token request
$Params = @{
    'Uri' = "https://login.microsoftonline.com/$TenantId/oauth2/v2.0/token"
    'Method' = 'Post'
    'Body' = $Body
    'ContentType' = 'application/x-www-form-urlencoded'
}

# Get token
$AuthResponse = Invoke-RestMethod @Params

# Get users and add to collection
$Headers = @{
    'Authorization' = "Bearer $($AuthResponse.access_token)"
}
$Result = Invoke-RestMethod -Uri 'https://graph.microsoft.com/v1.0/users?$select=userType,userPrincipalName' -Headers $Headers
$AllUsers = @()
$AllUsers+=$Result.value

# Get more users and add to collection
while($Result.'@odata.nextLink' -ne $null) {
    $Result = Invoke-RestMethod -Method 'Get' -Uri $Result.'@odata.nextLink' -Headers $Headers
    $AllUsers+=$Result.value
}

# Stop timer
Write-Host "Time to create collection: $($elapsed.Elapsed.ToString())"

# Start timer
$elapsed = [System.Diagnostics.Stopwatch]::StartNew()

# Group users into set batches
$Users = $AllUsers | Where-Object {$_.userType -ne 'Guest'}
$Counter = [pscustomobject] @{ Value = 0 }
$GroupSize = 20
$GroupsOfUsers = $Users | Group-Object -Property { [math]::Floor($Counter.Value++ / $GroupSize) }

# Process each batch of users
ForEach ($Batch in $GroupsOfUsers) {
    # Collection for batch requests
    $myBatchRequests = @()

    Write-host `n
    $Groups = $Batch.Group
    ForEach ($Group in $Groups) {
        Write-host "Processing:" -NoNewline -ForegroundColor Green
        $Group.userPrincipalName
        
        # Calculate url for each group of users
        $url = "/users/"+$Group.userPrincipalName+"/onPremisesExtensionAttributes/"
    
        # Body
        $myRequest = @{ 
            id     = $Groups.indexof($Group)
            method = "PATCH"
            url    = $url
            body = @{
                "extensionAttribute1" = ""
                "extensionAttribute2" = ""
                "extensionAttribute3" = ""
                "extensionAttribute4" = ""
                "extensionAttribute5" = ""
                "extensionAttribute6" = ""
                "extensionAttribute7" = ""
                "extensionAttribute8" = ""
                "extensionAttribute9" = ""
                "extensionAttribute10" = ""
                "extensionAttribute11" = ""
                "extensionAttribute12" = ""
                "extensionAttribute13" = ""
                "extensionAttribute14" = ""
                "extensionAttribute15" = ""
            }
            headers = @{
                "Content-Type" = "application/json"
            }   
        } 
        $myBatchRequests += $myRequest
    }
    # Combine batch requests for Graph API batching
    $allBatchRequests = @{ 
        requests = $myBatchRequests
    }

    # Convert body to JSON for Graph API
    $batchBody = $allBatchRequests | ConvertTo-Json -Depth 5
    $batchBody

    # Parameters for Graph API POST request
    $Parameters = @{
        ContentType = 'application/json'
        Method = 'POST'
        Body = $batchBody
        Uri = 'https://graph.microsoft.com/v1.0/$batch'
        Headers = @{ Authorization = "Bearer " + $($AuthResponse.access_token) }
    }
    
    # Execute Graph API POST request
    # $UserAdjustments = Invoke-RestMethod @Parameters
    Write-host $UserAdjustments.responses.body.error.message -ForegroundColor Red

    $myBatchRequests = $null
}

# Stop timer
Write-Host "Time spent on requests: $($elapsed.Elapsed.ToString())"

RESULT

This Post Has 2 Comments

  1. Daniel

    Now the question is, how do we null out attrubutes like Mail, for a cloud only user that isnt licensed.

    I keep getting “invalid value”.

    1. Paul Bludov

      Hi Daniel,
      Have you adjusted that line? – because extension attributes sit underneath onPremisesExtensionAttributes property while Mail does not.
      $url = “/users/”+$Group.userPrincipalName+”/onPremisesExtensionAttributes/”

Leave a Reply