---
name: microsoft-sharepoint-oauth-app
description: >-
  Registers a Microsoft Entra ID application for SharePoint Online access
  through Microsoft Graph. Builds on the `microsoft-entra-app-registration`
  base skill; read that first. This skill covers only what SharePoint adds:
  the `Sites.Selected` per-site grant model and the three conditions it needs,
  the broad `Sites.*.All` alternatives and how security reviews react to them,
  the Selected-scopes family and its permission-inheritance cost, the retired
  Azure ACS / SharePoint Add-In model, Graph site/drive/list addressing, and
  SharePoint throttling. Use when asked to get SharePoint OAuth credentials,
  connect SharePoint Online or OneDrive-for-Business document libraries, set
  up an Entra app for SharePoint sites, lists or drives, fix a SharePoint
  connector returning `403 accessDenied`, or migrate off ACS / Add-In auth.
  Prefer this over the generic Microsoft skill whenever SharePoint sites or
  `Sites.Selected` are involved.
---

# SharePoint (Microsoft Entra ID) OAuth2 App Registration

Get a working OAuth2 client for SharePoint Online — an Entra ID app registration with the right `Sites.*` permissions
— for a platform that connects many customers' SharePoint tenants.

Registering the app is the base skill's job and takes twenty minutes. **The permission you request is the whole
negotiation**, and it is what this file is about. SharePoint's tenant-wide scopes grant your app every site collection
in the tenant, including every user's OneDrive, and customer security teams increasingly refuse them outright. The
least-privilege alternative, `Sites.Selected`, grants nothing on consent and then needs an out-of-band step from every
customer's admin. That trade decides whether a customer can say yes, and no amount of portal work substitutes for it.

## Built on: `microsoft-entra-app-registration`

**Read the base skill first.** It is the source of truth for the Entra mechanics this one assumes:

- **Supported account types** — single tenant vs multitenant vs personal Microsoft accounts, the permission ceilings
  each imposes, and `AADSTS50194` when a single-tenant app is called through `/common`.
- **Redirect URIs** — the Web vs SPA vs public-client platform choice (SPA cannot carry a client secret and forces
  PKCE), exact case-sensitive matching, the count and length limits, and the Unified.to callback hosts.
- **Delegated vs application permissions, and admin consent** — how the two modes differ, which permissions need an
  admin, and that Microsoft Graph *application* permissions require a **Privileged Role Administrator**, not an
  Application Administrator. That one stalls SharePoint runs more than any other.
- **Client secrets and certificates** — the 24-month cap, the **Value** shown exactly once, `AADSTS7000222` on
  expiry, add-then-delete rotation, and the per-cloud authority and Graph hosts (commercial, US Gov L4/L5, China).
- **`offline_access`, publisher verification and the generic AADSTS symptom table** — `Sites.*` scopes carry no
  refresh token of their own; `offline_access` is what gets you one.

The base also holds the inputs to collect, the run order, tenant ownership, the credential hand-off and the generic
stop-and-ask conditions. Everything below is SharePoint-specific.

## Extra inputs to collect

On top of the base skill's batch:

| Input | Notes |
| --- | --- |
| **`Sites.Selected` or tenant-wide `Sites.*.All`?** | Blocking product decision — read §2 first |
| **If `Sites.Selected`: which site(s), and who will stamp the grant?** | Needs a SharePoint Administrator per customer (§2) |
| **Read-only, or read/write?** | Decides the role on the per-site grant and the `Sites.*` verb (§1, §2) |

Two base-skill choices have SharePoint-specific answers. **Supported account types**: SharePoint Online sites belong
to work/school tenants, so the personal-account audiences buy nothing and cost something — pick **Multiple Entra ID
tenants**. And on **reuse vs new registration**: a new client ID is worse here than for most vendors, because every
`Sites.Selected` grant is customer-side state your platform cannot recreate. Each one must be re-stamped, per site,
by each customer's admin.

## Platform state (verified 2026-09-20 — re-verify before trusting)

- **Azure ACS for SharePoint Online is retired.** Deprecated 2023-11-27; stopped working for new tenants 2024-11-01;
  **fully retired April 2, 2026**. Microsoft states plainly there is no option to extend ACS with SharePoint Online
  beyond that date, and it covers all environments including Government and DoD clouds. The replacement for granting
  an application access to SharePoint is **Microsoft Entra ID**. Some Microsoft pages still carry pre-retirement
  future-tense wording ("will stop working from April 2nd 2026") because the articles have not been re-edited — the
  date has passed; treat ACS as gone.
