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.
And 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.
Hi Vasil,
Thank you for this amazing script. I seem to be running into an error when I attempt to run this script:
System.Exception: |Microsoft.Exchange.Net.AAD.AADException|We failed to update the group mailbox. Please try again later.
I have all 3 modules installed at the required version and connected to the 365 tenant. Would you happen to have any suggestion on this error?
Thanks again!
Are you trying to run the script via application permissions? Some M365 Group-related cmdlets do not work with such, at least on Exchange side. Instead, you will have to use the Graph SDK or directly call the corresponding endpoint, i.e. Remove-MgGroupMemberByRef.
Hi Vasil,
I’ve got it working, looks like there was an issue with one of the modules.
Thank you!
Hi Vasil, thank you for the tutorials! how do we export the csv into a different location? thanks!
You can edit line 220 to change the location. Alternatively, use the -OutVariable to store the output, then Export-CSV it to location of your choice.
Thanks! I’ll try that, my last question will be, how do we login or authenticate using managed identity to run the script? I noticed we required to login from the web calling the exchange task, how do we do that from the script using managed identity and also I guess logging in into graph to run the other task? Thanks again!
The script is not intended for automated use. If you want to use it in such manner, you have to update the connectivity block. Both the Graph and Exchange modules support managed identity nowadays.
Hello Vasil,
First of all thank you for sharing this script.
Now im getting a situation that you might be able to suggest on, if the user I want to remove is marked as the owner I get the error message:
ERROR: User object “user@company.onmicrosoft.com” is Owner of the “groupname” group and cannot be removed…
how can I force it to remove it even it is marked as the owner.
The backend will not allow you to remove a user, if he’s the only owner of the group. Technically speaking, the script’s logic can be updated to automatically assign another owner, say a designated administrator, but there are just too many ifs and buts with such approach, so I’ve resisted adding it. Feel free to update it yourself. Or if you don’t want to mess with the script itself, add another owner to the group(s) in question via the admin portal, then rerun the script.
Write-ErrorMessage : |Microsoft.Exchange.Net.AAD.AADException|We failed to update the group mailbox. Please try again later.
Replace:
#Remove-UnifiedGroupLinks -Identity $Group.ExternalDirectoryObjectId -Links $user.Value.DistinguishedName -LinkType Member -Confirm:$false -WhatIf:$WhatIfPreference -ErrorAction Stop
By:
Remove-MgGroupMemberByRef -GroupId $Group.ExternalDirectoryObjectId -DirectoryObjectId $user.Value.ExternalDirectoryObjectId -ErrorAction Stop -Confirm:$false -WhatIf:$WhatIfPreference
Thanks Juliano. I’m not a fan of the Graph module and try to avoid it where possible. That said, feel free to update any of the script samples as you see fit 🙂
Hi, I have included your script as a part of my company’s offboarding script, and it does exactly what I need it to do.
My script calls a CSV with usernames and then at some point in the script I have this code to remove the groups:
foreach ($User in $CSV_Users) {
$Username = $User.username
$UPN = “”+$Username+”@companyname.com”
Push-Location $RemoveGroupsLocation
.\Remove_User_All_GroupsV2.ps1 -Identity $UPN -IncludeAADSecurityGroups -IncludeOffice365Groups -Quiet
Write-Host “$Username has been removed from all groups.”
}
$RemoveGroupsLocation is the saved location of your ps1 file.
Now, sometimes it works flawlessly, but then other times I get this error:
WARNING: Unable to find type [Microsoft.Graph.PowerShell.Authentication.Utilities.DependencyAssemblyResolver].
Check-Connectivity : The term ‘Select-MgProfile’ is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling of the name, or if a path was included, verify that the path is correct and try again.
At C:\Users\\Temp\Remove_User_All_GroupsV2.ps1:112 char:13
+ if (Check-Connectivity -IncludeAADSecurityGroups:$IncludeAADS …
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [Write-Error], WriteErrorException
+ FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException,Check-Connectivity
ERROR: Connectivity test failed, exiting the script…
It’s acting as if I don’t have the graph module installed. Sometimes just re-running that section in the script works without any problems. Sometimes I have to run “Update-Module Microsoft.Graph” first to get it to work.
Just wondering if there was any idea why this happens.
You likely have the “V2” version installed… Microsoft changed how the “beta” endpoint queries are performed. I’m yet to update the script to support that.
Can you try replacing line 27 with this:
Or you can simply comment it out.
Thanks. I’ll try that the next time I have to offboard someone tomorrow.
Appreciate the quick response.
Just an update, this line change did seem to do the trick. I’ve done it about 5 times and it hasn’t errored yet.
Thanks again.
Hey Vishal
How would we run Connect-MgGraph -ClientID -TenantId -CertificateThumbprint if we automate this script without users consent? I already have this process setup to be used for another script but I can’t get the -scope to work with it.
This would allow our helpdesk to run this script without asking for username/password and we don’t give them admin rights.
Hello,
can I load csv with disabled users to your script?
It works fine for me for single user.
Thank you,
Jan
You can just do this:
where the test.csv file has a column UserPrincipalName to designate each user.
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…
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.
Thank you, Vasil! Updating did the trick. Many, many thanks!
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?
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
And what happens if you run the same cmdlet outside of the script?
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.
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.
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
Just commenting to say thanks and this is an awesome script.
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.
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?
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.
Actually, it’s quite an easy fix. You can get the updated version here: https://github.com/michevnew/PowerShell/blob/master/Remove_User_All_GroupsV2.ps1
Thanks for spotting the issue.
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.
The updated version of the script already uses Get-User, try it. Or you can simply run the script first, then remove the license 🙂
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.
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.
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?
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.
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”
What’s in the $users variable?
This will work:
Or when importing from CSV: