skip to content

Resource-Server Verification

Every service that receives a token repeats the same checks, and the interesting part is the caching and the allowlist around them. Interviewers ask what happens when the issuer is unreachable.

on this pageshow

questions

4

A verified access token's `scope` claim names three permissions — may the caller now perform all three on your service?

level: juniorimportance: must knowfreq 58%

answer

  1. two checks, not one
  2. a ceiling, not a grant
  3. the issuer never saw your records
  4. insufficient_scope is 403, invalid_token is 401
  5. unknown scope maps to nothing

basics

~20 s

No. A scope records the ceiling the issuer put on what the caller may ask for; it grants nothing on your service. Your own authorization rules still decide, and both the scope check and your rule must pass.

solid answer

~50 s

A `scope` value is written by the issuer at mint time and says what the holder was permitted to **ask** for on the resource owner's behalf. It is a ceiling on delegation, not a permission your service handed out, and the issuer has no idea which records this subject owns. So a request that survives verification still faces two checks: does the token carry the scope this endpoint requires, and does your own rule allow this subject to touch this record. A token missing the required scope is a `403` with `insufficient_scope` — you know who the caller is, the credential just does not cover the call. A token that is expired or unverifiable is a `401` with `invalid_token`. Map the verified claims into a principal your code owns, and map a scope string you do not recognise to no permission rather than to everything.

code

http · 8 lines
http
GET /gates/7/admissions HTTP/1.1
Host: gates.venue.example
Authorization: Bearer <access token>

HTTP/1.1 403 Forbidden
WWW-Authenticate: Bearer realm="gates", error="insufficient_scope", scope="gate.enter"
Cache-Control: no-store
Content-Length: 0

go deeper

for a junior

Recall that a scope limits what may be asked for and never grants anything by itself. Two checks run: the credential's scope, then the service's own rule about this subject and this record.

for a middle

Explain the mapping step — verified claims become a principal your code defines, unknown values become no permission, and a missing scope becomes the empty set rather than a wildcard.

for a senior

Show the failure you have actually seen: a service that trusted scope alone and let one over-broad credential read every record of a shape, or one whose 403 storm was really a 401 problem.

for a principal

Argue where authorization facts should live at all. Anything the issuer freezes into a token is a snapshot with a staleness budget; a coarse scope plus a live local rule is the combination that survives between mints.

