Remove user from all Microsoft 365 Groups (updated 2023 version of the script)

The script to remove a user, or a set of users from all groups they’re currently a member of has been one of my most popular contributions. Due to the deprecation of older PowerShell modules and connectivity methods across the service, there are potential issues with running the old script. In this article we will introduce an updated version of the script, switching from using Remote PowerShell cmdlets and the Azure AD module to the ExO V3 PowerShell module and the latest Graph API SDK for PowerShell.¬† Those requirements are now being enforced by few additional #Requires statements which you can find on top of the file, should you need to make some amendments.

Apart from ensuring the script can continue running even after Microsoft pulls the plug on basic authentication, ExO RemotePowerShell connectivity and the Azure AD module, I’ve also taken this opportunity to make some minor updates to the script, such as outputing the list of removals to a CSV file, more verbose output and a switch to prevent output to the console window. We will talk about these changes as we go over the script structure. So, grab your copy of the script from my GitHub repo, and let’s get going.

The internal structure of the script has not changed. The removal task is still performed by a single function, Remove-UserFromAllGroups, which you can dot-source for your needs in other scripts. The only other function, Check-Connectivity, is used to handle all the connectivity and permission checks and has received a massive overhaul in this version. Since we’re no longer depending on the Azure AD module to handle removals from Azure AD security group, but using the Graph SDK for PowerShell instead, the script will now try to detect an existing connection via the Get-MgContext cmdlet, and also check whether the required permissions have been granted. Speaking of permissions, you will need the Group.ReadWrite.All scope to ensure smooth operation of the Remove-MgGroupMemberByRef cmdlet. And since we’re also poking around with the Get-MgUserMemberOf cmdlet in order to obtain a list of Azure AD security groups for the user, you will also need the Directory.Read.All scope. If the permissions are not already granted, the script will run the Connect-MgGraph with the corresponding scope parameter, prompting you to consent.

On Exchange Online’s side of things, connectivity is checked via the Get-ConnectionInformation cmdlet, ensuring we can reuse existing sessions. As a fail safe, the script will try to fetch a single recipient object, and if it fails, will prompt you to connect via the Connect-ExchangeOnline cmdlet. In order to minimize the ExO module’s footprint, only the following three cmdlets will be loaded: Get-EXORecipient, Remove-DistributionGroupMember, Remove-UnifiedGroupLinks. The function will not verify whether you have sufficient permissions to run the two Remove-* cmdlets, so make sure you are connecting with a user/service principal that can actually run them. Otherwise, prepare to get flooded by funky error messages ūüôā

The rest of the script is dedicated to the Remove-UserFromAllGroups function, the one responsible for handling the input and removing each matched user from any and all groups. The cmdlet accepts several parameters, handles pipeline input and can be used in dot-sourced scenarios, if you prefer. The list of parameters is as follows:

  • Identity – a list of user objects, designated by alias, display name, distinguished name, GUID, legacy Exchange DN, email address or UserPrincipalName values. Multiple values can be provided, separated by commas or in array form. For each value, the script will try to find a matching user object via the Get-EXORecipient cmdlet. If no matches are found, the value will be dropped and the script will continue to the next one.
  • IncludeOffice365Groups – use this parameter to force the removal from any Microsoft 365 Groups. By default, the script will only remove users from Distribution groups and mail-enabled security groups.
  • IncludeAADSecurityGroups – use this parameter to force the removal from any Azure AD security groups.
  • Quiet – as the script now generates some output, both in the console and to a CSV file, containing a list of all users and the corresponding groups they were removed from, you can use the -Quiet parameter to suppress the console output.
  • Verbose – use this parameter to generate additional details on the cmdlet progress. A lot of noise will be generated by the #Requires statements and the Get-EXORecipient cmdlet, so be warned.
  • WhatIf – probably the most important switch, as it will allow you to preview the list of groups each user will be removed from, without actually committing the changes. Effects will vary depending on the type of group/module used, but hopefully that will improve in time, as Microsoft gets their **** together…

As the script is basically a wrapper around the Remove-UserFromAllGroups cmdlet, the list of parameters above represents the set of parameters accepted by the script itself. Should you want to use the cmdlet in your own scripts, you can dot source the script as follows:

