Skip to main content

Create an App Registration with Sites.Selected for SharePoint

This guide explains how to:

  1. Create an App Registration in Microsoft Entra ID (formerly Azure AD).
  2. Grant it the Microsoft Graph application permission Sites.Selected.
  3. Give the application read, write, manage, or fullcontrol access to one specific SharePoint site. This guide uses read by default.
  4. Retrieve the required Tenant ID, Client ID, client secret, and Site ID.

1. Concepts and prerequisites​

Why use Sites.Selected?​

Broad permissions such as Sites.Read.All, Sites.ReadWrite.All, and Sites.FullControl.All grant access to every SharePoint site in the tenant. This is rarely desirable.

Sites.Selected grants no access by default. The application can only access sites that have been explicitly assigned to it, with the selected permission level. This follows the recommended principle of least privilege.

Prerequisites​

ItemDetails
Role required to create the appApplication Administrator, Cloud Application Administrator, or Global Administrator
Role required to grant admin consentPrivileged Role Administrator or Global Administrator
Identity used to assign a siteAn application or SharePoint administrator account with Sites.FullControl.All
ToolsA browser (Entra portal) and one of: Graph Explorer, PowerShell (PnP or MSGraph), or an HTTP request (Postman/curl)

The account used in this guide has the Global Administrator role.

2. Create the App Registration​

Use the Entra portal​

  1. Open the Azure portal or the Microsoft Entra admin center.
  2. Search for Microsoft Entra ID, then open the service.
Searching for and opening Microsoft Entra ID in the Azure portal
  1. Select Add > App registration.
Adding an App registration from Microsoft Entra ID
  1. Complete the following fields:
    • Name: for example, svc-sharepoint-myapp.
    • Supported account types: select Single tenant only, unless you specifically need a multi-tenant application.
    • Redirect URI: leave this empty because it is not required for the client credentials flow.
  2. Select Register.
SharePoint application registration form

Retrieve the application identifiers​

On the application Overview page, copy the Application (client) ID and Directory (tenant) ID.

Newly created application with the Client ID and Tenant ID to retrieve
IdentifierLocationPurpose
Application (client) IDOverviewApplication authentication
Directory (tenant) IDOverviewTenant authentication

3. Add the Sites.Selected permission​

  1. In the application, open API permissions > Add a permission.
  2. Select Microsoft Graph.
Adding a Microsoft Graph permission to the application
  1. Select Application permissions, not Delegated permissions, because this is machine-to-machine access.
  2. Search for Sites.Selected, select it, then choose Add permissions.
Selecting the Sites.Selected application permission
  1. Select Grant admin consent for [tenant].
Button used to grant administrator consent for Sites.Selected
  1. Confirm the operation when prompted.
Confirming administrator consent in Microsoft Entra ID

The Status column must display a green check mark and Granted for [tenant].

Sites.Selected permission with administrator consent granted
Administrator consent is required

Without administrator consent, the permission is configured but remains inactive.

4. Create a client secret​

The application needs a credential to authenticate. A certificate is preferable in production; this guide uses a client secret.

  1. From Overview, select Add a certificate or secret. You can also open Manage > Certificates & secrets.
Opening the certificate or client secret creation page
  1. On the Client secrets tab, select New client secret.
  2. Enter a description, select an expiration period that complies with your security policy, then select Add.
Creating a client secret with a description and expiration period
  1. Immediately copy the value in the Value column, not the Secret ID.
Copying the client secret value immediately after creation
The value is displayed only once

The secret value is no longer available after you leave this page. Store it in a secrets vault such as Azure Key Vault.

Authentication details​

ItemDescription
tenantIdDirectory (tenant) ID
clientIdApplication (client) ID
clientSecretClient secret value

5. Prepare the site assignment​

Required permission

The assignment must be performed by an identity with Sites.FullControl.All, such as a dedicated administration application.

Available site roles​

RoleAccess
readRead-only access; this is the default used in this guide
writeRead and write access
manageWrite access and selected management operations
fullcontrolFull control, equivalent to a site owner

6. Assign the site with Python​

The process has three stages: obtain an access token, resolve the site-id, and assign the permission.

Identity performing the assignment

