---
title: "OAuth scopes, and the least-privilege set"
url: "https://trailassembly.com/oauth-scopes-least-privilege/"
topic: "Data and Integration"
published: "2026-09-16T05:08:29+00:00"
updated: "2026-10-06T13:56:18+00:00"
---

# OAuth scopes, and the least-privilege set

**The short answer:** most integrations need `api` and nothing else. Add `refresh_token` only if your flow issues one, and identity scopes only if you consume the identity endpoint. `full` is the default on a large number of production apps because it makes the first test pass, and nobody goes back afterwards, because narrowing it later means every client re-authorises.

## What a scope is and is not

A scope is a ceiling on what a token may be used for. It is not a grant. A token with `api` issued to a System Administrator can read and write every record in the org, because the scope restricts the surface, not the identity behind it. If you want less access, change the [running user](https://trailassembly.com/dashboard-running-user/)‘s profile and [permission sets](https://trailassembly.com/permission-sets-after-cancellation/). The scope list only decides which doors the token fits.

Two rules follow. Scopes limit blast radius across product surfaces, not data. And a minimal-looking scope list on an app running as an admin is security theatre.

## What each of the common scopes actually grants

- `api`: the data APIs. REST, SOAP, Bulk, Tooling and Metadata all sit behind this one scope. It is what an integration needs, and it is more than people assume.

- `refresh_token` (also written `offline_access`) — permission to issue a refresh token. Not included in `full`, which surprises people. [JWT](https://trailassembly.com/salesforce-oauth-flows-compared/) bearer and client credentials flows do not use one at all, so requesting it there is noise.

- `web`: lets an access token open the Salesforce user interface through the frontdoor URL. A token with this scope is close to a browser session. Do not give it to a backend job.

- `visualforce`: access to Visualforce pages. Same category of risk as `web`.

- `openid`: returns an ID token and allows the identity endpoint. The correct scope for “log in with Salesforce” and the wrong one for data access.

- `profile`, `email`, `address`, `phone`: individual claim groups on the identity response. Ask for the ones you read.

- `chatter_api`: Connect REST API only. Useful precisely because it is narrow: a Chatter bot needs this and not `api`.

- `full`: everything the user can do, across every API surface, including scopes Salesforce adds in future releases. That last part is the real objection. You are consenting to a list that changes without you.

Product-specific scopes also exist for CRM Analytics, [Data Cloud](https://trailassembly.com/grounding-a-prompt-with-data-cloud/), Pardot and the [Agentforce](https://trailassembly.com/agentforce-permissions/) model APIs. Request one only when you call that product.

## Least-privilege sets worth copying

**Server-to-server data integration (JWT or client credentials):** `api`. That is the whole list.

**User-facing app that reads Salesforce data (web server flow):** `api refresh_token`. Add `openid` if you need to know who logged in.

**Pure single sign-on, no data:** `openid profile email`. No `api`. If the app never queries an object, do not let its tokens query objects.

**Mobile app embedding Salesforce UI:** `api web refresh_token`, and accept that this is a broad token deserving a short refresh token policy.

## Checking what a token can really do

Do not trust the app configuration screen. Ask the token. The introspection endpoint returns the scopes a live token was issued with:

```apex
curl https://MyDomain.my.salesforce.com/services/oauth2/introspect \
  -d token=$ACCESS_TOKEN \
  -d client_id=$CONSUMER_KEY \
  -d client_secret=$CONSUMER_SECRET \
  -d token_type_hint=access_token
```

The response includes `scope` and `active`. That is the ground truth, and it sometimes differs from the configuration, because a token carries the scopes it was issued with. Narrowing the app’s list does not shrink tokens already out there.

## Narrowing an app that is already live

Sequence it, because the failure is an outage.

1. Work out what the app actually calls. Login History filtered to that app shows the API type in use, which tells you whether anything touches the UI surfaces.

2. Create a second app with the narrow scope list and point one non-critical client at it.

3. Move the remaining clients, then revoke the old app’s tokens through Connected Apps OAuth Usage.

Editing the live app instead works, and fails every client at an unpredictable moment when its current token expires. The parallel app costs an hour and removes the guesswork.

## Frequently asked

**Why does my token request fail with invalid_scope?** The requested scope must be a subset of the scopes selected on the app. Requesting something the app does not offer is rejected outright rather than trimmed.

**Does `full` include `refresh_token`?** No. That combination is why so many apps list both, and it is the one legitimate reason to see `full` in a config.

**Is Metadata API a separate scope?** No, `api` covers it. The gate is the user’s Modify Metadata or Modify All Data permission, not the scope.

---

Source: https://trailassembly.com/oauth-scopes-least-privilege/
Trail Assembly — independent Salesforce write-ups.