. .\Remove_User_All_GroupsV2.ps1 -WhatIf

This in turn will allow you to run the cmdlet directly:

#Remove a user from all DGs/MESGs
Remove-UserFromAllGroups userA

#Remove a user from all types of groups
Remove-UserFromAllGroups userA@domain.com -IncludeAADSecurityGroups -IncludeOffice365Groups

#Remove a set of users, or use the pipeline
"userA","userB" | Remove-UserFromAllGroups -WhatIf

Get-EXORecipient -Filter {CountryOrRegion -ne "US"} -RecipientType UserMailbox,MailUser  | Remove-UserFromAllGroups -IncludeAADSecurityGroups -IncludeOffice365Groups

Anyway, back to detailing what the script/function does. After validating the set of user objects provided via the –Identity parameter, the script will proceed with obtaining a list of groups the given user is a member of, depending on the rest of the parameters provided. By default, only distribution groups and mail-enabled security groups are included. Use the -IncludeOffice365Groups switch to also include Microsoft 365 Groups, and/or -IncludeAADSecurityGroups the switch to include Azure AD Security groups. Dynamic Distribution groups are not included, and while M365 groups/AAD security groups with dynamic membership will be included in the processing, the corresponding removal cmdlet will thrown an error. To remove the user(s) from any such objects, you will have to adjust the recipient filter/membership query, outside of the script.

The script includes a lot of error handling, but it’s more than likely that not every possible exception is handled. And since we cannot cover dynamic membership scenarios anyway, I’d strongly advice you to recheck the set of groups the given user(s) is a member of after running the script, in order to avoid nasty surprises. In addition, while testing the script I run into few scenarios that I consider bugs on Microsoft side, which can again lead to unexpected results. But more on that in a separate article.

Here is probably a good place to re-iterate on the different types of group objects within the service, and why we need different methods to handle them. As neither the Exchange Online cmdlet nor the Graph API covers all possible group objects a given user can be a member of, we have no choice but to use both PowerShell modules. If we only use the Exchange methods, no Azure AD security groups will be covered, and as those can be used to delegate access to various functionalities, including admin roles, you’re effectively accepting additional risks by neglecting to include them. If we choose to use the Azure AD/Graph methods instead, no changes can be made to DG/MESG membership, as those objects are authored in ExODS, so we end up in a similar situation.

And again, even when including both modules, not all scenarios are handled. Apart from not being able to remove users from any groups with dynamic membership, few other edge cases stand out. If an Azure AD group is created as role-assignable one, membership operations against it can only be performed by principals holding the RoleManagement.ReadWrite.Directory permission. My personal view here is that such permissions should never be granted to third-party tools (or a random script you downloaded from the internet), so I’ve chosen not to address this scenario. Of course, you can add a line or two and handle it yourself. Another scenario not currently addressed is when the user is the last member/owner of a Microsoft 365 Group. I’ll likely add a switch to handle such cases in a future version of the script.

OK, enough talk. Here’s how to run the script (after you’ve downloaded it from my GitHub repo). The first example will remove a given user from all DGs and MESGs. The second one will remove it from all supported group types. The third one represents a bulk scenario – a set of users is provided, and to the -WhatIf switch is used to preview the changes, before committing to the removal. While the examples below use the Alias value as input, I’d strongly encourage you to provide the UPN instead.

#Remove user from all DGs/MESGs
.\Remove_User_All_GroupsV2.ps1 -Identity ChristieC

#Remove user from all supported group types
.\Remove_User_All_GroupsV2.ps1 -Identity ChristieC -IncludeAADSecurityGroups -IncludeOffice365Groups

#Remove a set of users from all supported group types
.\Remove_User_All_GroupsV2.ps1 -Identity ChristieC,DebraB,PattiF -WhatIf -IncludeAADSecurityGroups -IncludeOffice365Groups

Here are also some examples of the output (new addition to the script). By default, output will be written to the console and a CSV file within the script directory, with each line representing the user object and a group he is removed from. Note that the User column will reflect the value(s) you provided for the -Identity parameter, which should make it easier to zero in on specific user(s). Lastly, you can suppress the console output by using the -Quiet switch.