- **SharePoint Add-Ins are retired on the same date.** No new marketplace listings since 2024-03-01, no marketplace
  acquisition since 2024-07-01, stopped working for all tenants 2026-04-02. The replacement for in-page extensibility
  is SharePoint Framework (SPFx); the replacement for a provider-hosted add-in's auth is an Entra ID app. SharePoint
  **on-premises** add-ins are *not* retired and keep working from an on-prem app catalog.
- **Remote event receivers** registered against an Entra application get a longer runway — they work until
  **July 1, 2027**. Ones registered through ACS died with ACS. For new work, use SharePoint webhooks or Microsoft
  Graph change notifications.
- **CSOM, JSOM and the SharePoint REST API are not retired**, but Microsoft recommends Microsoft Graph first: Graph
  costs fewer throttling resource units for the same work, and CSOM/REST carry extra internal limits (§4).
- **`*.Selected` scopes now cover more than sites and support both modes.** Beyond `Sites.Selected` there are
  `Lists.SelectedOperations.Selected`, `ListItems.SelectedOperations.Selected` and
  `Files.SelectedOperations.Selected`, and as documented all Selected scopes support **delegated and application**
  modes. The newer tuple naming is cosmetic — no functional difference from the `Sites.Selected` form.

If the Entra admin center or Graph does not behave like this, stop and report what you actually see.

## 1. The `Sites.*` family

SharePoint access is requested as Microsoft Graph permissions. The site-level permissions, in the order customer
admins will tolerate them:

| Permission | What it reaches |
| --- | --- |
| **`Sites.Selected`** | **Nothing until an admin grants a specific site** — see §2 |
| `Sites.Read.All` | Read **every** site collection in the tenant, including every user's OneDrive |
| `Sites.ReadWrite.All` | Read/write every site collection |
| `Sites.Manage.All` | The above plus list/library management |
| `Sites.FullControl.All` | Full control of every site collection — also what it takes to *stamp* a `Sites.Selected` grant (§2) |
| `Files.Read.All` / `Files.ReadWrite.All` | Files across sites and OneDrive, independent of the `Sites.*` family |

Identity-only scopes (`openid`, `profile`, `email`, `offline_access`, `User.Read`) carry no SharePoint access.

**Mixing defeats the point.** Any scope present in the token is honored, so requesting `Sites.Selected` *alongside*
`Sites.Read.All` or `Files.Read.All` does not produce a least-privilege app — it produces a tenant-wide app with a
least-privilege label, and a customer security review will find it. If you go the `Sites.Selected` route, the broad
scopes must come out of the request.

**Product fact — as of 2026-09-20.** Unified.to's SharePoint connector requests two different things depending on how
the connection is made:

- Its **delegated (user-authorizes) OAuth2 flow** requests the **broad tenant-wide** kind — per object:
  `sites.read.all` for reading SharePoint sites and pages; `files.read.all` + `sites.read.all` for reading files, and
  `files.readwrite.all` + `sites.readwrite.all` for writing them; plus `Directory.Read.All`, `User.Read.All` and
  `Group.Read.All` (and the `ReadWrite` equivalents) where it reads directory users and groups — always with
  `openid`, `profile`, `email`, `User.Read` and `offline_access`, and a login-only flow that requests just `openid`,
  `User.Read`, `email`, `profile`. It sends `prompt=select_account` on authorize and ships Unified.to's own shared
  client credentials by default.
- Its **app-only (client credentials) connection** is the least-privilege kind: the customer registers **their own**
  Entra app holding only **`Sites.Selected`**, gives admin consent, has an admin stamp a per-site grant, and supplies
  a client ID, client secret, tenant ID and a **single Graph site id** that anchors all file access to that one site.

That split predicts the reaction you will get. Expect the delegated scope list — `Sites.ReadWrite.All` and
`Directory.Read.All` in particular — to be refused or escalated by any customer with a security review, and expect
the app-only path to be the one that gets approved. **Confirm the current scope set with the connector's owner before
you register anything**; scope lists change and this one is a snapshot.

## 2. `Sites.Selected` — the least-privilege model

This is the section that makes a SharePoint connector approvable. Understand it before choosing a permission.

Consenting `Sites.Selected` grants **zero** access. Access exists only when all **three** of these are true, and
missing any one produces the same opaque `403 accessDenied`:

1. The app is **consented** in Entra ID for the `Sites.Selected` permission (application or delegated). Consent is
   also what creates the service principal in the customer's tenant — until it exists, nothing customer-side can
   reference your app at all.
2. An admin has **granted the app a role on a specific site** via `POST /sites/{siteId}/permissions`.
3. The **token actually carries** the `Sites.Selected` scope on the call.

### Stamping the grant

```http
POST https://graph.microsoft.com/v1.0/sites/{siteId}/permissions
Content-Type: application/json

{
  "roles": ["write"],
  "grantedToIdentities": [{
    "application": {
      "id": "<your app's client ID>",
      "displayName": "<your app name>"
    }
  }]
}
```

