Teams Graph Api

  • Using the Graph API. We can do the same thing via the Graph API with either user-level or application-level permissions. If we look at the documentation you can see that, just like PowerShell, there are different resources to create and modify Teams. Create the Group and add Members.
  • Technically Microsoft has multiple Graph APIs, or endpoints, like the Intelligent Security Graph, the education API, Office Graph and more, but we can easily interact with them simultaneously, as well as use data from one in the other, so lets refer to Microsoft Graph as an entity going forward.
  • Get a Teams channel SharePoint Url using Graph API. Microsoft Teams uses SharePoint online to store the files for a Team. Each channel in a Team is a folder in the ‘Documents’ library in the SharePoint site collection for the Team. The problem we currently have is for renamed Channels, the underlying folder stays with the original name in the document library in SharePoint so there is a disassociation between the two which leads to great confusion for users.
  • Microsoft recently announced Teams management capabilities through the Microsoft Graph API. Graph is a RESTful API that can be called to manage many of Microsoft cloud services. For example, you can write an application or a scheduled PowerShell script that calls Graph to manage Azure AD, Office 365 and Intune, all through the same API.

The Microsoft Graph explorer is a tool that lets you make requests and see responses against the Microsoft Graph.

I was asked to deliver a daily extract of information about our organization’s Microsoft Teams adoption. It was to include the Team’s settings, its owners, its members (separated by internal members and guests), and when it was to expire. Ideally, I’d be able to schedule this to run each morning and drop the resulting file in a Team’s Channel document library.

The first approach

My first approach to the problem was to just see what I could do without thinking too hard. So, with the MicrosoftTeams module, I knew I could retrieve info about all the teams. The downside is that I would have to be elevated to something like “Team Services Administrator” in Azure AD.
$Teams = Get-Team
Getting the team members would be a little more difficult because the commands that get that info are in the Exchange Remote Admin module.

The last requirement was to provide a list of the Teams currently in the recycle bin along with when they’d been deleted. That took the AzureADPreview module.

There are a few problems with these parts to a solution:

  1. None of those modules are natively Core compatible.
  2. The “Classic” Exchange Module requires the use of basic authentication.
  3. Running as myself, I would have to elevate to something like “Team Services Administrator” in Azure AD to use the Teams module.
  4. The report, capturing info for about 2300 Teams, takes around 45 minutes to run.


Could I use an App Registration in Azure AD to connect to Teams and AzureAD? Yes! But… Neither the classic Exchange Admin module nor the new ExchangeOnline module support App Registration authentication. What do all these modules really have in common? The Microsoft Graph API.
So, I started with the great guest post by Alex Asplund on the Adam The Automator blog. His post walks through creating the App Registration. I hadn’t created an App Registration before, but I had the rights to elevate to Global Admin to create one and assign the right application permissions for Graph API:

  • Directory.Read.All
  • Group.Read.All
  • GroupMember.Read.All
  • Team.ReadBasic.All
  • TeamMember.Read.All
  • User.Read.All

I chose to use the certificate method of authenticating for the OAuth token and thus created a self-signed Certificate, then exported and uploaded it to the App Registration.Again, I used the code snippets in the blog to create an OAuth token to use with Graph API requests.
One of the requirements was to provide the expiration date of each group. Researching the outputs of various teams- and groups-related graph endpoints, the creation date and the last-renewed date are returned. We need to be able to calculate the expiration date, and for that we need to know what the lifecycle policy is for the groups.

The request returns you a collection (of one, in my case) of Group Lifecycle Policies with all of the policy’s properties including the groupLifetimeInDays value that we’re after.
Now we want to get the groups. We want to end up with the Teams, and Teams are “Unified” groups in Azure AD. The following will get the Unified groups in your organization:

Note: When using the OData queries via PowerShell, you need to be careful to include the backtick that escapes the dollar sign, otherwise PowerShell is going to try and evaluate $filter.

Another thing to be aware of is the Graph endpoints generally return just 100 items per page, but that response includes a link to get the next page. So, to deal with that and get all of the groups, use a while loop:

That gives us all of the Unified groups, but not all Unified groups are Teams!

We know that going and getting the Team object, the members, and the owners is going to involve a lot of waiting for Inovke-RestMethod responses, so let’s start by leveraging some parallelism with:

Another point where we can achieve some efficiency is with making batch requests to the Graph API using the '$batch' endpoint. Again, be mindful of your single vs. double quotes here. By using single quotes in this case, we avoid the dollar sign issue. Using the batch endpoint is well documented at . Create a hashtable key-value pair that is a collection of hashtables, one for each of the requests you want to submit in the batch. Convert that to JSON, and submit it as the body of the request.

