name | description | page_type | languages | products | urlFragment | ||||||
---|---|---|---|---|---|---|---|---|---|---|---|
Azure Function app for SharePoint webhooks |
This quickstart uses azd CLI to deploy an Azure Function app that registers and processes SharePoint Online webhooks on your own tenant. |
sample |
|
|
azd-functions-sharepoint-webhooks |
This template is based on this repository. It uses Azure Developer command-line (azd) tools to deploy an Azure function app that registers and processes SharePoint Online webhooks on your own tenant.
It uses the Flex Consumption plan, is written in TypeScript and uses the popular library PnPjs to communicate with SharePoint.
Multiple HTTP-triggered functions are created to show, list, register, process and remove webhooks on your SharePoint lists and document libraries.
When receiving a notification from SharePoint, the service function adds an item to the list webhookHistory
(created if it does not exist), and records the event in Application Insights.
The resources are deployed in Azure with a high level of security:
- The function app connects to the storage account using a private endpoint.
- No public network access is allowed on the storage account.
- All the permissions are granted to the function app's managed identity (no secret, access key or legacy access policy is used).
- All the functions require an app key to be called.
The account running azd
must have at least the following roles to successfully provision the resources:
- Azure role
Contributor
: To create all the resources needed - Azure role
Role Based Access Control Administrator
: To assign roles (to access the storage account and Application Insights) to the managed identity of the function app
-
Run
azd init
from an empty local (root) folder:azd init --template Yvand/azd-functions-sharepoint-webhooks
Supply an environment name, such as
spofuncs-quickstart
when prompted. Inazd
, the environment is used to maintain a unique deployment context for your app. -
Add a file named
local.settings.json
in the root of your project with the following contents, and replace the placeholders with your own values:{ "IsEncrypted": false, "Values": { "AzureWebJobsStorage": "UseDevelopmentStorage=true", "FUNCTIONS_WORKER_RUNTIME": "node", "TenantPrefix": "YOUR_SHAREPOINT_TENANT_PREFIX", "SiteRelativePath": "/sites/YOUR_SHAREPOINT_SITE_NAME" } }
-
Review the file
infra/main.parameters.json
to customize the parameters used for provisioning the resources in Azure. Review this article to manage the azd's environment variables.Important: Ensure the values for
TenantPrefix
andSiteRelativePath
are identical between the fileslocal.settings.json
(used when running the function app locally) andinfra\main.parameters.json
(used to set the environment variables in Azure). -
Install the dependencies and build the function app:
npm install npm run build
It can run either locally or in Azure:
- To run the function app locally: Run
npm run start
. - To provision the resources in Azure and deploy the function app: Run
azd up
.
The authentication to SharePoint is done using DefaultAzureCredential
, so the credential used depends if the function app runs locally, or in Azure.
If you never heard about DefaultAzureCredential
, you should familirize yourself with its concept by reading this article.
DefaultAzureCredential
will preferentially use the delegated credentials of Azure CLI
to authenticate to SharePoint.
Use the Microsoft Graph PowerShell script below to grant the SharePoint delegated permission AllSites.Manage
to the Azure CLI
's service principal:
Connect-MgGraph -Scope "Application.Read.All", "DelegatedPermissionGrant.ReadWrite.All"
$scopeName = "AllSites.Manage"
$requestorAppPrincipalObj = Get-MgServicePrincipal -Filter "displayName eq 'Microsoft Azure CLI'"
$resourceAppPrincipalObj = Get-MgServicePrincipal -Filter "displayName eq 'Office 365 SharePoint Online'"
$params = @{
clientId = $requestorAppPrincipalObj.Id
consentType = "AllPrincipals"
resourceId = $resourceAppPrincipalObj.Id
scope = $scopeName
}
New-MgOauth2PermissionGrant -BodyParameter $params
Warning
The service principal for Azure CLI
may not exist in your tenant. If so, check this issue to add it.
Note
AllSites.Manage
is the minimum permission required to register a webhook.
Sites.Selected
cannot be used because it does not exist as a delegated permission in the SharePoint API.
DefaultAzureCredential
will use a managed identity to authenticate to SharePoint. This may be the existing, system-assigned managed identity of the function app service, or a user-assigned managed identity.
This tutorial will assume that the system-assigned managed identity is used.
Navigate to your function app in the Azure portal > click Identity
and note the Object (principal) ID
of the system-assigned managed identity.
In this tutorial, it is d3e8dc41-94f2-4b0f-82ff-ed03c363f0f8
.
Then, use one of the scripts below to grant this identity the app-only permission Sites.Selected
on the SharePoint API:
Using the Microsoft Graph PowerShell SDK
# This script requires the modules Microsoft.Graph.Authentication, Microsoft.Graph.Applications, Microsoft.Graph.Identity.SignIns, which can be installed with the cmdlet Install-Module below:
# Install-Module Microsoft.Graph.Authentication, Microsoft.Graph.Applications, Microsoft.Graph.Identity.SignIns -Scope CurrentUser -Repository PSGallery -Force
Connect-MgGraph -Scope "Application.Read.All", "AppRoleAssignment.ReadWrite.All"
$managedIdentityObjectId = "d3e8dc41-94f2-4b0f-82ff-ed03c363f0f8" # 'Object (principal) ID' of the managed identity
$scopeName = "Sites.Selected"
$resourceAppPrincipalObj = Get-MgServicePrincipal -Filter "displayName eq 'Office 365 SharePoint Online'" # SPO
$targetAppPrincipalAppRole = $resourceAppPrincipalObj.AppRoles | ? Value -eq $scopeName
$appRoleAssignment = @{
"principalId" = $managedIdentityObjectId
"resourceId" = $resourceAppPrincipalObj.Id
"appRoleId" = $targetAppPrincipalAppRole.Id
}
New-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $managedIdentityObjectId -BodyParameter $appRoleAssignment | Format-List
Using az cli in Bash
managedIdentityObjectId="d3e8dc41-94f2-4b0f-82ff-ed03c363f0f8" # 'Object (principal) ID' of the managed identity
resourceServicePrincipalId=$(az ad sp list --query '[].[id]' --filter "displayName eq 'Office 365 SharePoint Online'" -o tsv)
resourceServicePrincipalAppRoleId="$(az ad sp show --id $resourceServicePrincipalId --query "appRoles[?starts_with(value, 'Sites.Selected')].[id]" -o tsv)"
az rest --method POST --uri "https://graph.microsoft.com/v1.0/servicePrincipals/${managedIdentityObjectId}/appRoleAssignments" --headers 'Content-Type=application/json' --body "{ 'principalId': '${managedIdentityObjectId}', 'resourceId': '${resourceServicePrincipalId}', 'appRoleId': '${resourceServicePrincipalAppRoleId}' }"
Navigate to the Enterprise applications in the Entra ID portal > Set the filter Application type
to Managed Identities
> Click on your managed identity and note its Application ID
.
In this tutorial, it is 3150363e-afbe-421f-9785-9d5404c5ae34
.
Warning
In this step, we will use the Application ID
of the managed identity, while in the previous step we used its Object ID
, be mindful about the risk of confusion.
Then, use one of the scripts below to grant it the app-only permission manage
on a specific SharePoint site:
Note
The managed identity of the function app service is granted SharePoint permission manage
, because it is the minimum required to register a webhook.
Using PnP PowerShell
Connect-PnPOnline -Url "https://YOUR_SHAREPOINT_TENANT_PREFIX.sharepoint.com/sites/YOUR_SHAREPOINT_SITE_NAME" -Interactive -ClientId "YOUR_PNP_APP_CLIENT_ID"
Grant-PnPAzureADAppSitePermission -AppId "3150363e-afbe-421f-9785-9d5404c5ae34" -DisplayName "YOUR_FUNC_APP_NAME" -Permissions Manage
Using m365 cli in Bash
targetapp="3150363e-afbe-421f-9785-9d5404c5ae34"
siteUrl="https://YOUR_SHAREPOINT_TENANT_PREFIX.sharepoint.com/sites/YOUR_SHAREPOINT_SITE_NAME"
m365 spo site apppermission add --appId $targetapp --permission manage --siteUrl $siteUrl
Important
The app registration used to run those commands must have at least the following permissions:
- Delegated permission
Application.ReadWrite.All
in the Graph API - Delegated permission
AllSites.FullControl
in the SharePoint API
For security reasons, when running in Azure, function app requires an app key to pass in query string parameter code
. The app keys can be found in the function app service > App Keys.
Most of the HTTP functions take optional parameters tenantPrefix
and siteRelativePath
. If they are not specified, the values set in the app's environment variables will be used.
Review this README for more information.
You can use the Visual Studio Code extension REST Client
to execute the requests in the .http file.
It takes parameters from a .env file on the same folder. You can create it based on the sample files azure.env.example
and local.env.example
.
Below is a sample script in Bash that calls the function app in Azure using curl
:
# Edit those variables to fit your app function
funchost="YOUR_FUNC_APP_NAME"
code="YOUR_HOST_KEY"
notificationUrl="https://${funchost}.azurewebsites.net/api/webhooks/service?code=${code}"
listTitle="YOUR_SHAREPOINT_LIST"
# List all the webhooks registered on a list
curl "https://${funchost}.azurewebsites.net/api/webhooks/list?code=${code}&listTitle=${listTitle}"
# Register a webhook
curl -X POST "https://${funchost}.azurewebsites.net/api/webhooks/register?code=${code}&listTitle=${listTitle}¬ificationUrl=${notificationUrl}"
# Show this webhook registered on a list
curl "https://${funchost}.azurewebsites.net/api/webhooks/show?code=${code}&listTitle=${listTitle}¬ificationUrl=${notificationUrl}"
# Remove the webhook from the list
# Step 1: Get the webhook id in the output of the function /webhooks/show
webhookId=$(curl -s "https://${funchost}.azurewebsites.net/api/webhooks/show?code=${code}&listTitle=${listTitle}¬ificationUrl=${notificationUrl}" | \
python3 -c "import sys, json; document = json.load(sys.stdin); document and print(document['id'])")
# Step 2: Call function /webhooks/remove and pass the webhookId
curl -X POST "https://${funchost}.azurewebsites.net/api/webhooks/remove?code=${code}&listTitle=${listTitle}&webhookId=${webhookId}"
The same script, which calls the function app when it runs in your local environment:
# Edit those variables to fit your app function
funchost="YOUR_FUNC_APP_NAME"
code="YOUR_HOST_KEY"
notificationUrl="https://${funchost}.azurewebsites.net/api/webhooks/service?code=${code}"
listTitle="YOUR_SHAREPOINT_LIST"
# List all the webhooks registered on a list
curl "http://localhost:7071/api/webhooks/list?listTitle=${listTitle}"
# Register a webhook
curl -X POST "http://localhost:7071/api/webhooks/register?listTitle=${listTitle}¬ificationUrl=${notificationUrl}"
# Show this webhook registered on a list
curl "http://localhost:7071/api/webhooks/show?listTitle=${listTitle}¬ificationUrl=${notificationUrl}"
# Remove the webhook from the list
# Step 1: Get the webhook id in the output of the function /webhooks/show
webhookId=$(curl -s "http://localhost:7071/api/webhooks/show?listTitle=${listTitle}¬ificationUrl=${notificationUrl}" | \
python3 -c "import sys, json; document = json.load(sys.stdin); document and print(document['id'])")
# Step 2: Call function /webhooks/remove and pass the webhookId
curl -X POST "http://localhost:7071/api/webhooks/remove?listTitle=${listTitle}&webhookId=${webhookId}"
When the function app runs in your local environment, the logging goes to the console.
When the function app runs in Azure, the logging goes to the Application Insights resource configured in the app service.
The KQL query below shows the entries from all the HTTP functions, and filters out the logging from the infrastructure:
traces
| where isnotempty(operation_Name)
| project timestamp, operation_Name, severityLevel, message
| order by timestamp desc
The KQL query below does the following:
- Includes only the entries from the function
webhooks/service
(which receives the notifications from SharePoint) - Parses the
message
as a json document (which is how this project writes the messages) - Includes only the entries that were successfully parsed (excludes those from the infrastructure)
traces
| where operation_Name contains "webhooks-service"
| extend jsonMessage = parse_json(message)
| where isnotempty(jsonMessage.['message'])
| project timestamp, operation_Name, severityLevel, jsonMessage.['message'], jsonMessage.['error']
| order by timestamp desc
The Flex Consumption plan is currently in preview, be aware about its current limitations and issues.
You can delete all the resources this project created in Azure, by running the command azd down
.
Alternatively, you can delete the resource group, which has the azd environment's name by default.