- **Roles**: `read`, `write`, `owner`, `fullcontrol`. Pick the weakest that works — `read` for a read-only connector,
  `write` for read/write file sync.
- **Who can call it**: the grant on a *site* requires **`Sites.FullControl.All`**, and in delegated workflows the
  signed-in user must hold **SharePoint Administrator or higher**. Microsoft's own note on why the bar is that high:
  a `Sites.Selected` grant can itself confer full control of a site collection.
- **Where it is done**: this is a Graph API call, not a checkbox in a portal. In practice the customer's admin runs it
  from Graph Explorer or Microsoft Graph PowerShell (`New-MgSitePermission -SiteId $siteId -BodyParameter $params`).
  Your run should hand them the exact request body with the client ID filled in.
- **It is per customer, per site, out of band.** Your platform cannot self-serve this. Design the connect flow to say
  so, and expect onboarding to include a step only the customer's admin can do.
- **Managing it later**: `GET /sites/{siteId}/permissions` lists grants, `DELETE /sites/{siteId}/permissions/{id}`
  revokes one. Revoking the Entra consent kills access to every granted site at once; deleting one permission kills
  only that site.

### Below the site level

The same pattern works at list, item and file level with `Lists.SelectedOperations.Selected`,
`ListItems.SelectedOperations.Selected` and `Files.SelectedOperations.Selected`, granted via
`POST /sites/{siteId}/lists/{listId}/permissions` and the listItem/driveItem equivalents.

Two consequences to raise before anyone designs around them:

- Granting below the site level **breaks SharePoint permission inheritance** on that resource, which counts against
  the tenant's unique-security-scope limits per list or library. A site-level grant does not break inheritance,
  because the site is the root of inheritance.
- Scope levels do not ladder upward. `Files.SelectedOperations.Selected` can never reach a list or site; `Sites.*`
  can be used to grant file-level permissions but not the reverse.

### Delegated `Sites.Selected`

In delegated mode, effective access is the **intersection** of the user's SharePoint permissions and the app's granted
role. A user who cannot see the site sees nothing through your app, no matter what the grant says — which is usually
the behavior a customer wants, and occasionally the reason a connector "randomly" returns empty lists.

## 3. Addressing: site vs drive vs list

Graph exposes SharePoint as `site` → `list` → `listItem`, with a document library also surfacing as a `drive` of
`driveItem`s. Sites are **read-only** in Graph (you cannot create one); lists, list items and drive items are
read-write.

| Path | What it addresses |
| --- | --- |
| `/sites/root` | The tenant's default site |
| `/sites/{hostname}:/sites/{path}` | A site by URL — e.g. `/sites/contoso.sharepoint.com:/teams/hr` |
| `/sites/{hostname},{spsite-id},{spweb-id}` | A site by composite id — what `Sites.Selected` grants key on |
| `/sites/{site-id}/drive` and `/drives` | The default document library, and all of them |
| `/sites/{site-id}/lists/{list-id}/items` | List items |
| `/drives/{drive-id}/items/{item-id}` | A file or folder |

A site id is only unique *within* its site collection, which is why the id is composite. Resolve it once by path and
store it; do not try to construct it. For `Sites.Selected`, the **site id of each granted site** — in the composite
`{hostname},{siteCollectionId},{webId}` form — is part of what you hand off alongside the credentials.

## 4. Throttling — what the credentials have to survive

This is where SharePoint connectors fail in production rather than at registration.

- Throttled calls return **HTTP 429** or **503** with a **`Retry-After`** header. Honoring it is required — throttled
  requests still count against your quota, so retrying early makes things worse and can escalate to an outright block
  at the app or user level.
- Microsoft's throttling doc lists `RateLimit` headers in its summary but then states that SharePoint Online **does
  not return or support IETF `RateLimit` headers**. Do not build on them; honor `Retry-After`.
- Limits are measured in **resource units**, not requests: 1 per single-item read, delta-with-token or file download;
  2 per multi-item query or write; **5 per permission operation, including `$expand=permissions`**. Budget for that
  last one if you enumerate sharing — and note that stamping and auditing `Sites.Selected` grants is exactly that
  kind of operation.
- Per app, per tenant: roughly **1,250–6,250 resource units per minute** and **1.2M–6M per 24 hours**, scaling with
  the tenant's license count. Multitenant apps are metered **per tenant**, so one noisy customer does not throttle the
  others.
- **Delta with a token is the cheapest way to scan** content — Microsoft deliberately prices it at 1 unit.
- **Decorate your traffic.** Undecorated traffic is deprioritized. Send a `User-Agent` of the form
  `ISV|CompanyName|AppName/Version` (or `NONISV|…` for an in-house app).