The identity calling POST /sites/{site-id}/permissions must have Sites.FullControl.All.

Two approaches are available:

  • Dedicated administration application — recommended: a bootstrap application with Sites.FullControl.All grants access to the target application that has Sites.Selected. ADMIN_CLIENT_ID and ADMIN_CLIENT_SECRET are therefore different from TARGET_CLIENT_ID.
  • Temporary self-assignment: temporarily grant Sites.FullControl.All to the target application, assign the site, then remove the tenant-wide permission. This temporarily weakens the least-privilege model.

This guide uses a dedicated administration application. Create a second App Registration, then add the Sites.FullControl.All application permission.

Adding Sites.FullControl.All to the administration application

Grant administrator consent, create a client secret, and retrieve the administration application's Tenant ID, Client ID, and secret value.

Environment variables​

Create a .env file:

TENANT_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
# Identity performing the grant; requires Sites.FullControl.All
ADMIN_CLIENT_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
ADMIN_CLIENT_SECRET=admin-application-secret
# Target application with Sites.Selected
TARGET_CLIENT_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
TARGET_APP_NAME=svc-sharepoint-myapp
SITE_URL=https://contoso.sharepoint.com/sites/Finance
GRANT_ROLES=read

Create requirements.txt:

requests
python-dotenv

grant_sharepoint_permissions.py​

"""
Manages permissions (read/write/manage/fullcontrol) for an Azure AD app
on a specific SharePoint site through Microsoft Graph (Sites.Selected).

Available actions:
list – lists all site permissions
grant – grants the roles in GRANT_ROLES to the target app
revoke – revokes the target app permission (by permission ID or client ID)

Usage :
python grant_sharepoint_permissions.py list
python grant_sharepoint_permissions.py grant
python grant_sharepoint_permissions.py revoke
python grant_sharepoint_permissions.py revoke --permission-id <id>
"""
import os
import sys
import json
import argparse
import requests
from dotenv import load_dotenv

load_dotenv()

TENANT_ID = os.environ["TENANT_ID"].strip()
ADMIN_CLIENT_ID = os.environ["ADMIN_CLIENT_ID"].strip()
ADMIN_CLIENT_SECRET = os.environ["ADMIN_CLIENT_SECRET"].strip()
TARGET_CLIENT_ID = os.environ["TARGET_CLIENT_ID"].strip()
TARGET_APP_NAME = os.environ.get("TARGET_APP_NAME", "svc-sharepoint-app").strip()
SITE_URL = os.environ["SITE_URL"].strip()
GRANT_ROLES = [r.strip() for r in os.environ.get("GRANT_ROLES", "read,write").split(",") if r.strip()]

# Derives the host and path from SITE_URL
# ex. https://contoso.sharepoint.com/sites/Finance
_url_parts = SITE_URL.replace("https://", "").split("/", 1)
SP_HOST = _url_parts[0] # contoso.sharepoint.com
SP_SITE_PATH = _url_parts[1] if len(_url_parts) > 1 else "" # sites/Finance

GRAPH = "https://graph.microsoft.com/v1.0"


# ── Step 1: access token ──────────────────────────────────────────────────
def get_token() -> str:
print("→ Retrieving access token …")
url = f"https://login.microsoftonline.com/{TENANT_ID}/oauth2/v2.0/token"
resp = requests.post(url, data={
"grant_type": "client_credentials",
"client_id": ADMIN_CLIENT_ID,
"client_secret": ADMIN_CLIENT_SECRET,
"scope": "https://graph.microsoft.com/.default",
})
resp.raise_for_status()
print(" ✓ Token obtained")
return resp.json()["access_token"]


# ── Step 2: resolve site ID ─────────────────────────────────────────
def get_site_id(token: str) -> str:
print(f"→ Resolving site ID for {SP_HOST}/{SP_SITE_PATH} …")
url = f"{GRAPH}/sites/{SP_HOST}:/{SP_SITE_PATH}"
resp = requests.get(url, headers={"Authorization": f"Bearer {token}"})
resp.raise_for_status()
site_id = resp.json()["id"]
print(f" ✓ site-id = {site_id}")
return site_id


