privileged.id

OAuth 2.0

Scopes and consent

Scopes decide how much a client can do; audiences decide where. Consent is where users get tricked. How to design all three.

Reviewed 7 min read 7 primary sources

A scope is how an OAuth client says what it wants to do, and how an authorization server records what it was allowed to do. Consent is the moment a person agrees. Together they decide how much damage a compromised client, a leaked token or a deceived user can cause, which makes scope design an access-control decision, not an API naming exercise.

Scope syntax

RFC 6749 §3.3 defines the format: a space-delimited list of case-sensitive strings whose meaning is defined by the authorization server. Order does not matter, and each string adds an access range.

scope       = scope-token *( SP scope-token )
scope-token = 1*( %x21 / %x23-5B / %x5D-7E )

In other words, any printable ASCII except space, double quote and backslash. So calendar.read, https://api.example.com/files.write and openid are all valid scope tokens. The rules that matter in practice:

  • The authorization server MAY fully or partially ignore the requested scope, based on policy or the user's choices. If the issued scope differs from what was requested, it MUST return the scope parameter so the client knows what it actually got.
  • If the client omits scope, the server MUST either use a documented default or fail the request with invalid_scope.
  • The client SHOULD request the minimal scope necessary, and the server SHOULD take the client's identity into account when deciding what to grant (§10.3).
  • On a refresh, the requested scope MUST NOT include anything not originally granted (§6). Refresh can narrow access, never widen it.

Because the client may receive less than it asked for, it must read the scope in the token response and degrade gracefully, rather than assume. An API that needs more answers 403 with error="insufficient_scope" and may name the needed scope in WWW-Authenticate (RFC 6750 §3.1).

Designing scopes for least privilege

RFC 9700 §2.3 makes privilege restriction a security requirement, not a style preference: the privileges associated with an access token SHOULD be restricted to the minimum required for the use case, and tokens SHOULD be restricted to certain resources and actions, with the resource server refusing requests outside them. This is least privilege applied to delegated access.

Requested scope versus granted scope Requested openid email calendar.read calendar.write resource= cal.example.com Policy + consent client allowed scopes? user approves which? least privilege audience = one API Granted token scope: openid email calendar.read aud: cal.example.com
The client asks; policy and the user decide; the token records only what was granted, for one audience.
PatternBetterWhy
calendar (everything)calendar.read, calendar.writeRead access is enough for most integrations; write is a separate decision.
adminSpecific admin actions, e.g. users.disableA catch-all scope makes every compromised client a full admin.
Same scopes for every clientPer-client allow-list of scopesA client should not be able to ask for what it was never meant to have.
One token for every APIOne audience per tokenA token leaked by one API cannot be replayed at another.
Scopes that encode user rolesScopes for client permissions; roles checked by the APIA scope says what the client may do on the user's behalf. Whether this user may do it is still the API's decision.

The last row is the most commonly misunderstood. A scope limits a client; it does not grant the user anything. A token with users.disable issued to a helpdesk app for a user who is not an administrator must still fail at the API. The effective permission is the intersection of what the user may do and what the client was delegated.

Audience restriction and resource indicators

Scopes say what; the audience says where. RFC 9700 §2.3 says access tokens SHOULD be audience-restricted to a specific resource server or a small set of them, that every resource server must check, on every request, that the token was meant for it, and that if not, the resource server MUST refuse. Audience restriction is what stops a malicious or compromised API from replaying the tokens it receives at other APIs (RFC 9700 §4.9).

The client can name its target with the resource parameter of RFC 8707, an absolute URI with no fragment. The authorization server SHOULD then audience-restrict the token to that resource, and rejects unknown or unacceptable resources with invalid_target:

GET /authorize?response_type=code&client_id=s6BhdRkqt3
  &state=tNwzQ87pC6llebpmac_IDeeq-mCR2wLDYljHUZUAWuI
  &redirect_uri=https%3A%2F%2Fclient.example.org%2Fcb
  &scope=calendar%20contacts
  &resource=https%3A%2F%2Fcal.example.com%2F
  &resource=https%3A%2F%2Fcontacts.example.com%2F
  &code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&code_challenge_method=S256 HTTP/1.1
Host: authorization-server.example.com

Adapted from RFC 8707 Figure 2 (PKCE added). The client then asks the token endpoint for one resource at a time, getting a token whose aud is just https://cal.example.com/, and another for the contacts API. In JWT access tokens (RFC 9068 §3) the aud claim SHOULD equal the requested resource; without one, the server MUST use a default audience, and every scope string in the token MUST have meaning for the resources in aud (§2.2.3).

Consent is the user's decision to delegate. A good consent screen shows who is asking, what they will be able to do, and for how long; a poor one shows an unfamiliar app name and a list of internal scope strings. Guidance in the standards:

  • The OAuth 2.1 Internet-Draft (§7.9) says authorization servers SHOULD present "clear and meaningful information" during the prompt: the client name, the developer or organization that registered it, and the specific scopes, and SHOULD let administrators and resource owners view the clients that have been granted access and revoke those grants.
  • RFC 6750 §3 notes that scope values are "intended for programmatic use and [are] not meant to be displayed to end-users". Map each scope to a plain-language description.
  • OpenID Connect requires explicit consent for refresh tokens that enable offline access: the provider MUST always obtain consent to returning a refresh token that enables offline access, and a previously saved consent "is not always sufficient" (OIDC Core §11).
  • Clickjacking defeats consent by tricking the user into clicking "Allow" in a hidden frame. RFC 9700 §4.16: authorization servers MUST prevent clickjacking and SHOULD use CSP frame-ancestors.