UserRemoveAllGroupsRemove user example outputAnd that’s all I can think of about the script. In summary, we now have an updated version of the “remove user from all Microsoft 365 groups” script, which should work fine even after the coming deprecations in Exchange Online and Azure AD.

27 thoughts on “Remove user from all Microsoft 365 Groups (updated 2023 version of the script)

  1. Jan BartoŇ° says:

    Hello,

    can I load csv with disabled users to your script?
    It works fine for me for single user.

    Thank you,

    Jan

    Reply
    1. Vasil Michev says:

      The script accepts pipeline input, so you can just do this:

      $csv = Import-Csv .\test.csv
      .\Remove_User_All_GroupsV2.ps1 -Identity $csv.UserPrincipalName -WhatIf

      where the test.csv file has a column UserPrincipalName to designate each user.

      Reply
  2. Schelly says:

    Oops, sorry, I pasted the wrong error! Here’s what I’m getting:
    VERBOSE: Obtaining security group list for user “sbtest@rsimail.com”…
    Get-MgUserMemberOf_List1: C:\scripts\offboarding\removeFromGroups.ps1:190
    Line |
    190 | … gUserMemberOf -UserId $($user.value.ExternalDirectoryObjectId) -All Р…
    | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    | The specified filter to the reference property query is currently not supported.
    VERBOSE: No matching security groups found for “sbtest@rsimail.com”, skipping…

    Reply
    1. Vasil Michev says:

      Can you try updating the module version, 1.9.2 is quite old by now. The script was tested with 1.19.0 and above and should require this version as minimum, perhaps the #Required statement doesn’t trigger properly.

      Reply
      1. Schelly says:

        Thank you, Vasil! Updating did the trick. Many, many thanks!

        Reply
  3. Schelly says:

    Thank you for this beautiful script! For some reason, I’m getting an error when I try to use the -IncludeAADSecurityGroups swtich: VERBOSE: Obtaining security group list for user “sbtester2@rsimail.com”…
    Get-MgUserMemberOf: C:\scripts\offboarding\removeFromGroups.ps1:190
    Line |
    190 | $GroupsAD = Get-MgUserMemberOf -UserId $_($user.value …
    | ~~
    | Cannot bind argument to parameter ‘UserId’ because it is an empty string.
    VERBOSE: No matching security groups found for “sbtest@mail.com”, skipping…

    Everything else works fine. I’m using 1.9.2 MS Graph inside PS 7.3.5. Any ideas?

    Reply
  4. Rodriguez says:

    It was working fine, now I am getting the error:

    Get-MgUserMemberOf : Resource ‘My_user_ID_edited_here’ does not exist or one of its queried reference-property objects are not present.
    At C:\Users\myname\company\IT-Team – System\PowerShell Scripts\Offboarding.ps1:152 char:58

    Reply
  5. James says:

    Hi Vasil,

    Attempting to use the following:
    .\Remove_User_All_GroupsV2.ps1 -Identity account1@domain.com -IncludeAADSecurityGroups -IncludeOffice365Groups

    This account is unlicenced, however appears when using Get-User:
    Name RecipientType
    —- ————-
    account1 User

    I am getting the following error:
    Write-ErrorMessage : Ex6F9304|Microsoft.Exchange.Configuration.Tasks.ManagementObjectNotFoundException|The operation
    couldn’t be performed because object ‘account1@domain.com’ couldn’t be found on
    ‘xxxxxxxxxxxxxxxxxxx.PROD.OUTLOOK.COM’.
    At C:\Users\*\AppData\Local\Temp\tmpEXO_zhxrnc5n.bya\tmpEXO_zhxrnc5n.bya.psm1:1120 char:13
    + Write-ErrorMessage $ErrorObject
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : NotSpecified: (:) [get-User], ManagementObjectNotFoundException
    + FullyQualifiedErrorId : [Server=VI1PR08MB4029,RequestId=18e5511c-7658-fdeb-ea96-ed2f6dd5deca,TimeStamp=Wed, 10 M
    ay 2023 15:35:56 GMT],Write-ErrorMessage

    ERROR: No matching users found for “account1@domain.com”, check the parameter values.

    Are you able to provide any assistance?

    Many thanks.

    Reply
    1. Vasil Michev says:

      Pffft, there’s an annoying issue with -RecipientType parameter, it actually filters based on the RecipientTypeDetails value, screwing the results.
      I’ve updated the script to use -Filter instead, try the new version.

      Reply
      1. James says:

        Hi Vasil,

        Thank you, just to confirm this was successful and I was able to remove all disabled users including those unlicenced from the respective groups.

        Kind regards,
        James

        Reply
  6. Ross says:

    Just commenting to say thanks and this is an awesome script.

    Reply
  7. Tristan says:

    Thanks for the quick reply. I am running the latest script but am still getting an error of “The operation couldn’t be performed because object ‘username’ couldn’t be found.” So it still seems to be looking for a mailbox for the user when it doesn’t have one.

    Unfortunately, I have 100s of users in this state, and my company’s process deletes the M365 license before I would have a chance to run your script.

    Reply
  8. Sam says:

    I’ve noticed this will only remove licensed users from groups. If they don’t have a license it won’t work. Do you know if that’s the case?

    Reply
    1. Vasil Michev says:

      The user must be a valid Exchange recipient, otherwise it will fail the Get-ExORecipient cmdlet. You can change that with Get-User instead, I’ll update the script when I get some free time.

      Reply
      1. Tristin says:

        Hi Vasil, great script, does everything I need on users for group removal. But…we disable our users and remove their M365 licenses, so I, too, was hoping you could offer an alternative version using Get-User instead of Get-EXORecipient.

        Reply
        1. Vasil Michev says:

          The updated version of the script already uses Get-User, try it. Or you can simply run the script first, then remove the license ūüôā

        2. Tristin says:

          Thanks for the quick reply. Using the latest script (from two weeks ago), I get an object not found error. This user exists in Azure AD, is disabled, but has no license.

          Write-ErrorMessage : Ex6F9304|Microsoft.Exchange.Configuration.Tasks.ManagementObjectNotFoundException|The operation couldn’t be performed because object ‘bsimpson’ couldn’t be found on ‘REDACTED.REDACTED.PROD.OUTLOOK.COM’.
          At C:\Users\tristan\AppData\Local\Temp\tmpEXO_0kot2gok.5qy\tmpEXO_0kot2gok.5qy.psm1:1120 char:13
          + Write-ErrorMessage $ErrorObject
          + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
          + CategoryInfo : NotSpecified: (:) [get-User], ManagementObjectNotFoundException
          + FullyQualifiedErrorId : [Server=REDACTED,RequestId=REDACTED,TimeStamp=Wed, 12 Apr 2023 18:55:16 GMT],Write-ErrorMessage

          ERROR: No matching users found for “bsimpson”, check the parameter values.

        3. Vasil Michev says:

          Can you see the user via the Get-User cmdlet? Afaik every Azure AD user should be covered by this cmdlet, regardless of whether they’re licensed for Exchange Online.

  9. Sameer Chopra says:

    I’m getting an error stating:

    VERBOSE: Checking connectivity to Exchange Online PowerShell…
    VERBOSE: Parsing the Identity parameter…
    VERBOSE: Running Get-Recipient
    Get-EXORecipient : Error while querying REST service. HttpStatusCode=404 ErrorMessage=

    Not sure why it’s getting an error with the rest service returning a 404. Any idea?

    Reply
    1. Vasil Michev says:

      Not sure, what’s the full error message? What happens if you run Get-ExORecipient manually against the user? The user must be a valid Exchange recipient, otherwise the cmdlet will fail.

      Reply
  10. Jacob says:

    Do you happen to have an example of how you’d setup an array value and then pass it into the command line?

    I have tried taking multiple UPN adding them to $users and then within the command line “.\Remove_User_All_GroupsV2.ps1 -Identity $users -WhatIf -IncludeAADSecurityGroups -IncludeOffice365Groups”

    Reply
    1. Vasil Michev says:

      What’s in the $users variable?

      This will work:

      $users = @("user1@domain.com","user2@domain.com")
      .\Remove_User_All_GroupsV2.ps1 -Identity $users -WhatIf

      Or when importing from CSV:

      $csv = Import-Csv .\testtttt.csv
      .\Remove_User_All_GroupsV2.ps1 -Identity $csv.UserPrincipalName -WhatIf
      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.