Safe Teams External Access Means Limiting Communications to Known Domains
In September 2022, a security researcher demonstrated how an external participant in a Teams chat could send a modified GIF containing malware that infected the recipient’s PC. By default, Teams allows users to chat with anyone from any other Microsoft 365 tenant. The solution to something like the GIFShell exploit is to restrict Teams external access to an allow list (whitelist) of known tenants.
The problem then becomes how to populate the allow list. At the time, I suggested that organizations could make a start by building a list of the domains that guest accounts came from. It isn’t difficult to use PowerShell to find all guest accounts, extract their domains, and use that information to build a allow list, which is large enough to accommodate approximately 4,000 domains.
Job done, or so I thought. However, others believe that using guest domains as the basis for an allow list isn’t good enough. There’s no guarantee that the set of domains derived from guest accounts will cover every possible domain that an external chat participant might come from. Some arrangement is therefore necessary to allow people to request the addition of a domain to the allow list. This reduces the spontaneous nature of chat-based collaboration across organizations, but it might be the price necessary for secure communications.
Then someone decided that it might be possible to examine all external chats in an organization to discover the set of domains that external chat participants belong to. Sounds good, but how can you find and process the information from user chats? That’s what I explain in this article.
Accessing Chat Participants is the Key for Teams External Access
In another article, I explain the basics of using the Microsoft Graph PowerShell SDK to find and process Teams chat and chat messages. The PowerShell script dissected in that article allows administrators to remove messages from group chats that might be objectionable or contain sensitive information. The script needed here has to find chats too, but one-to-one chats rather than group chats. After finding a list of user accounts with the Get-MgUser cmdlet, we can use a different filter with the Get-MgBetaUserChat cmdlet to find the one-to-one chats for each user.
Each chat has a list of participants that can be fetched using the Get-MgChatMember cmdlet. One of the participants is a tenant account, the other might be an external participant. To build a list of external domains, the script needs to examine the participants for each chat and find those involving external participants. With that data, the script can build a domain list. At least, that’s the theory.
Defining the Solution
The outline of the solution is clear. However, detail makes the difference and there are some important details to consider here.
First, the script must use application permissions to access user chats. This wasn’t a problem for the previous script because I used an Azure Automation runbook with a managed identity for authentication. The necessary Graph permissions are assigned to the Azure Automation account.
In this scenario, I want to use the Get-CsTenantFederationConfiguration cmdlet from the Microsoft Teams module to fetch details about the tenant’s external access configuration. The policy management cmdlets that Teams inherited from Skype for Business Online do not support managed identities (the other Teams cmdlets do because they are Graph-based). I could get around the problem and still use Azure Automation by leveraging Azure Key Vault to store account credentials and using those credentials to connect to Teams. In the end, I decided to go with a different approach and use certificate-based authentication because all cmdlets support this approach.
Because the script uses certificate-based authentication, it needs a registered Entra ID app to store the certificate and hold the Graph permissions necessary to access the data needed by the script:
- CrossTenantInformation.ReadBasic.All: Read tenant identifier and return tenant display name.
- User.Read.All: Read user account information.
- Chat.Read.All: Read user chats.
- Directory.Read.All: Read home tenant information from the directory.
- Mail.Send: Send an email with the results.
In addition, because the script runs a Teams policy management cmdlet, the app must hold the Teams administrator role. This isn’t difficult because the role assignment is easily done using the Entra ID admin center, but it is an example of the kind of detail required to make everything work.
The steps outlined in this article describe how to create a self-signed certificate (enough for testing) and upload it to a registered app (Figure 1).
![The certificate thumbprint used by an Entra ID registered app for authentication](https://s40823.pcdn.co/wp-content/uploads/2023/11/Teams-External-Access-App-Cert-1024x539.jpg)
The article also describes how to use PowerShell to assign an Entra ID administrative role to an app using code like this:
$TeamsAppSp = (Get-MgServicePrincipal -Filter "DisplayName eq ‘PowerShellGraph'").Id $TeamsRoleId = (Get-MgDirectoryRole | Where-Object {$_.DisplayName -eq "Teams Administrator"}).Id $NewAssignee = @{ "@odata.id" = ("https://graph.microsoft.com/v1.0/directoryObjects/{0}" -f $TeamsAppSp) } New-MgDirectoryRoleMemberByRef -DirectoryRoleId $TeamsRoleId -BodyParameter $NewAssignee
With the app assigned the necessary permissions and role and a certificate in place, we can write some code.
Scripting the Solution
The steps in the script are:
- Connect to the Microsoft Graph endpoint using certificate-based authentication. You need the client identifier for the registered app, the tenant identifier, and the certificate thumbprint to connect.
- Connect to the Teams endpoint using certificate-based authentication. Get-CsTenantFederationConfiguration is the only cmdlet used from the Teams module. All other cmdlets come from the Microsoft Graph PowerShell SDK.
- Find the set of registered domains for the tenant. This information is used to figure out if a one-to-one chat is with someone from inside or outside the tenant.
- Fetch the set of licensed user accounts in the tenant. This can include shared mailboxes with licenses, but it’s the easiest and quickest way to find the set of accounts that use Teams.
- For each user, find the set of their one-to-one chats. A small delay is imposed between each user to avoid Microsoft 365 throttling.
- For each chat, retrieve the participant list and remove the user being processed from the list. Because these are one-to-one chats, only one other participant should remain.
- Find the domain name for the other participant to figure out if the chat is internal or external.
- If external, resolve the tenant domain by using the tenant identifier.
- If the chat is hosted by the home tenant, retrieve the number of messages in the chat. You can’t retrieve this information for chats hosted by external tenants.
- Record what’s been found and go back to process the next chat.
- After processing chats for all users, check the Teams external access configuration and compare the domains in its allow list against the domains used for external chats. Report any domains used for external chats that are not in the allow list.
- Email the results to a predetermined recipient. The results are details of external chats and the information about external domains.
- Declare success.
Figure 2 shows an example of the email sent by the script. The set of properties for each record is defined in the script and doesn’t include all those captured in the report. No filtering of the data is done. For example, you might decide to concentrate on chats with external participants initiated in the last two years and ignore earlier chats.
![Email sent to an administrator with details of external Teams chats](https://s40823.pcdn.co/wp-content/uploads/2023/11/Teams-External-Access-EMail-1024x659.jpg)
Running the Script
You can download the script from GitHub. Remember, this script works but it is only intended to demonstrate the principles of using the Graph SDK to fetch and process information about Teams chats and their participants. It is not designed to be an off-the-shelf solution and I have not run it at true scale. Given the need to fetch all one-to-one chats for each user processed, the script is not lightning-fast. You might want to run it for a limited set of users first. If so, apply a filter to the set of users fetched from Entra ID. For instance, to limit the set to users from a specific department, you could modify the Get-MgUser call to something like this:
[array]$Users = Get-MgUser -Filter "assignedLicenses/`$count ne 0 and userType eq 'Member' and department eq 'IT'" -ConsistencyLevel eventual -CountVariable Records -All | Sort-Object displayName
Before attempting to run the script, make sure that all the prerequisites are satisfied and that you use a registered app assigned the necessary Teams administrator role and Graph permissions.
Make Decisions Based on Data
This exercise is a good example of how useful the Microsoft Graph PowerShell SDK is when the need arises to extract information from Microsoft 365. Without access to user chats, it’s impossible to know who outside the tenant people are conversing with. The Graph SDK exposes their secrets and helps administrators make better decisions based on data rather than intuition, and that’s always a good thing.
Microsoft Platform Migration Planning and Consolidation
Simplify migration planning, overcome migration challenges, and finish projects faster while minimizing the costs, risks and disruptions to users.
Has anyone encountered this error?
Can’t fetch participant information for chat 19:323dc082-0c43-434a-a27a-4d23d8262cc2_c3f29ef2-863c-4799-b778-b447d2513e10@unq.gbl.spaces
I get this on every chat and no results.
The Real Person!
The Real Person!
Are you running the script with the right permissions? If you run the code interactively, the script can use delegated permissions to fetch your chats but it cannot access chats in other accounts. In the article, I say:
First, the script must use application permissions to access user chats. This wasn’t a problem for the previous script because I used an Azure Automation runbook with a managed identity for authentication. The necessary Graph permissions are assigned to the Azure Automation account.
If you want to run the job interactively, create a registered Entra ID app, assign it application permissions, create a certificate and upload it to the app, and then run Connect-MgGraph with the certificate thumbprint, app id, and tenant id.
@Tony: Spelling mistake in Line 65: Lookimng for chats for user 😊
Thanks for this amazing script!
The Real Person!
The Real Person!
Aha. A bug I can fix! 😉
Thanks for sharing the script Tony. I had the same idea a couple of months ago but never found the time to get into developing it, so this is much appreciated!
Great walkthrough of a clever use of the available information.
One non-technical note though: replace whitelist with allowlist.
The Real Person!
The Real Person!
Well, the whitelist term is used for external access but I won’t fall out with you and have changed the term in the article.