For first-party apps inside one organization, administrators often pre-approve consent so employees are not prompted. That is reasonable for clients the organization owns; it should not extend to arbitrary third-party apps.

Incremental authorization

Ask for scopes when the user reaches the feature that needs them, not all at sign-in. An app that requests calendar write access only when the user first creates an event gets a consent prompt that makes sense in context and holds fewer privileges for users who never use the feature. Standard OAuth allows this simply by sending a new authorization request with the additional scope; since a refresh can never widen scope, extra access always involves a fresh authorization. (The term "incremental authorization" is common industry usage, not a term defined in RFC 6749.)

An illicit consent grant, also called consent phishing, targets the authorization step instead of the password. The OAuth 2.1 draft describes it in §7.9: an attacker registers their own client with a legitimate authorization server, builds an authorization request asking for scopes over the victim's data (mail, files, contacts, often offline access), and sends the link in a phishing message. The victim lands on the genuine sign-in page, authenticates normally, possibly with a passkey, and sees a real consent screen from their real provider. If they click "Allow", the attacker's client receives tokens.

Critically, phishing-resistant authenticators do not protect against consent phishing.

The access persists after a password reset, because the attacker holds tokens, not the password, until the grant is revoked. A related trick uses a registered client's redirect URI as a phishing landing page: RFC 9700 §4.11.2 notes that a malicious client can make the authorization server redirect the user to the attacker's site even when they decline, and says the server SHOULD only automatically redirect to redirect URIs it trusts. Device code phishing is the same idea with a different grant.

Defenses, from the draft and common practice:

  • Control who can register clients and which scopes each may request (the OAuth 2.1 draft §7.9 says servers SHOULD consider such controls), and, where possible, verify developers before granting sensitive scopes (SHOULD).
  • Require administrator approval for third-party apps requesting high-impact scopes (mail, files, directory, offline access) instead of letting every user decide.
  • Show the publisher and a plain description of each permission on the consent screen.
  • Give users and administrators a list of granted apps with one-click revocation (RFC 7009), and review grants periodically as part of access reviews.
  • Alert on new grants of sensitive scopes to unfamiliar clients.

OpenID Connect standard scopes

OpenID Connect reuses the scope parameter to request identity claims. These values are defined in OIDC Core §5.4 and §11:

ScopeRequestsNotes
openidAn OpenID Connect authentication (an ID token)REQUIRED in every OIDC request; without it "the behavior is entirely unspecified".
profilename, family_name, given_name, middle_name, nickname, preferred_username, profile, picture, website, gender, birthdate, zoneinfo, locale, updated_atDefault profile claims.
emailemail, email_verifiedEmail is not a stable identifier (§5.7).
addressaddressStructured postal address.
phonephone_number, phone_number_verified
offline_accessA refresh token usable when the user is not presentIgnored unless the response type returns a code; explicit consent rules above.

Claims requested through these scopes are "Voluntary Claims": the provider may decline to return some of them. Request only the ones you will use; each is personal data you must then protect.

Checklist

  • Scopes split by action (read/write/admin) and by API; no catch-all scopes.
  • Per-client scope allow-lists at the authorization server.
  • Tokens audience-restricted, with resource (RFC 8707) or scope-to-audience mapping; APIs reject wrong aud.
  • APIs enforce both scope and the user's own permissions.
  • Clients read the granted scope and handle less than requested.
  • Scopes requested incrementally, when the feature is used.
  • Consent screens show publisher and plain-language permissions; framing blocked.
  • Admin approval for sensitive third-party scopes; grants reviewable and revocable.
  • OIDC clients request only the identity claims they use.

Sources

  1. RFC 6749: The OAuth 2.0 Authorization Framework, IETF. IETF Trust Legal Provisions (brief quotations with attribution).
  2. RFC 9700: Best Current Practice for OAuth 2.0 Security, IETF. IETF Trust Legal Provisions (brief quotations with attribution).
  3. RFC 8707: Resource Indicators for OAuth 2.0, IETF. IETF Trust Legal Provisions (brief quotations with attribution).
  4. RFC 9068: JSON Web Token (JWT) Profile for OAuth 2.0 Access Tokens, IETF. IETF Trust Legal Provisions (brief quotations with attribution).
  5. The OAuth 2.1 Authorization Framework, draft-ietf-oauth-v2-1-16 (Internet-Draft, work in progress), IETF. IETF Trust Legal Provisions (brief quotations with attribution).
  6. OpenID Connect Core 1.0 incorporating errata set 2, OpenID Foundation. OpenID Foundation specification (cited, brief quotations).
  7. RFC 6750: The OAuth 2.0 Authorization Framework: Bearer Token Usage, IETF. IETF Trust Legal Provisions (brief quotations with attribution).