Better Than Audit Events at Extracting Full Details of Teams Online Meetings
It’s great that Microsoft is finally making the set of additional audit events available to tenants with Purview Audit (Standard) licenses. Some might be excited about the prospect of being able to analyze data for Teams online meetings from the MeetingDetail and MeetingParticipantDetail events, but as I point out elsewhere, the information available in those events isn’t as comprehensive as you might imagine.
Although the audit events are helpful and might be the right solution in many cases, if you want to find out everything about a Teams online meeting, Microsoft Graph APIs are the only answer. This article explains how to use Graph APIs to find Teams online events in user calendars and extract the attendance information and other details for the meetings. A fully functional PowerShell script is available for you to adapt to meet your own requirements.
The basic approach is to connect to the Microsoft Graph in app-only mode (using an Entra ID app to authenticate) and use the Graph permissions assigned to the app to read data from user calendars and the Teams store. The final step is to generate an HTML report from the data.
Create an Entra ID Application to Access Online Meetings
The first step is to create an Entra ID app to hold the permissions needed to retrieve Teams online meeting data, calendar data, and other information. The app needs an X.509 certificate (self-generated or bought) for authentication. After creating the app, assign the following application permissions:
- Calendars.Read: Read calendar data to find online meetings to process.
- Group.Read.All: Read group membership to find the accounts to process.
- OnlineMeetings.Read.All: Read Teams online meeting data.
- OnlineMeetingArtifact.Read.All: Read attendance reports for Teams online meetings.
- CrossTenantInformation.ReadBasic.All: Read information in attendee reports to resolve tenant name for external (federated) participants.
- Organization.Read.All: Read information about the tenant (like its display name).
Figure 1 shows the set of application permissions assigned and consented to for the app used by the script.
Note the application (client) identifier, the tenant (directory) identifier, and the certificate thumbprint from the app properties. Store the values in PowerShell variables to use in the script. These elements are used to authenticate when connecting to the Graph with the Connect-MgGraph cmdlet.
Create an Application Access Policy
With application permissions, the app can access data from across the tenant. Well, yes, but only for groups, calendars, and cross-tenant information. The application permissions used to access Teams online meetings have a further level of protection called an application access policy.
An application access policy links one or more Entra ID apps with accounts to allow the apps to process online Teams data belonging to those accounts. In some respects, it’s like the RBAC for applications mechanism used by Exchange Online to restrict access to mailboxes.
The Teams admin center doesn’t support the management of application access policies. These actions must be performed with PowerShell. First, we create an application access policy by running the New-CsApplicationAccessPolicy cmdlet:
New-CsApplicationAccessPolicy -Identity 'Read Teams Meeting Data' -AppIds "78b6e21f-127d-470d-a61b-295d5c2d058c" -Description "Permission to read Teams Meetings for users" Identity : Tag:Read Teams Meetings AppIds : {78b6e21f-127d-470d-a61b-295d5c2d058c} Description : Permission to read Teams Meetings for users
With the application access policy created, we can grant it to the accounts whose online meeting data the script should process. It is possible to grant an application access policy to all accounts that are not assigned a specific policy, but in this instance, we run the Grant-CsApplicationAccessPolicy cmdlet to specific accounts. Run the cmdlet for each account, passing the identifier for the account as the assignee. Either the user principal name or GUID works:
Grant-CsApplicationAccessPolicy -PolicyName 'Read Teams Meeting Data' -Identity 21a3ce8f-6d55-4e57-9210-d85a2f1618ec
You can assign only one application access policy at a time to a particular user. Assigning a new application access policy to a user overrides any existing application access policy. In addition, it’s only possible to grant an application access policy to a member account with a valid Teams license. It can take up to 30 minutes before an application access grant allows an app to access online meeting data.
After making the assignments, check the accounts that the app can process with:
Get-CsOnlineUser -Filter {ApplicationAccessPolicy -eq "Read Teams Meeting Data"} | Select-Object DisplayName,UserPrincipalName,ApplicationAccessPolicy DisplayName UserPrincipalName ApplicationAccessPolicy ----------- ----------------- ----------------------- Alex Wilber AlexW@o365maestro.onmicrosoft.com Read Teams Meeting Data "Popeye" Doyle Popeye.Doyle@o365maestro.onmicrosoft.com Read Teams Meeting Data Alain Charnier Alain.Charnier@o365maestro.onmicrosoft.com Read Teams Meeting Data
Connect to the Microsoft Graph
After waiting for the policy assignments to be effective, connect to the Microsoft Graph with the Microsoft Graph PowerShell SDK. We want to use the permissions assigned to the app that has access to online data for the specified accounts, so Connect-MgGraph includes the app identifier, tenant identifier, and certificate thumbprint in its parameters:
$AppId = "78b6e21f-127d-470d-a61b-295d5c2d058c" $TenantId = "22e90715-3da6-4a78-9ec6-b3282389492b" $CertThumbprint = "F79286DB88C21491110109A0222348FACF694CBD" Connect-MgGraph -AppId $AppId -TenantId $TenantId -CertificateThumbprint $CertThumbprint
To check that all the required permissions are available, run Get-MgContext. The script will only work if the five permissions listed below are available to the session:
(Get-MgContext).scopes OnlineMeetings.Read.All OnlineMeetingArtifact.Read.All Calendars.Read Group.Read.All CrossTenantInformation.ReadBasic.All Organization.Read.All
Steps to Process Teams Online Meetings
The script uses the following steps:
- Find the set of user accounts to extract Teams online meeting data for. To make the set of accounts easier to manage, the script uses a distribution list to hold the accounts and reads the membership with the Get-MgMemberGroup cmdlet.
- For each account, read the set of events from the calendar folder in the user’s mailbox and extract the set of online Teams meetings.
- For each meeting, extract the details including the attendance reports.
- Generate a report from the data.
It all sounds straightforward, but as usual, there are a few details that get in the way.
Some Issues to Deal with When Fetching Teams Online Meeting Data
First, access to data for Teams online meetings seems to stop after 60 days or thereabouts. The calendar information is always available, but not the details about the online aspect. It seems like Teams moves its meeting data out of the online cache after 60 days and any subsequent attempt to return details about a meeting fails. If you attempt to open an old meeting in a Teams client, you can see the data. I assume some other API is used to retrieve the old data and load it into the cache.
This is an example of where audit events are better. If you have Purview Audit (Standard) licenses, the data is available for 180 days. With Purview Audit (Premium), the data is available for 365 days.
Decoding and Coding Meeting Identifiers
Second, you’re probably aware of the meeting URL published for each Teams meeting. When using the Get onlineMeeting API to retrieve meeting details, the API requires the identifier of an online meeting object. The documentation is not terribly helpful in terms of defining the format of the meeting identifier, but some searching revealed a method using the following steps:
- Fetch the meeting join URL. The value will be something like: https://teams.microsoft.com/l/meetup-join/19%3ameeting_YzU5NTYwM2QtMmU3YS00YmNlLWEwZWMtNzA0NTk4OTZlNjJi%40thread.v2/0?context=%7b%22Tid%22%3a%22a662313f-14fc-43a2-9a7a-d2e27f4f3478%22%2c%22Oid%22%3a%225b52fba5-349e-4622-88cd-d790883fe4c4%22%7d (according to MC772556, Microsoft plans to adopt a shorter meeting join URL format from August 2024).
- Decode the URL.
- Find the meeting identifier in the decoded meeting join URL (a value like 19:meeting_ZmY4YTJhNmUtY2Y3OS00ZGZlLWI1ODEtYjA2ZTZmZjA0YzQ0@thread.v2).
- Create a lookup value formatted in a specific way. The value contains the user identifier for the organizer and the meeting identifier and looks like this: 1*880e1e61-af63-43c9-a48e-d6b63684c21c*0**19:meeting_ZmY4YTJhNmUtY2Y3OS00ZGZlLWI1ODEtYjA2ZTZmZjA0YzQ0@thread.v2.
- Create a BASE64 encoded value of the meeting lookup identifier.
I assume that Microsoft obfuscates Teams meeting identifiers in this manner to limit the possibility of someone misusing an identifier to access meetings that they shouldn’t. In any case, here’s the PowerShell code I used:
$MeetingURL = $Item.onlinemeeting.joinUrl.trim() $DecodedURL = [System.Web.HttpUtility]::UrlDecode($MeetingURL) $MeetingIdStart = $DecodedURL.IndexOf("19:") $MeetingIdEnd = $DecodedURL.IndexOf("thread") $MeetingId = $DecodedURL.Substring($MeetingIdStart, $MeetingIdEnd - $MeetingIdStart +9) $MeetingIdLookup = ("1*{0}*0**{1}" -f $Organizer.id, $MeetingId) $Base64MeetingId = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($MeetingIdLookup))
The script can then construct a URI to use with a Graph request. The URI looks like:
https://graph.microsoft.com/v1.0/users/880e1e61-af63-43c9-a48e-d6b63684c21c/onlineMeetings/MSo4ODBlMWU2MS1hZjYzLTQzYzktYTQ4ZS1kNmI2MzY4NGMyMWMqMCoqMTk6bWVldGluZ19abVk0WVRKaE5tVXRZMlkzT1MwMFpHWmxMV0kxT0RFdFlqQTJaVFptWmpBMFl6UTBAdGhyZWFkLnYy
The return value is the details Teams holds about the online meeting.
After that, the only remaining thing to deal with is the fact that a meeting can start and stop several times. Each instance generates an attendance report and other artifacts, like a transcript.
We’re interested in knowing who attends any instance of a meeting, so the script finds all attendance reports and extracts the attendance data for each report.
The Report
No matter how nice a PowerShell script is, if it doesn’t produce useful output the code is useless. In this case, the output is an HTML report listing the online meetings by user with details of the meeting and its attendance (Figure 2).
Note that when guest accounts attend a meeting, they usually switch to the host tenant before joining (a discipline enforced by some organizations). This makes it seem like the account is from that tenant (which they are), so you don’t see the home tenant name except for federated attendees (those that sign into attend a meeting in another tenant using their own account).
The script is available from GitHub. I hope that the PowerShell code is understandable. If it isn’t, please be sure to fix it for me. I can use all the help I can get.