You can see above that we’re getting the Team, the members, and the owners, all keyed by the ID property. Submit that block of JSON to the $batch endpoint and parse the results:

The advantage here is that we only make one request instead of three transactions. For the members, we’ll deal with pagination in the same way as with the Teams themselves.

Distinguishing the “guests” from the internal “members” is just a matter of examining the User Principal Name (UPN); AzureAD guests will have “EXT” in middle of the UPN.

We’ve collected all the information we need for the team, we only need to calculate that expiration date before we generate some output:

It’s a matter of outputting a [pscustomobject] with all of the team properties.

The last requirement I was presented with was to provide a listing of all of the Groups that were in the recycle bin. That’s found with the Graph endpoint $DeletedGroupsURI = '' As with the Groups themselves and the members, the results are paginated and are handled the same ways as before. My organization wanted an Excel spreadsheet so I used Doug Finke’s ImportExcel module to export the collections.


Working with the Graph API can allow you to customize your approach to AzureAD and Teams management. Using the modern App Registration authentication acts not unlike JEA, in that an unprivileged account can be enabled to perform specific limited tasks. In this case we were able to efficiently gather all of the information we needed about the state of our Teams environment, while managing and maintaining control over access. With no elevated access required, “who” the script runs as becomes less important, as long as that entity has access to the file location.

The full code for this solution will be available as TeamsReportByGraph.ps1

Edited by AJ Lewis

So we have our Microsoft Teams app set up, and can successfully authenticate to get a user’s information. But we’ll probably want to more with Teams. To do that, we’ll need to ask for more permissions from the user, which we’ll do during auth with “scopes”. Depending on what we want to do, we’ll need to ask specific scopes that covers that functionality. One note, we’re mostly focusing on Work accounts… Personal accounts are fairly limited in what they allow for Personal/Free Teams tenants.

Ms Graph Teams Api

Delegated vs Application scopes

Teams Graph Api Tutorial

The Microsoft docs have a full list of scopes that you’ll probably want to bookmark if you do anything with the Graph API. The first thing you may notice is that there are “delegated permissions” and “application permissions”.

There’s a page that goes into detail about everything, but basically, Application permissions are set on your app’s page in the Azure portal. They’ll be that same for everyone, you set the “scope” parameter to , and they’ll all require a Teams admin to consent for users to use them. I’ll go into admin consent in a moment.

Delegated permission are added to “scope” parameter and can either require admin consent or not. The delegated permission allow for more flexibility, so that’s what we’ll be using. Also, there are a few Graph API endpoints that don’t require an admin to consent to them, so it opens up more possibilities.

To Admin Consent or Not to Admin Consent


There are quite a few API endpoints that only provide basic information about various parts of Teams, and don’t require a Teams admin to consent to them. Though, there are still cases where an admin locks down their tenant so much that even non-admin consent endpoints may not work.

For example, if we want to get a list of Teams that a user is a member of, during auth, we’ll need to request one of these scopes: Team.ReadBasic.All, TeamSettings.Read.All, TeamSettings.ReadWrite.All, User.Read.All, User.ReadWrite.All, Directory.Read.All, Directory.ReadWrite.All. In the documentation, they list the scopes needed in order of least privileged to most privileged, meaning how much data your app will have access to. It’s best practice to use the lowest privilege that your app needs, but once you know what endpoints your app needs to hit, you may be able to use an elevated one to cover multiple endpoints.

In the “list of Teams” example, we can ask for the “Team.ReadBasic.All” scope, which does not require an admin to approve its usage. However, if we notice we also need to be able to update a user’s info, we’ll need to request “User.ReadWrite.All” which does require an admin to consent… and we could just use that single scope to cover both endpoints.

If we decide to use the “application permissions” instead of delegated ones, they ALL require an admin to consent. This likely means a non-admin user would try to login, then get stopped on Microsoft’s scopes page without being able to continue. So let’s stick with non-admin, delegated permissions to see what we can do…

Getting a channel list

For this task, we want to get all the channels a user is a member of. Luckily, we can get that data without (generally) needing an admin to approve it. Let’s update our login method:


We added two new scopes, and those permission should now show up on the Microsoft login page

Now, after they get redirected, we can make the endpoint calls we want. First, to get the list of the Teams the user is in, grab the first Team from the list, then get the channels in that Team.

Now, after logging in, authenticating, and being redirected back to our site… we should get something like this:

Teams Graph Api Call

Now, with just adding a few scopes, we’re able to get some good information about the user’s Teams and the channels they’re in. In the next post, we’ll see about a practical application for all this.