If you need to automate permission in SharePoint reliably, use Microsoft Graph with .NET 8 and Azure-managed identities. The goal: grant site-scoped access (least privilege via Sites.Selected), verify effective roles, and perform read/write operations without client secrets.
The Problem
SharePoint permissions are often over-provisioned or managed manually. That leads to audit gaps, break-glass patterns, and production drift. You need a repeatable, least-privilege approach that grants only the required access to specific sites, automates verification, and avoids client secrets.
Prerequisites
Required tools and permissions:
- .NET 8 SDK
- Azure CLI v2.58+ (logged in as a tenant admin for one-time grants)
- Microsoft Graph application permissions consent capability (tenant admin)
- Azure subscription access to create a user-assigned managed identity (Contributor on resource group)
The Solution (Step-by-Step)
Step 1. Choose the authentication model
Use managed identity for workloads in Azure (Functions, Container Apps). This removes client secrets entirely. For CI/CD, use workload identity federation instead of secrets.
- Runtime principal: user-assigned/system-assigned managed identity
- Graph permission model: application permission Sites.Selected for the runtime principal
- Grant site-scoped roles: read or write at the specific SharePoint site level
Why Sites.Selected: it blocks blanket access (e.g., Sites.Read.All) and forces explicit grants per site.
Step 2. Infrastructure as Code (Bicep): create a user-assigned managed identity
// main.bicep
targetScope = 'resourceGroup'
// User-assigned managed identity that will call Microsoft Graph
resource uami 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = {
name: 'sp-sites-selected-uami'
location: resourceGroup().location
}
output uamiClientId string = uami.properties.clientId
output uamiPrincipalId string = uami.properties.principalId
After deployment, assign Microsoft Graph application permission Sites.Selected to the managed identity’s service principal. This is a one-time admin action.
# Assign Graph app role (Sites.Selected) to the managed identity service principal
# 1) Get Graph service principal (well-known)
GRAPH_SP_ID=$(az ad sp list --filter "appId eq '00000003-0000-0000-c000-000000000000'" --query "[0].id" -o tsv)
# 2) Get the Sites.Selected app role ID
SITES_SELECTED_ROLE_ID=$(az ad sp show --id $GRAPH_SP_ID --query "appRoles[?value=='Sites.Selected' && allowedMemberTypes[@]=='Application'].id" -o tsv)
# 3) Get your managed identity's service principal object id
UAMI_PRINCIPAL_ID=<uamiPrincipalId from bicep output>
# 4) Assign the app role to the managed identity
az ad sp add-approle-assignment \
--id $UAMI_PRINCIPAL_ID \
--principal-object-id $UAMI_PRINCIPAL_ID \
--resource-id $GRAPH_SP_ID \
--app-role-id $SITES_SELECTED_ROLE_ID
Admin consent is implicit when you add an app role assignment to Graph for an enterprise application (service principal). Validate in Entra ID under Enterprise applications > Your Managed Identity > Permissions.
Step 3. One-time site-scoped grant to the managed identity
Sites.Selected requires a per-site grant. Use an admin-only tool to grant read/write on a single site to the managed identity. Below is a .NET 8 minimal API you can run locally as a tenant admin via Azure CLI authentication to grant permissions. It uses DI, file-scoped namespaces, and the Microsoft Graph SDK.
using Azure.Identity;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Graph;
using Microsoft.Graph.Models;
using Microsoft.Kiota.Abstractions.Authentication;
namespace SharePointPermAdmin;
var builder = WebApplication.CreateBuilder(args);
// Admin credential for local, one-time operations only.
// Uses Azure CLI token of a tenant admin. No client secrets.
builder.Services.AddSingleton((sp) =>
{
// Authenticate as the signed-in Azure CLI account
var credential = new AzureCliCredential();
// Adapter for Microsoft Graph SDK
var authProvider = new TokenCredentialAuthenticationProvider(
credential,
// Microsoft Graph default scope for app-permission endpoints
new[] { "https://graph.microsoft.com/.default" });
return new GraphServiceClient(authProvider);
});
var app = builder.Build();
// Code Overview: Grants site-level permission (read or write) to a target application or managed identity
// Endpoint parameters:
// - hostname: e.g., "contoso.sharepoint.com"
// - sitePath: e.g., "/sites/Engineering"
// - targetAppId: the clientId of the target app or managed identity (UAMI clientId)
// - role: "read" or "write" (least privilege)
app.MapPost("/grant", async (GraphServiceClient graph,
string hostname, string sitePath, string targetAppId, string role) =>
{
// 1) Resolve the site by hostname and site path
var site = await graph.Sites["{hostname}:{sitePath}"].GetAsync();
if (site is null) return Results.NotFound("Site not found.");
// 2) Prepare the permission grant
var requestedRole = role.Equals("write", StringComparison.OrdinalIgnoreCase) ? "write" : "read";
var permission = new Permission
{
// Roles supported for Sites.Selected are "read" and "write"
Roles = new List<string> { requestedRole },
GrantedToIdentities = new List<IdentitySet>
{
new IdentitySet
{
Application = new Identity
{
// targetAppId can be the clientId of a user-assigned managed identity
// or an app registration clientId
Id = targetAppId,
DisplayName = "Sites.Selected App"
}
}
}
};
// 3) Grant the permission at the site-level
var created = await graph.Sites[site.Id].Permissions.PostAsync(permission);
// 4) Return what was granted for audit
return Results.Ok(new
{
SiteId = site.Id,
GrantedRoles = created?.Roles,
GrantedTo = created?.GrantedToIdentities?.Select(x => x.Application?.Id)
});
});
app.Run();
Run this locally with an Azure CLI context that has tenant admin privileges. This API grants the site-scoped role to your managed identity (identified by its clientId). Keep this tool restricted and audit its use.
Step 4. Runtime workload: access the site using the managed identity
Your production service (Azure Functions or Container Apps) uses DefaultAzureCredential to get a token for Graph and perform read actions (if granted read) or write actions (if granted write).
using Azure.Identity;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Graph;
using Microsoft.Kiota.Abstractions.Authentication;
namespace SharePointRuntime;
var builder = WebApplication.CreateBuilder(args);
// Graph client using managed identity in Azure (no secrets)
// In local dev, DefaultAzureCredential falls back to Azure CLI login.
builder.Services.AddSingleton((sp) =>
{
var credential = new DefaultAzureCredential();
var authProvider = new TokenCredentialAuthenticationProvider(
credential,
new[] { "https://graph.microsoft.com/.default" });
return new GraphServiceClient(authProvider);
});
var app = builder.Build();
// Code Overview: Example read-only endpoint listing drive root items for a given site.
// Requires the managed identity to have Sites.Selected + "read" on that site.
app.MapGet("/sites/{hostname}/{*sitePath}/drive-items", async (GraphServiceClient graph, string hostname, string sitePath) =>
{
// 1) Resolve site by hostname and site path
var site = await graph.Sites[$"{hostname}:{sitePath}"].GetAsync();
if (site is null) return Results.NotFound("Site not found.");
// 2) Read drive root items (read permission is sufficient)
var items = await graph.Sites[site.Id].Drive.Root.Children.GetAsync();
return Results.Ok(items?.Value?.Select(i => new { i.Id, i.Name, i.Size, i.LastModifiedDateTime }));
});
app.Run();
Step 5. Optional: strict front-end validation for an internal admin form
If you build an admin UI to call the grant API, validate inputs with Zod and strict TypeScript types.
import { z } from 'zod';
// Discriminated union for role
const RoleSchema = z.union([z.literal('read'), z.literal('write')]);
// Strict payload schema
export const GrantPayloadSchema = z.object({
hostname: z.string().min(1).regex(/^([a-zA-Z0-9-]+)\.sharepoint\.com$/),
sitePath: z.string().min(1).startsWith('/'),
targetAppId: z.string().uuid(),
role: RoleSchema
});
export type GrantPayload = z.infer<typeof GrantPayloadSchema>;
// Example safe submit
export async function submitGrant(payload: GrantPayload) {
const parsed = GrantPayloadSchema.parse(payload); // throws on invalid input
const res = await fetch('/grant?'
+ new URLSearchParams({
hostname: parsed.hostname,
sitePath: parsed.sitePath,
targetAppId: parsed.targetAppId,
role: parsed.role
}), { method: 'POST' });
if (!res.ok) throw new Error('Grant failed');
return res.json();
}
Best Practices & Security
- Best Practice: Use Sites.Selected and grant per-site roles (read/write) instead of tenant-wide Sites.Read.All or Sites.FullControl.All.
- Best Practice: Prefer managed identity (Azure) or workload identity federation (CI/CD) over client secrets.
- Best Practice: Separate duties. Keep the grant tool under tenant admin control; the runtime app should never be able to self-elevate.
- Best Practice: Log every permission grant with who, what, when, and siteId. Store in an immutable audit store.
- Best Practice: Implement retries with exponential backoff for Graph calls and handle 429/5xx responses gracefully.
- Best Practice: Validate all inputs server-side. Reject unknown hostnames and unexpected site paths.
- Best Practice: Monitor with Microsoft 365 audit logs and alert on unexpected grants or role changes.
Minimum permissions required (explicit)
- Runtime principal (managed identity): Microsoft Graph application permission Sites.Selected.
- Admin/grant principal: Microsoft Graph application permission Sites.FullControl.All or delegated SharePoint admin role sufficient to create site-level grants via Graph.
- SharePoint site roles used for Sites.Selected: read (equivalent to site Read), write (equivalent to Edit). Grant the lowest role that satisfies requirements.
Error handling notes
- Catch GraphServiceException and map common statuses: 403 (insufficient role), 404 (site not found), 429 (throttle) with Retry-After.
- Add circuit breakers/timeouts. Do not leak raw exception messages in responses.
- Return correlation IDs in responses to aid troubleshooting.
Summary
- Grant permission in SharePoint using Sites.Selected for least privilege and auditability.
- Use managed identities or workload identity federation to remove secrets and simplify compliance.
- Automate grants and access with .NET 8, Microsoft Graph SDK, and IaC for consistent, repeatable operations.