- Do **not** register multiple app IDs for the same workload to dodge limits — they share the tenant's resources and
  Microsoft calls this out as a known anti-pattern.
- App-only search with `Sites.Read.All` or stronger is throttled at **25 requests/second**.

## 5. Verify the SharePoint-specific parts

Run the base skill's end-to-end check first (second tenant, refresh token, token claim inspection). Then:

1. For `Sites.Selected`: confirm access is **denied before** the site grant and **allowed after** it. If it works
   before the grant, your token is carrying a broader scope than you think — go back to §1.
2. Read one site (`GET /sites/{siteId}`), then one drive item under it. Both, not just the site.
3. If the design depends on a second site, confirm it too — grants do not generalize.

| Symptom | Cause |
| --- | --- |
| Auth succeeds, every SharePoint call is `403 accessDenied` | `Sites.Selected` consented but no per-site grant stamped — or the grant is on a different site (§2) |
| Works for the site you granted, 403 on a second site | Grants are per site; each one needs its own `POST /sites/{id}/permissions` (§2) |
| Delegated calls return fewer items than the admin expects | Delegated access is the intersection of user and app permissions (§2) |
| A `Sites.Selected` connector that used to work returns 403 on every site at once | The Entra consent was revoked, which kills all granted sites together (§2) |
| Sudden 429/503 under load | Throttling; honor `Retry-After`, switch scans to delta-with-token (§4) |
| Persistent 503 with no recovery | The app has been blocked, not throttled — Microsoft notifies the tenant in the Message Center (§4) |
| Instructions mention `appregnew.aspx` / `appinv.aspx` | Retired ACS path — dead since 2026-04-02 (Platform state) |
| A remote event receiver stops firing | Registered through ACS (dead) rather than an Entra app (works until 2027-07-01) (Platform state) |

## Hand off — the SharePoint additions

The base skill's hand-off list applies. Add to the closing summary: for `Sites.Selected`, **which sites were granted,
with which role, and who stamped them**; and the composite Graph **site id** of each, since an app-only connection is
anchored to it.

## Stop and ask

On top of the base skill's conditions, hand back to a human when:

- The choice between `Sites.Selected` and tenant-wide `Sites.*.All` has not been made — it is a product and security
  decision with an onboarding cost attached, not a config detail.
- The customer's admin cannot or will not stamp a per-site grant, or no one in the room holds SharePoint
  Administrator.
- Someone proposes migrating a live app to a new client ID — that re-authorizes every customer *and* re-stamps every
  site grant.
- A plan depends on Azure ACS, SharePoint Add-Ins, `appregnew.aspx`/`appinv.aspx`, or ACS-registered remote event
  receivers. Those are retired; the plan needs rewriting, not a workaround.
- Microsoft commercial-marketplace or Partner Center listing comes up: Microsoft states that Partner Center offers
  must be **published** before their client IDs are used in production, and listing involves compliance, legal and
  volume claims that are business decisions.
- Granting below the site level is proposed and nobody has weighed the permission-inheritance cost (§2).

## References

The base skill carries the shared Entra references. These are the SharePoint-specific ones; verified 2026-09-20,
every link returned HTTP 200.

- Selected permissions overview (`Sites.Selected` and friends) — https://learn.microsoft.com/en-us/graph/permissions-selected-overview
- Grant an app permission to a site — https://learn.microsoft.com/en-us/graph/api/site-post-permissions
- List site permissions — https://learn.microsoft.com/en-us/graph/api/site-list-permissions
- Delete a site permission — https://learn.microsoft.com/en-us/graph/api/site-delete-permission
- Working with SharePoint sites in Microsoft Graph (site/list/drive addressing) — https://learn.microsoft.com/en-us/graph/api/resources/sharepoint
- Get a site — https://learn.microsoft.com/en-us/graph/api/site-get
- Avoid getting throttled or blocked in SharePoint Online — https://learn.microsoft.com/en-us/sharepoint/dev/general-development/how-to-avoid-getting-throttled-or-blocked-in-sharepoint-online
- Microsoft Graph throttling guidance — https://learn.microsoft.com/en-us/graph/throttling
- Azure ACS retirement in Microsoft 365 — https://learn.microsoft.com/en-us/sharepoint/dev/sp-add-ins/retirement-announcement-for-azure-acs
- SharePoint Add-In retirement in Microsoft 365 — https://learn.microsoft.com/en-us/sharepoint/dev/sp-add-ins/retirement-announcement-for-add-ins
- Add-In and ACS retirement FAQ — https://learn.microsoft.com/en-us/sharepoint/dev/sp-add-ins/add-ins-and-azure-acs-retirements-faq
- Microsoft Graph permissions reference (`Sites.*`) — https://learn.microsoft.com/en-us/graph/permissions-reference