## Two questions, asked in order When a request reaches a service carrying an access token, two separate questions must be answered, and only the first is about the token. 1. **Is this credential genuine and still usable?** The signature, the expiry, the intended audience. 2. **May this subject do this thing to this record?** That answer lives in your own data, not in the token. The `scope` claim sits between the two and is routinely mistaken for the second. It is not the second. It belongs to the first — it describes the credential, not the subject's standing in your system. ## What a scope value actually asserts `scope` is written by the issuer when the token is minted, and it records what the holder was permitted to **ask** for on the resource owner's behalf. It is a ceiling on delegation. Take a venue whose turnstile controllers accept tokens from a ticketing issuer. A token carrying `gate.enter` says: *whoever holds this may ask the gate service to admit someone*. It does not say that this holder is admitted, through gate 7, at 19:40, for seat 12B. Those are facts only the venue's own records contain. Two consequences follow: - **The issuer cannot be where your permissions live.** It would have to know your data model and re-mint a token every time a record changed. - **A scope check that passes is never the end of the check.** It narrows; it does not decide. ## Both must pass, and the order is cheap-first Verify the credential, map its claims, check the scope the endpoint requires, then run your own rule against the subject and the record. Two failure modes are common and both are one-sided: - **Skipping your own rule** because the scope looked right. The token then acts as a master key across every record of that shape — exactly what an over-broad or leaked token is for. - **Skipping the scope check** because your own rule covers it. You throw away the one control that limits what a token may be used for, so a credential minted for a narrow purpose can drive every call the subject is entitled to make. ## Turning verified claims into a principal your code owns The output of verification should be an object your codebase defines — the subject identifier, the issuer it came from, the permission set your mapping produced, and the credential's own expiry — not the raw claim map handed onward. - A raw claim map invites ad-hoc lookups scattered through business code, each one a place a missing claim is read as an empty string. - The mapping is the **single place** where a value you do not recognise is turned into nothing. - It is the boundary that stops a claim-shaped header injected by an intermediary from being read as a verified claim: the principal can only be built from a token this service verified itself. - It is testable. A table of claim inputs to expected permission sets is a cheap, high-value test. That mapping is a security decision, not plumbing. A prefix typo that silently produces an empty permission set fails safe and shows up as a support ticket. A mapping that treats an unknown value as a wildcard fails open and shows up in an incident report. ## Missing, unknown and unexpected values fail closed - **No `scope` claim at all** — the empty permission set, never "all". - **A scope string with no mapping** — no permission, and record the unseen value once with the issuer that sent it so a newly published scope arrives as a deployment task rather than a silent grant. Do not reject the whole request: issuers add scopes, and every extra one would become an outage. - **No usable subject identifier** — reject the credential; you cannot build a principal from it. - **A claim you did not expect** — ignore it. Nothing your service did not ask for should widen a request. ## Which failure gets which status | Situation | What the verifier concluded | Status | Error code | |---|---|---|---| | No `Authorization` header | Nobody has identified themselves | `401` | none — a bare `WWW-Authenticate: Bearer` challenge | | Signature, expiry or audience fails | The credential cannot be accepted | `401` | `invalid_token` | | Token fine, required scope absent | Caller known, credential does not cover this call | `403` | `insufficient_scope` | | Token fine, scope present, your rule says no | Caller known, your records say no | `403` | none — this is not a token problem | The distinction is worth holding: `401` means *I do not know who you are*, and clients respond by getting a fresh credential. `403` means *I know, and no*, and a client that retries with a new token learns nothing new. ## What this does not decide How permissions are modelled, and where in your stack the rule physically runs, are separate subjects with their own trade-offs. This is only about what the token contributed to the decision: a ceiling, checked early, that never replaces the rule underneath it.

  • Your service meets a scope string it has never been configured to understand. What should the mapping do with it?
    Map it to no permission and let the request continue to be judged on the scopes you do understand. Record the unrecognised value once, together with the issuer that sent it, so a newly published scope surfaces as a configuration task. Rejecting the request outright would break every caller the day the issuer adds a scope; treating it as a wildcard would hand out permissions nobody reviewed.
  • The token is valid and carries the required scope, but the record belongs to a different subject. Which status do you return?
    `403`, with no OAuth error code — `insufficient_scope` would be a lie, because the credential's scope was fine. This is your own rule refusing. Whether to go further and hide the record's existence behind a 404 is a separate design decision about what a denial reveals, and it is made per resource, not globally.
  • Why not let the issuer mint tokens carrying the caller's exact permissions, so your service only has to read them?
    Because the issuer would have to know your data model, and every permission change would need a freshly minted token before it took effect. Anything frozen into a token at mint is a snapshot with a staleness budget attached. A coarse scope plus a live lookup in your own records is the combination that stays correct between mints.

A note from a parent saying the child may buy bread and milk. The shop still refuses to sell alcohol, and still refuses if the account is empty. The note caps what may be asked for; the shop's own rules decide what is actually sold.

saying these in an interview costs you the question

  • Treats a scope string as proof the caller may act
  • Believes an expired access token should produce 403 rather than 401
  • Hands the raw claim map to business code as the permission set
  • Maps an unrecognised scope string to full access rather than to none
  • Thinks a valid signature has finished the authorization work
  • Assumes the issuer knows which records this subject owns
open as a page

Sixty turnstile verifiers meet access tokens carrying a `kid` their cached key set lacks, and the issuer answers slowly — what should each verifier do?

level: seniorimportance: must knowfreq 48%

basics

~20 s

Reject those tokens now — an unobtainable key is not evidence of validity — and schedule one bounded refetch instead of one per request. Tokens whose key identifier is already held keep verifying normally, so the outage stays confined to the unknown ones.

open as a page

Why should the signature algorithms your token verifier accepts be deployment configuration rather than a library default?

level: middleimportance: should knowfreq 41%

basics

~20 s

Because the verifier, not the sender, must decide how a token is checked, and that set has to change on the issuer's rotation schedule across a fleet deployed in waves. An inherited default is invisible in review and can drift between environments.

open as a page

Your verifier must ask the issuer whether each access token is still valid — what do you cache, and for how long?

level: middleimportance: should knowfreq 37%

basics

~20 s

Cache the decision you derived, keyed on a keyed digest of the token rather than the token itself, for the shorter of a configured TTL and the credential's own remaining life. The TTL you pick is exactly the revocation lag you are accepting.

open as a page