There is a Microsoft.Graph PowerShell module provided by Microsoft which simplifies usage of Microsoft Graph API. Below is how to authenticate to MS Graph and how to search within SharePoint and Teams Microsoft 365 content using Microsoft.Graph PowerShell module.
For daemon app authentication we need a certificate configured in Azure App and installed on the user machine. Daemon app authentication code sample (please specify your tenant id, app (client) id and certificate thumbprint:
Below is how do I search Microsoft 365 content programmatically from PowerShell using MS Graph API, PowerShell PnP, Microsoft Graph module, MSAL library being authenticated as user or daemon application. Let me focus on SharePoint content here but you can use the same technique to search through other Microsoft 365 services. Also, I’ll be using PowerShell but same ideas should work for other platforms/languages – Python, C#, node.js etc.
To search on behalf of currently authenticated user we need delegated “Sites.Read.All” API permissions. I recommend you to add both Graph API and SharePoint API permissions as different libraries might use different API’s under the hood. Ensure you add delegated “Sites.Read.All” even if you already have “Sites.FullControl.All” as by some reason “Sites.FullControl.All” does not always work for search.
Here is how an app API permissions to search as current user should look like:
For unattended search – e.g. search on behalf of daemon app – we need application “Sites.Read.All” API permissions. Again, I suggest both Graph API and SharePoint API permissions added. Here is how an app API permissions to search as daemon app should look like:
Ensure you got admin consent for API permissions.
In case you have incorrect permissions in your app – Microsoft Graph will be kind enough to inform you exactly what you need. Example:
“Access to ChatMessage in Graph API requires the following permissions: Chat.Read or Chat.ReadWrite, ChannelMessage.Read.All. However, the application only has the following permissions granted: Sites.Read.All, User.Read”
Assuming we have configured apps – let us get started with
Microsoft Graph API
Microsoft Graph API allows search through all the Microsoft 365 content – including Exchange e-mail messages, Yammer (Viva Engage) and Teams chat messages and surely OneDrive and SharePoint content (please refer to the original doc).
Authenticate as current user to Search with Graph API
I use MSAL.PS PowerShell module to get token, then I build a headers variable
If you are getting error message “SearchRequest Invalid (Region is required when request with application permission.)”:
that’s OK, just modify your body to include region like this (“region”: “NAM” for North America or “GBR” or …). Also, I can modify body with from/size for paging (technique used to iterate through search results if there are many) and return just specific fields to decrease traffic and improve performance:
For daemon app authentication we need a certificate configured in Azure App and installed on the user machine. Daemon app authentication code sample (please specify your tenant id, app (client) id and certificate thumbprint:
What if you need to bulk update Microsoft 365 groups membership e.g. to add a group owner or member for tens of thousands m365 groups? Iterating through groups one-by-one is unproductive and could take days. Can we do it faster? Here is what I found.
In my case, it was Microsoft 365 ownerless groups policy implementation for large tenant… Skipping details – I needed to update ownership for 10,000 Microsoft 365 groups and I was looking for a best/fastest possible option maybe some kind of bulk update or with multiple threads. And I figured out that the fastest way is to use PnP.PowerShell that calls Microsoft Graph API but run it against list of groups with PowerShell parallel trick. Here is the sample PowerShell code:
Sometimes, mostly during PoC or testing policies like retention policy or lifecycle policy you would need some documents created and updated weeks, months or even years ago.
But if you create or upload a document in SharePoint library – it will be just a regular new document. So, how to get old documents in the new environment?
I see two options:
Sync with OneDrive If you sync a library with your local folder (done Microsoft by OneDrive desktop app) and put some old document in your synced folder – the doc will be synchronized back to SharePoint library with Created and Modified properties preserved.
Make the document older with PowerShell With “Set-PnPListItem” PowerShell command you can update not only such properties like Title, but also “Created By”, “Modified By” and even date and time document was created and modified via “Created” and “Modified”. Optionally you can play with document history with “-UpdateType” parameter. UpdateType possible values are:
Update: Sets field values and creates a new version if versioning is enabled for the list
SystemUpdate: Sets field values and does not create a new version. Any events on the list will trigger.
UpdateOverwriteVersion: Sets field values and does not create a new version. No events on the list will trigger
As SharePoint or Teams admin you can manage Microsoft 365 groups (create, update, delete, manage membership etc.) having your admin role activated. I use Azure registered app with “Group.ReadWrite.All” Microsoft Graph API delegated permission and Microsoft.Graph PowerShell module.
When a user was not a group member or group owner – and is added to the group members – user gets notification “You’ve joined the <Group Name> group” via e-mail that comes from a group e-mail address.
When a user is added to the group owners (or elevated to group owner if user was a group member) – user does not get notification.
When a user was a group owner and now is added to the group members – user does not get notification.
All the actions are logged into Microsoft 365 audit log under your personal Id.
Script samples:
# This script is just a sample to demonstrate basic technique on deletion m365 groups with PowerShell and MS Graph
# please do not run this script as is, but update it upon your needs
# authentication with personal Id
# app must have as minimum "Group.ReadWrite.All" Microsoft Graph API delegated permission
# user must have SharePoint admin (or Teams admin) roles activated
Connect-MgGraph -ClientId $clientid -TenantId $tenantId
Get-MgContext | Select-Object Scopes -ExpandProperty Scopes
# sample data
$groups = @()
$groups += [PSCustomObject]@{GroupId = '443d22ae-683a-4fe4-8875-7bd78227a026' }
$groups += [PSCustomObject]@{GroupId = 'e5805388-c18c-48c0-b42d-6223cf8f3d82' }
# Get Groups
foreach ($group in $groups) {
Get-MgGroup -GroupId $group.GroupId
}
# add members to the group
$groupId = '443d22ae-683a-4fe4-8875-7bd78227a026'
$userId = 'df74e0d3-d78c-495b-b47a-549437d93cf7' # Adele
New-MgGroupMember -GroupId $groupId -DirectoryObjectId $userId
# add Owner to the group
$groupId = '443d22ae-683a-4fe4-8875-7bd78227a026'
$userId = 'eacd52fb-5ae0-45ec-9d17-5ded9a0b9756' # Megan
New-MgGroupOwner -GroupId $groupId -DirectoryObjectId $userId
# Delete group
# add Owner to the group
$groupId = '443d22ae-683a-4fe4-8875-7bd78227a026'
Remove-MgGroup -GroupId $groupId
Sites.Selected permissions are required for the non-interactive application to get access to a specific SharePoint site using Microsoft Graph API and/or SharePoint API.
Below are the steps to get access to SharePoint site with Sites.Selected API permissions:
1. Register an application in Azure (via Azure portal GUI, PowerShell script or helpdesk/servicedesk request), e.g. with GUI you’d login to portal.azure.com, the search for “App registrations” and select “+ New registration”:
2. Update the app “API permissions” – so both – MS Graph API Sites.Selected and SharePoint Sites.Selected application API permissions are configured:
Provide or request tenant admin consent for your API permissions. Finally your app registration “API permissions” should look like:
3. Under Certificates and secrets – generate client secret, copy secret value to safe location.
4. At the Overview page – grab your app client id and tenant id :
At this moment, having tenant id, app (client) id and client secret – you should be able to authenticate against Microsoft 365 tenant with app-only authentication path.
But! Having just Sites.Selected API permissions configured for app does not mean your app has access to SharePoint site. Access for the app to a specific site is provided by SharePoint team using PowerShell script or Graph API calls. That leads us to the next step.
5. Request access for the app to the SharePoint site (your SharePoint service admin should be able to do that via PowerShell script or Graph API calls ) – Here is the Graph API – Here is PowerShell PNP cmdlet
Obviously, read role allows an app to read site content; Write role is similar to “Contributor” user permissions – it allous CRUD operations agains list items (library documents and metadata), but does not allow create/update/delete or configure site lists/libraries or site itself. For this – you’d need Owner/Manage/FullControl role.
6. Once your SharePoint tenant/service admin confirmed that access has been provided – you can use app client id and client secret to work with SharePoint from your code using Graph API. If any concerns – you can validate your app access to the target SharePoint site with simple PowerShell scripts: here is the sample code
7. Secure your certificate and/or secret You do not hard-code secrets. Consider using vault to keep certificate/secret. If you host your application in Azure – consider using managed identity.
Note 1: Sites.Selected API permissions allows you call Microsoft Graph API with client Id and client secret. Calling SharePoint API with client secret is not supported. You have to use client id and certificate to call SharePoint API having app with Sites.Selected permissions.
Call SharePoint API with client Id and client secret is possible only if ACS-based permissions are provided for the app to the site, which is not recommended (see below).
Update: Microsoft announced decommissioning of ACS permissions. So do not use ACS for any new development.
It might be acceptable to provide ACS permissions for existing custom apps or 3-rd party or Microsoft apps/web apps (e.g. Alteryx, Azure Data factory) – apps that only support client id and secret and using SharePoint API under the hood – but just to not to break business processes and being aware of ACS EOL and planning a replacement of theses apps before 2026.
How do we know when the SharePoint site was last updated?
We have multiple “when the site was modified last time” properties – e.g. some we can retrieve with SharePoint CSOM:
Site LastContentModifiedDate
Web LastItemModifiedDate
Web LastItemUserModifiedDate
Also we can get
MS Graph site object with LastModifiedDateTime property
get usage reports via Microsoft Graph (activity reports), and
use “Last activity” field via Admin Center GUI
On the other hand – we can view and modify site in multiple ways – visit site home page, open and/or update document/list item, change site/library settings, configure site permissions, assign site sensitivity label, setup site property and so on.
Question: which site “last modified” or “last activity” properties reflect what events/actions?
This might be important if we think of retention policies, or any kind of clean-up processes… Let say, we are getting report on abandoned sites (inactive sites), but we are also assigning sites sensitivity labels, or we are updating site custom properties (e.g. for adaptive scopes), we have an ownerless groups policy working etc.
What if we assign site sensitivity label to an old inactive (5 years old) site – would it affect retention policy since site was updated this way?
Results
So i did some tests and based on detailed results below, it seems like
Web LastItemModifiedDate is triggered when user just visited site (but property LastItemUserModifiedDate is not triggered)
If a document or list Item updated by user or app – all properties are triggered
MS Graph site property LastModifiedDateTime, root web property LastItemModifiedDate and Site LastContentModifiedDate – same values
If site custom property is updated – it does not affect any site “last modified” property
The same for sensitivity label updated by app – it does not affect any site “last modified” property
The same for Microsoft ownerless groups policy – when user accept or decline group membership – no site “last modified” properties are changed (the same is true for Microsoft 365 group last modified date/time property).
Please refer to the table below
Detailed test results
Test results if the event triggers property update:
Event
Last Content Modified Date
Last Item Modified Date
Last Item User Modified Date
Graph Last Modified DateTime
GUI Last activity
Page viewed by user
Yes
Yes
No
Yes
Home Page viewed by user
Site Page viewed by user
Document or list item updated by user
Yes
Yes
Yes
Yes
Document or list item updated by app
Yes
Yes
Yes
Yes
Site config settings updated by user
Site config settings updated by app
Site custom property updated by app
No
No
No
No
Site Sensitivity label updated by user via SharePoint
Yes
No
No
No
Site/Group Sensitivity label updated by user via Teams
Site/Group Sensitivity label updated by user via Azure
No
No
No
No
Site Sensitivity label updated by app
No
No
No
No
Site collection admin updated by user
Yes
Yes
No
Yes
Site collection admin updated by app
Yes
Yes
No
Yes
SharePoint group membership updated by user
Yes
Yes
No
Yes
Standalone Site connected to a group by user
Yes
Yes
Yes
Yes
Add Microsoft Teams to Site by User
Yes
Yes
Yes
Yes
Update m365 group membership via M365 admin console by admin
Yes
Yes
No
Yes
Update m365 group membership via Azure by admin
Update m365 group membership via Teams by user
No
No
No
Yes
Update m365 group membership via App
Accept group ownership invitation sent by ownerless groups policy
No
No
No
No
Decline group ownership invitation sent by ownerless groups policy
After many years working with SharePoint I wrote a lot of PowerShell scripts that help me support, troubleshoot, administer and secure SharePoint. So I’m sharing my scripts with you.