skip to content

An OAuth 2.0 token response carries a `scope` member narrower than the client asked for — why, and what must the client do?

level: middleimportance: must knowfreq 57%

answer

  1. asked for is not granted
  2. policy, or the owner said no
  3. the server may ignore part of it
  4. differs means the response must say so
  5. read scope from the token response

basics

~20 s

An authorization server may partially or fully ignore a requested scope, on its own policy or the resource owner's instructions, and must then report what was granted. The client reads that member and adapts; this is specified behaviour, not a bug.

solid answer

~50 s

RFC 6749 §3.3 says the authorization server **MAY** fully or partially ignore the scope a client requested, based on its own policy or on the resource owner's instructions — a beekeeper can approve the hive notes and refuse the treatment write. When the issued access token's scope differs from the requested one, the server **MUST** include the `scope` response parameter stating what was actually granted; when the two match, the member may be absent. A client that assumes it received what it asked for is therefore broken by specification rather than by a server quirk: read `scope` from the token response, keep it with the token, and turn off the feature you did not get. A scope string the server does not define at all is a different outcome — that is the error code `invalid_scope`, with no token issued.

code

http · 10 lines
http
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store

{
  "access_token": "2YotnFZFEjr1zCsicMWpAA",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "hives:read"
}

go deeper

for a junior

Remember that a token response can hand back fewer scope strings than the request asked for, and that the response is where you learn what the token can actually do.

for a middle

Explain that the server may fully or partially ignore the requested scope, that it must report the granted set when it differs, and what your client code does with that value.

for a senior

Bring the failure you have seen: a feature that worked for everyone who clicked approve-all and broke for the first user who unticked a line, because nothing read the granted scope back.

for a principal

Decide how far a product degrades on a partial grant versus refusing to proceed, and make sure support can see that a user's grant is narrower than the default before they escalate it.

## Requesting is not receiving RFC 6749 §3.3 hands the authorization server two powers in one sentence: it **MAY fully or partially ignore** the scope requested by the client, based on **its own policy** or on the **resource owner's instructions**. Both sources matter and they fail differently. - *Policy*: the beekeepers' association may have decided that a visiting professional never gets write access to treatment records, whatever any client asks for. - *The owner*: the beekeeper at the consent screen unticks the treatment line and approves the notes. Either way the client asked for `hives:read hive-notes:read treatments:write` and the grant that exists afterwards is smaller. This is neither an edge case nor a defect in the deployment. ## What the response must say The counterpart rule is what makes the first one survivable: when the issued access token's scope is **different** from the one requested, the server **MUST include the `scope` response parameter** naming what was granted. When the two match, the member may be left out — plenty of servers include it anyway, which is harmless but is not something to depend on. The client's rule is short: **the token response, not the token request, is the source of truth for what this token can do.** ## Downgrade or error: two different outcomes | what the client asked for | outcome | what comes back | |---|---|---| | strings the server defines, all approved | success | an access token; `scope` may be absent | | strings the server defines, some refused | success, narrowed | an access token **plus** `scope` naming the granted subset | | a string the server does not define, or a malformed value | error | `invalid_scope`, no token issued | | more than the resource owner actually granted | error | `invalid_scope` from the token endpoint | The distinction is worth stating aloud in an interview, because candidates routinely expect an error where the specification produces a quiet, well-signalled narrowing. ## What a client does with a narrowed grant 1. **Record the granted scope with the token**, as part of the same record — the two belong together, and a later token may carry a different set. 2. **Decide per feature.** Some features degrade honestly (read-only view); some must be hidden, because offering a button that cannot work is worse than not offering it. 3. **If the missing access is the reason the user is here**, start a fresh authorization request naming the wider scope and let the resource owner decide again. 4. **Do not loop.** A refusal that immediately re-prompts is how a user gets trapped; treat a refusal as an answer and remember it for that session. ## The failure this rule prevents, and the one it exposes The classic production story: an integration works perfectly for the team that built it, because everyone clicked approve on everything. The first real user unticks one line. The client, which derived its capabilities from its own request, calls an API it no longer has access to; the call is refused, and the person sees an unexplained error with no route out. Every piece of information needed to avoid that was in the token response. ## Where this is easy to get wrong - **Deriving the granted scope from the request.** The request is what you wanted; the response is what you have. - **Caching the first response's scope for every later token.** Grants change when a resource owner revisits a consent screen. - **Reading an absent `scope` member as 'nothing granted'.** Absent means 'as requested', which is the opposite reading. - **Expecting `invalid_scope` for a narrowing.** A narrowing is a success with a smaller grant; `invalid_scope` means no token at all. - **Comparing string sets without respecting case.** A granted set echoed back is compared character for character, not loosely. ## What an interviewer is listening for The sentence that separates a candidate who has integrated something from one who has read a diagram is: *the server may grant less than I asked for, it has to tell me when it does, and my client reads that back.* Everything else — the error case, the degradation strategy, the incremental re-ask — hangs off that.

  • If the granted scope matches the request exactly, must the response still carry `scope`?
    No. The `scope` response parameter is required only when the issued scope differs from the requested one. A server may include it regardless, and many do, so a client should treat an absent member as 'exactly what I asked for' and a present member as authoritative, rather than depending on one shape.
  • How is `invalid_scope` different from a downgrade?
    A downgrade is a success: a token is issued for a narrower set and the response says so. `invalid_scope` is an error — the requested scope was invalid, unknown, malformed, or exceeded what the resource owner granted — and no token is issued. From the token endpoint it arrives as HTTP 400 with a JSON body; from the authorization endpoint it arrives by redirect back to the client's registered redirection endpoint.
  • Where should the granted scope be kept on the client side?
    With the token record itself, so the two travel and expire together. Deriving it from the original request, from configuration, or from the user's account settings drifts the moment the resource owner changes their mind at a later consent screen, and the drift is silent until an API call is refused.

saying these in an interview costs you the question

  • Assumes the granted scope always equals the requested scope
  • Treats a narrowed grant as a broken authorization server
  • Reads granted access from its own request, not the response
  • Expects invalid_scope whenever the server grants less
  • Counts on the scope member being present even when nothing changed