As Microsoft Teams grows into one of the most successful tools in the Microsoft 365 suite, more organizations are seeing Teams as business critical. Entire processes are built around Teams and it has become more important to be able to manage Teams at scale. Generally when managing Teams membership, we can use the Exchange Online cmdlets to manage membership of the Unified Group that underpins the Team, however when those Teams include Private Channels, this method doesn’t meet the requirements. The Teams PowerShell Module does have the cmdlets available but in my experience with it, it doesn’t perform well at scale. This will surely improve over time but to meet the requirement today, I have created a script to allow the copying of Teams and Private Channel membership between Teams both within the same tenancy, and between tenancies.
Prepare the Source Tenancy
As we are using Graph API, we need to create an Application Registration to handle authentication and permissions. To set this up, check out the post I wrote (Tip 1) on 7 Tips for Working with the Graph API in PowerShell. With the Application Registration set up in the source tenancy, create a Client Secret. Take note of the Application ID, Tenant ID and Client Secret as per Tip 3 and then add the Application permissions shown in Figure 1.

Prepare the Target Tenancy
The target tenancy is setup similar to the source however there are different permissions required to write the membership in the target Teams. Set up a app reg in the target and add Delegated permissions shown in Figure 2. As the ability to include Guest users in the process was something I wanted to include in the script, delegated permissions are required. This is because we can’t add Guests to a Team using application permissions for some reason…

As we will be using delegated permissions, you don’t need to create a Client Secret for the destination tenant, however make sure to enable “Allow Public Client Flows” under “Authentication” as shown in Figure 3.

It’s also important to note that using delegated permissions, your account must be an owner of the Teams and Channels in the destination.
Build the Mapping File
To translate the members in the source tenant into members in the target tenant, we need to provide a mapping file. This mapping file is a CSV and is pretty straightforward. We only need to provide two columns, “SourceID”, and “TargetID”. SourceID is the Object ID of the users in your source tenant and TargetID is the same value for the user in the target tenant. The below command can help export a list of all of the users and their Object IDs to CSV. It can be run in each tenant and used to create a mapping file using some Excel skills.
Get-AzureADUser -Filter "userType eq 'Guest'" -All $true | select Displayname, Mail, ObjectID | Export-Csv TargetGuests.csv -NoTypeInformation
In the end, the mapping file should look like the example in Figure 4.

If there are members you don’t want to migrate, simply exclude them from the mapping file.
Running the Script
Once you download the script from GitHub (Link at the bottom of the post), prepare the following parameters:
- SourceTeamObjectID – The Object ID of the Team / Group in the Source Tenant
- SourceTenantID – The Tenant ID of the source tenancy
- SourceClientID – The Client ID of the App Reg in the source tenancy
- SourceClientSecret – The Client Secret of the App Reg in the source tenancy
- TargetTeamObjectID – The Object ID of the Team / Group in the TargetTenant
- TargetTenantID – The Tenant ID of the Target tenancy
- TargetClientID – The Client ID of the App Reg in the Target tenancy
- MappingFile- The full file path to the mapping file
- TargetToken – An access token for the target tenancy, more on this below
To generate the Token for the TargetToken parameter, check out this post about requesting a delegated token, I recommend using MSAL.PS.
.\graph-Teams-Migration-Script.ps1 -SorceTeamObjectID fa9377ca-0824-4da5-aea9-496856298ecc -SourceClientSecret $SourceclientSecret -SourceClientID $SourceclientID -SourceTenantID $SourcetenantID -TargetClientSecret $TargetclientSecret -TargetClientID $TargetclientID -TargetTenantID $TargettenantID -TargetTeamObjectID 49736ecc-0755-4ec8-abba-6995e7c34a05 -MappingFile C:\temp\Mappingfile.csv -TargetToken $TargetToken.accesstoken

The script should look similar to Figure 5 and once complete, you’ll also find a log at “C:\temp” on your machine, the members will be present in the target Team in line with the source and according to the mapping file provided (Figures 6 and 7).


Note: There is a delay in Private Channel Membership showing updated in the Teams interface but searching for a user will show the membership is present (Figure 8 and 9)


Summary
This script is one part of an overall set of tools I have created to support migrations which I will be uploading here over the next few weeks. I highly recommend that you read and customize this script to meet your requirements before running in production. My favourite thing about this script is that it doesn’t rely on exporting members from source and translating that for import. The export and translation is done on the fly and it can be run on a loop as long as a complete mapping file is provided.
Download the script from GitHub here!
Pingback: Dealing with Teams Guest Users During Tenant to Tenant Migrations – Sean McAvinue