Table of Contents
Administrative Interfaces Can List Hidden Group Memberships, but Graph-Based Apps Need Extra Permission
A user of the Microsoft 365 Groups and Teams activity report script (which I should do some work on to upgrade some really old code) pointed out that they weren’t getting details of groups with hidden membership. I’ve written about groups with hidden membership before and observed that administrative interfaces like the Microsoft 365 admin center or Entra admin center have access to hidden membership (Figure 1) where user-facing clients like Outlook block access to hidden group memberships.

PowerShell modules built for administrative use also count is administrative interfaces, so cmdlets like Get-UnifiedGroupLinks from the Exchange Online management module report hidden memberships as happily as they list open memberships. Modules like Exchange Online assume that anyone running their cmdlets is an administrator, so they extend the same access to data that the administrator enjoys through an admin portal.
Listing Hidden Group Memberships is Different with the Graph
The Graph API is different. Working on a least permission model, the Graph makes no assumptions about permissions when a session starts and the only access to data available during the session is via granted permissions. The permissions can be delegated (access to data available to the signed-in user) or application (available to tenant data). Delegated permissions are used for interactive sessions. Application permissions are used by apps (which can run in interactive sessions).
When the problem was first reported, I did a quick check but couldn’t find anything wrong. But I screwed up by running commands in an interactive session signed in with an account that held the Exchange administrator role. Interactive sessions use delegated permissions, but they also respect any of the administrative roles assigned to the account, so while the Get-MgGroupMember cmdlet would normally stop me seeing the members of a group with hidden membership, the Exchange administrator role removed the block and made the membership visible.
This experience proves once again that any testing for a Graph access scenario should be done with application permissions. In other words:
- Create an app (registration).
- Assign the app the lowest possible level of permissions you think are needed to get the job done.
- Test.
- Refine as necessary using different permissions until the test is successful.
Fetching Hidden Group Membership
In this scenario, I started off with an app with consent to use the Group.Read.All and User.Read.All permissions. The former is needed to read group details; the latter to retrieve member information (user accounts). I then disconnected my current interactive Microsoft Graph PowerShell SDK session and signed in with the app, using the thumbprint of an X.509 certificate uploaded to the app to authenticate. Running Get-MgContext confirmed the available permissions (scopes):
Connect-MgGraph -Tenantid $TenantId -AppId $AppId -CertificateThumbprint $Thumbprint (Get-MgContext).Scopes | Format-List User.Read.All Group.Read.All
Now attempt to read the membership of a group with hidden membership. The Get-MgGroupMember cmdlet returns nothing, but we know why because the visibility property of the group is set to HiddenMembership. A group might have no members, but if its visibility property is set to HiddenMembership, there might be data to retrieve,
Get-MgGroupMember -GroupId $GroupId Get-MgGroup -GroupId $GroupId | Select-Object Visibility Visibility ---------- HiddenMembership
The Visibility property is most often used to note whether a group has private or public membership. Unfortunately, it’s not a filterable property for the Graph, so to find the groups with hidden membership, you do something like this:
[array]$Groups = Get-MgGroup -All -PageSize 500 $Groups | Where-Object {$_.Visibility -eq 'HiddenMembership' } | Format-Table DisplayName, Id, Visibility
To find details of the hidden membership, grant consent for the app to use the Member.Read.Hidden permission. Disconnect and reconnect using the app and make sure that Member.Read.Hidden is available. Now run Get-MgGroupMember again:
[array]$Members = Get-MgGroupMember -GroupId $GroupId Id DeletedDateTime -- --------------- eff4cd58-1bb8-4899-94de-795f656b4a18 cad05ccf-a359-4ac7-89e0-1e33bf37579e 08dda855-5dc3-4fdc-8458-cbc494a5a774 75ba0efb-aed5-4c0b-a5de-be5b65187c08 4daa5f06-55eb-4d79-9a24-1be369919fec 59e09287-ac1b-4ff7-80a3-08d0d1eed939
Or to see the display names of the members:
$Members.additionalProperties.displayName Tony Redmond James Ryan Sean Landy Terry Hegarty Otto Flick Hans Geering (Project Management)
If an app has a higher-level permission, such as Directory.Read.All, it can also read hidden membership. The same permission governs access to hidden memberships of Entra ID groups and administrative units.
The Takeaway about Graph Permissions
The takeaway here is not to assume anything about Graph permissions. The ability of the Microsoft Graph Command Line Tools app (used for Microsoft Graph PowerShell SDK interactive sessions) to accrue delegated permissions over time is both a benefit and a problem. When you can, use app-only mode to validate exactly what permissions are required to run your code.
Need some assistance to write and manage PowerShell scripts for Microsoft 365? Get a copy of the Automating Microsoft 365 with PowerShell eBook, available standalone or as part of the Office 365 for IT Pros eBook bundle.
Didn’t know that the characters from the BBC-Sitcom “allo allo” (like Otto Flick or Hans Geering) are working for Tony Redmond’s enterprise 😉
Gotta get test names somehow… and I set up new ones regularly after I get tired of the spam sent to the test accounts…