# ── Action : list ────────────────────────────────────────────────────────────
def list_permissions(token: str, site_id: str) -> None:
print(f"→ Listing permissions granted to app {TARGET_CLIENT_ID} ({TARGET_APP_NAME}) …")
url = f"{GRAPH}/sites/{site_id}/permissions"
resp = requests.get(url, headers={"Authorization": f"Bearer {token}"})
try:
resp.raise_for_status()
except requests.HTTPError:
print(f"\n ✗ HTTP {resp.status_code}")
print(json.dumps(resp.json(), indent=2))
sys.exit(1)

all_permissions = resp.json().get("value", [])

# Filter: only permissions whose application matches TARGET_CLIENT_ID
matched = []
for perm in all_permissions:
identities = perm.get("grantedToIdentities") or []
if not identities and perm.get("grantedTo"):
identities = [perm["grantedTo"]]
for identity in identities:
app = identity.get("application") or {}
if app.get("id") == TARGET_CLIENT_ID:
matched.append(perm)
break

if not matched:
print(f" (no permissions found for app {TARGET_CLIENT_ID})")
return

print(f" ✓ {len(matched)} permission(s) found for this app :\n")
for perm in matched:
perm_id = perm.get("id", "—")
roles = ", ".join(perm.get("roles", []))
identities = perm.get("grantedToIdentities") or []
if not identities and perm.get("grantedTo"):
identities = [perm["grantedTo"]]
for identity in identities:
app = identity.get("application") or {}
name = app.get("displayName") or "—"
cid = app.get("id") or "—"
print(f" ┌ permission id : {perm_id}")
print(f" │ roles : {roles}")
print(f" │ name : {name}")
print(f" └ client id : {cid}")
print()


# ── Action : grant ───────────────────────────────────────────────────────────
def grant_permissions(token: str, site_id: str) -> dict:
print(f"→ Granting {GRANT_ROLES} to app {TARGET_CLIENT_ID} ({TARGET_APP_NAME}) …")
url = f"{GRAPH}/sites/{site_id}/permissions"
payload = {
"roles": GRANT_ROLES,
"grantedToIdentities": [
{
"application": {
"id": TARGET_CLIENT_ID,
"displayName": TARGET_APP_NAME,
}
}
],
}
resp = requests.post(
url,
headers={
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
},
json=payload,
)
try:
resp.raise_for_status()
except requests.HTTPError:
print(f"\n ✗ HTTP {resp.status_code}")
print(json.dumps(resp.json(), indent=2))
sys.exit(1)

result = resp.json()
print(" ✓ Permission granted successfully!")
print(f" → permission id = {result.get('id')}")
print(json.dumps(result, indent=2))
return result


# ── Action : revoke ──────────────────────────────────────────────────────────
def revoke_permissions(token: str, site_id: str, permission_id: str | None = None) -> None:
"""
Revokes a permission.
- If permission_id is provided, deletes that permission directly.
- Otherwise, finds all permissions whose client ID matches
TARGET_CLIENT_ID and deletes them.
"""
headers = {"Authorization": f"Bearer {token}"}

if permission_id:
perm_ids_to_delete = [permission_id]
else:
print(f"→ Finding permissions for app {TARGET_CLIENT_ID} …")
url = f"{GRAPH}/sites/{site_id}/permissions"
resp = requests.get(url, headers=headers)
try:
resp.raise_for_status()
except requests.HTTPError:
print(f"\n ✗ HTTP {resp.status_code}")
print(json.dumps(resp.json(), indent=2))
sys.exit(1)

permissions = resp.json().get("value", [])
perm_ids_to_delete = []
for perm in permissions:
identities = perm.get("grantedToIdentities") or []
if not identities and perm.get("grantedTo"):
identities = [perm["grantedTo"]]
for identity in identities:
app = identity.get("application") or {}
if app.get("id") == TARGET_CLIENT_ID:
perm_ids_to_delete.append(perm["id"])
break

if not perm_ids_to_delete:
print(f" ✗ No permissions found for app {TARGET_CLIENT_ID} on this site.")
sys.exit(1)

print(f" ✓ {len(perm_ids_to_delete)} permission(s) to revoke : {perm_ids_to_delete}")

