Remove all Office 365 licenses for a group of users from CSV file via Graph

As Microsoft has committed to deprecating the good old MSOnline and the not so good Azure AD PowerShell modules next year, I’ve started updating some of the scripts and code samples I’ve shared on this blog to use the latest and greatest. Which in this case means using the Graph API directly, or the “Microsoft Graph” (MG) wrapper module, also known as Microsoft Graph PowerShell SDK.

We’re starting with the “remove all licenses from a set of users” scenario, which we covered previously here and here. The premise remains the same – you’ve prepared a list of users, either via CSV file or by generating the list dynamically via filters and such, and you need to remove all licenses assigned to them in a single step. Because the Graph has not been designed with “ease of use” in mind, make sure to use a proper identifier for the user, such as objectID or UserPrincipalName.

Once you have the list prepared, all you need to do is handle connectivity and iterate over each user to remove any corresponding licenses. There are multiple different factors to consider when talking about the connectivity part, such as whether you plan to run this code in the context of a user or an app (service principal), the console host and so on. Discussing all these details goes well beyond the scope of the article, but of course feel free to replace the connectivity part with your own code. At the very least, make sure to explore the different parameters the Connect-MgGraph cmdlet offers.

And since we’re talking about Graph API access, do note that you might need to consent to the required permissions. User.ReadWrite.All is what the samples below use, again feel free to change it as required in case additional operations will be needed.

Putting the connectivity part aside, the script is fairly simple. We iterate over the list of users and run the Get-MgUser cmdlet against each entry, just to make sure we can find a valid object within the directory. If a matching user is found, we then run the Get-MgUserLicenseDetail cmdlet to obtain the list of assigned licenses, then proceed to remove each of them. Unlike the previous versions of the script, we now run a separate removal operation for each SKU, just so we can explicitly flag the ones assigned by group-based licensing. Rinse and repeat for any remaining users.

Connect-MgGraph -Tenant tenant.onmicrosoft.com -Scopes User.ReadWrite.All

#Import the list of users, or generate it dynamically as needed
$users = Import-Csv .\Users-to-disable.csv
#$users = Get-MgUser -Filter "Department eq 'Marketing'"

foreach ($user in $users) {
Write-Verbose "Processing licenses for user $($user.UserPrincipalName)"
try { $user = Get-MgUser -UserId $user.UserPrincipalName -ErrorAction Stop }
catch { Write-Verbose "User $($user.UserPrincipalName) not found, skipping..." ; continue }

$SKUs = @(Get-MgUserLicenseDetail -UserId $user.id)
if (!$SKUs) { Write-Verbose "No Licenses found for user $($user.UserPrincipalName), skipping..." ; continue }

foreach ($SKU in $SKUs) {
Write-Verbose "Removing license $($SKU.SkuPartNumber) from user $($user.UserPrincipalName)"
try {
Set-MgUserLicense -UserId $user.id -AddLicenses @() -RemoveLicenses $Sku.SkuId -ErrorAction Stop #-WhatIf
}
catch {
if ($_.Exception.Message -eq "User license is inherited from a group membership and it cannot be removed directly from the user.") {
Write-Verbose "License $($SKU.SkuPartNumber) is assigned via the group-based licensing feature, either remove the user from the group or unassign the group license, as needed."
continue
}
else {$_ | fl * -Force; continue} #catch-all for any unhandled errors
}}
}

If you prefer the script in a Gist form, click here.

Since I’m not a big fan of the lazy approach Microsoft has chosen for the MG module, I might as well just do the tasks by leveraging Graph API calls directly. Sure, the connectivity part gets a bit more complicated, and some additional queries might be needed, but you might as well use this as a learning experience. And you don’t have any dependencies on the MG module. You can find the “raw” Graph-based version of the script over at GitHub.

As usual, make sure to adjust the authentication details, or replace the code with your preferred function. Some additional error checking wouldn’t hurt either, but that’s all you get for free 🙂

The script can easily be adjusted to remove only specific licenses, or assign licenses for that matter, as well as any permutations of these operations. But that’s for another article.

5 thoughts on “Remove all Office 365 licenses for a group of users from CSV file via Graph

  1. Jason Gallas says:

    I get the following error when running this. Is there a permission issue or something?

    PSMessageDetails :
    Exception : System.Exception: [Request_BadRequest] : License assignment failed because service plan 61a2665f-1873-488c-9199-c3d0bc213fdf depends on the service plan(s) 2da8e897-7791-486b-b08f-cc63c8129df7
    TargetObject : { UserId = a25990ab-ec7a-4b9b-b1ed-1c574824e601, body = Microsoft.Graph.PowerShell.Models.Components103UmuuRequestbodiesAssignlicenserequestbodyContentApplicationJsonSchema }
    CategoryInfo : InvalidOperation: ({ UserId = a259…ionJsonSchema }:f__AnonymousType6`2) [Set-MgUserLicense_AssignExpanded], Exception
    FullyQualifiedErrorId : Request_BadRequest,Microsoft.Graph.PowerShell.Cmdlets.SetMgUserLicense_AssignExpanded
    ErrorDetails : License assignment failed because service plan 61a2665f-1873-488c-9199-c3d0bc213fdf depends on the service plan(s) 2da8e897-7791-486b-b08f-cc63c8129df7

    Status: 400 (BadRequest)
    ErrorCode: Request_BadRequest
    Date: 2023-08-03T15:24:42

    Headers:
    Transfer-Encoding : chunked
    Vary : Accept-Encoding
    Strict-Transport-Security : max-age=31536000
    request-id : 36b9bb8c-5847-4218-b1fc-420f5a8a76d8
    client-request-id : 8e22e034-14b4-4e45-8960-d546b5642854
    x-ms-ags-diagnostic : {“ServerInfo”:{“DataCenter”:”West US”,”Slice”:”E”,”Ring”:”4″,”ScaleUnit”:”002″,”RoleInstance”:”BY3PEPF00015E00″}}
    x-ms-resource-unit : 1
    Cache-Control : no-cache
    Date : Thu, 03 Aug 2023 15:24:42 GMT

    Reply
  2. Corey says:

    Hi – This is exactly what I was looking for. However, when I run it, it authenticates for Graph, gives the message “Welcome To Microsoft Graph!” and then does nothing else.
    Has the functionality changed since 2021?

    Reply
    1. Vasil Michev says:

      Should still work. Make sure you’re providing a CSV file with a column called “UserPrincipalName” and values populated with the desired users’ UPN or ObjectIDs.

      Reply

Leave a Reply

Your email address will not be published. Required fields are marked *

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