for pid in perm_ids_to_delete:
print(f"→ Revoking permission {pid} …")
url = f"{GRAPH}/sites/{site_id}/permissions/{pid}"
resp = requests.delete(url, headers=headers)
try:
resp.raise_for_status()
except requests.HTTPError:
print(f"\n ✗ HTTP {resp.status_code}")
try:
print(json.dumps(resp.json(), indent=2))
except Exception:
print(resp.text)
sys.exit(1)
print(f" ✓ Permission {pid} revoked successfully!")


# ── Entry point ───────────────────────────────────────────────────────────
def main() -> None:
parser = argparse.ArgumentParser(
description="Manages Sites.Selected permissions on a SharePoint site through Microsoft Graph.",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
python grant_sharepoint_permissions.py list
python grant_sharepoint_permissions.py grant
python grant_sharepoint_permissions.py revoke
python grant_sharepoint_permissions.py revoke --permission-id abc123
""",
)
parser.add_argument(
"action",
nargs="?",
default="list",
choices=["list", "grant", "revoke"],
help="Action to perform: list (default) | grant | revoke",
)
parser.add_argument(
"--permission-id",
dest="permission_id",
default=None,
help="(revoke only) ID of the permission to revoke. "
"If omitted, all TARGET_CLIENT_ID permissions are revoked.",
)
args = parser.parse_args()

tok = get_token()
sid = get_site_id(tok)

if args.action == "list":
list_permissions(tok, sid)
elif args.action == "grant":
grant_permissions(tok, sid)
elif args.action == "revoke":
revoke_permissions(tok, sid, permission_id=args.permission_id)


if __name__ == "__main__":
main()

Installation​

Install dependencies in a dedicated virtual environment (venv), using pip or pip3 depending on your system.

# 1) Create the virtual environment
python3 -m venv .venv # or: python -m venv .venv

# 2) Activate the virtual environment
source .venv/bin/activate # macOS / Linux
# .venv\Scripts\activate # Windows (PowerShell / CMD)

# 3) Install dependencies from requirements.txt
pip install -r requirements.txt # or: pip3 install -r requirements.txt

# 4) Run the script (no argument defaults to the list action)
python grant_sharepoint_permissions.py # or: python3 grant_sharepoint_permissions.py

Available subcommands​

The script accepts an action argument. list is the default action: running the script without an argument is equivalent to list.

# These two commands are equivalent
python grant_sharepoint_permissions.py
python grant_sharepoint_permissions.py list

Complete command reference:

CommandEffect
python grant_sharepoint_permissions.pyLists the app permissions (default: list)
python grant_sharepoint_permissions.py listSame as above
python grant_sharepoint_permissions.py grantGrants the roles in GRANT_ROLES to TARGET_CLIENT_ID
python grant_sharepoint_permissions.py revokeRevokes all permissions for TARGET_CLIENT_ID
python grant_sharepoint_permissions.py revoke --permission-id <id>Revokes one specific permission by its id
Find a permission ID

Run list first to retrieve permission IDs before revoking a specific permission.

Useful variations​

  • Change the permission level: edit GRANT_ROLES in .env (read, read,write, manage, fullcontrol).
  • Self-assignment (case B): use the same values for ADMIN_CLIENT_ID/SECRET and TARGET_CLIENT_ID (the app must then have Sites.FullControl.All).

7. Troubleshooting​

AADSTS7000215 — invalid client secret​

The secret is incorrect or has expired. Make sure you copied the secret Value, not its Secret ID, and check for leading or trailing spaces.

403 Forbidden when accessing the site​

The token is valid, but the target application does not have the required permission on that site. Run list, then assign the permission again with grant. Remember that Sites.Selected only provides access to explicitly assigned sites.

Authorization_RequestDenied during grant​

The identity performing the assignment does not have Sites.FullControl.All. The administration application identified by ADMIN_CLIENT_ID needs this application permission with administrator consent, not the target application.

8. Information to retain​

ItemSourceSensitive?
Tenant IDApplication OverviewNo
Client IDApplication OverviewNo
Client secret or certificateCertificates & secretsYes — store it in a secrets vault
Site IDResponse from GET /sites/...No
Permission IDResponse from POST /permissionsNo, but useful for revocation