Omri Gazitt

Scopes are still not permissions. But the token isn't going away.

· 11 min read

A few years ago I wrote a post called OAuth2 scopes are not permissions. The argument was that developers keep reaching for scopes as an authorization mechanism, and that this goes badly for four reasons: scopes explode combinatorially once you want resource-level granularity, a token minted at login is a stale snapshot of permissions that may have changed since, scopes carry no resource context so they cannot express “read this document,” and shortening token lifetimes to fix the staleness throws away the reason you wanted a JWT in the first place.

I still believe every word of that. The diagnosis holds up.

The prescription has not held up as well. What I recommended was, in effect, to get authorization out of the token: make the decision at request time, against current state, with the actual resource in hand. That is the right architecture for the decisions that need it. It is also an argument I have now watched lose, repeatedly, for a reason that has nothing to do with whether it is correct.

The whole world thinks in tokens.

Not as a considered position - as a default. Every API gateway inspects one. Every framework’s middleware parses one. Every tutorial ends with one. Every developer’s mental model of “is this caller allowed to do this” starts with “what’s in the token.” Telling that ecosystem to stop putting authorization in tokens is telling it to stop breathing through its nose. You can be right and be ignored at the same time.

Two ships passing in the night

Here is what I think actually happened over the last several years.

Two communities worked on adjacent halves of the same problem and barely spoke to each other.

On one side, the OAuth and OpenID crowd kept making token issuance richer and more expressive. Rich Authorization Requests gave us structured, per-resource grants instead of opaque scope strings. Token Exchange gave us a general delegation primitive. Identity Chaining and the Identity Assertion Authorization Grant extended it across trust domains. Transaction Tokens carried the original caller’s context down a call chain. Every one of these specs is careful, well-reviewed, and genuinely useful - and every one of them, at the exact moment where it has to decide whether a token should be issued and what should be in it, says some variation of that’s local policy, out of scope.

On the other side, the fine-grained authorization crowd - the policy-language people, the Zanzibar implementations, eventually the AuthZEN working group - built increasingly good machinery for making exactly that kind of decision. And we pointed essentially all of it at the resource server. Our reference architecture was an API gateway or an application middleware calling a PDP at request time. Our examples were always “may this request proceed.”

I can say this from personal experience: in my five years running Aserto and working on Topaz, every time a team asked us about wrapping a PDP decision in a token, we always thought “ugh, they’re thinking about it wrong”.

To summarize, the OAuth community owned the decision point and declared the process of how to make that decision out of scope. The FGA / AuthZEN community specified how to formulate the question “can this subject perform this action on this resource?”, but never seriously looked at the AS as a critical decision point.

Two ships in the night.

The turn: two enforcement points, not one

The first time I drew this differently was in the AuthZEN interop architecture. We laid out three layers of enforcement: fine-grained in the application, medium-grained at the API gateway (“may this subject call this endpoint”), and coarse-grained during authentication, with the IdP acting as a policy enforcement point.

tokenrequest

Authorization Server
coarse-grained
what authority goes in the token?

API Gateway
function-level
may this caller invoke this endpoint?

Resource Server
fine-grained
may this subject act on this
resource, right now?

one policy
three different questions

The important thing about that picture is that it is defense in depth, not a ranking. The layers are not competing for the job. They answer different questions at different resolutions, and the outer ones do not substitute for the inner ones.

That is the part I had wrong. My old post read as though authorization had one correct home and tokens were a mistake people made on the way to finding it. But the issuance decision is not a degraded version of the request-time decision. It is a different decision, with its own question - what authority should this credential carry - and it gets made in every deployment on earth, whether or not anyone specifies it.

Then, at the Gartner IAM interop in December 2025, we built the outer layer.

The scenario is small and, I think, more important than it looked at the time. An IdP is about to mint a token. It knows who the user is. It does not know what to put in the token. So instead of reading permissions out of a config file, it asks a PDP - using AuthZEN’s resource search endpoint rather than an evaluation:

{
  "subject": { "type": "user", "id": "<user_id>" },
  "action":  { "name": "delete" },
  "resource":{ "type": "record" }
}

and gets back the set of things that subject can actually reach:

{
  "results": [
    { "type": "record", "id": "105" },
    { "type": "record", "id": "111" },
    { "type": "record", "id": "117" }
  ]
}

which goes straight into the token as a claim. Six users, twenty records, a working demo, multiple independent implementations interoperating.

I want to be careful about what that demonstrates. It doesn’t mean the resource server can stop asking. Those record IDs are a snapshot with a lifetime; if someone’s access is revoked a minute later, the token does not know. What it means is narrower and still worth having: the authorization data in the token is no longer a hand-maintained config table duplicating logic that lives somewhere else. It is a projection of live policy state, computed at issuance from the same engine the resource server consults. Same policy, two enforcement points, one source of truth.

Which layer carries the weight

Once you stop treating the two as rivals, the interesting question is how the work divides - and that turns out to be genuinely scenario-dependent.

Plenty of applications have three roles: viewer, editor, admin. They derive from group membership, they change when someone changes jobs, and they are the same for every resource in the app. That is an issuance-time decision, and putting it in the token is correct. Making a policy call on every request to re-derive a fact that changes twice a year is the definition of over-engineering.

Google Docs is the other pole. The resource is one document, the answer changes the instant the owner shares or revokes access, and no credential minted ten minutes ago (or even ten seconds ago) can be trusted to know. That decision has to happen at the resource server, in real time, against current state. This is the case I was arguing about, and I still think everything I said about it holds.

But look at what Google actually does, because it is the common case and it is neither pole. For an application to touch your Drive at all, it needs a scope - https://www.googleapis.com/auth/drive.readonly - which Google documents as “View and download all your Drive files.” That is an issuance-time decision about what this application may do on your behalf, and it is deliberately coarse: it says all your files, because a scope string has no way of saying which ones. Then, on every request, the per-document check happens inside Drive against the sharing graph.

Drive APIGoogle'sauthorizationserverAppDrive APIGoogle'sauthorizationserverApponce, at issuanceon every requestmay this app read this user's Drive at all?scope .../auth/drive.readonly"View and download all your Drive files"files.get(doc_42)is doc_42 shared withthis user, right now?the one document, or nothing

Neither layer is doing the other’s job. The scope bounds what the application can ever ask for; the sharing graph decides what it actually gets. Remove the scope and a calendar integration can read your tax returns. Remove the per-document check and the scope means exactly what it says - “view ALL docs”.

What I’ve learned since 2021

I haven’t changed my mind about the diagnosis in the original post. I have changed my mind about how many places authorization lives, and therefore about where the fight is worth having.

Authorization happens at two surfaces. It always did. The request-time one has a specification, a standard interface, and a decade of good engineering behind it. The issuance-time one has a handful of mutually incompatible vendor hooks and, in the standards, the phrase out of scope. I spent my energy arguing that the second surface isn’t “real” authorization - which is both wrong and, since it exists regardless, useless.

Arguing that authorization does not belong in tokens is arguing with the tide. The version of the argument that is both true and winnable is narrower:

If a token is going to carry authorization - and it is - then it had better be minted by something that consulted a policy engine, and the policy engine had better be able to constrain what comes out.

That is a much smaller ask. It does not require anyone to change how they enforce at the resource server, what libraries they use, or how their gateway works. It changes one thing: what happens inside the authorization server in the milliseconds before it signs.

And the complaints from the original post turn into tractable engineering problems once a PDP is in that path - not because they evaporate, but because something is finally positioned to act on them. Stale permissions become a lifetime the PDP can shorten when it knows the policy is volatile. Overbroad grants become a scope the PDP can narrow. Missing resource context becomes something the PDP can supply. None of these were ever token problems. They were issuance problems, and they went unsolved because nothing was in a position to solve them.

Why now

Three things changed roughly at once.

AuthZEN 1.0 went Final in January 2026, with the search endpoints in it. The mechanism the interop scenario depends on is no longer a draft.

The OAuth issuance stack converged. Token Exchange is long since an RFC; identity chaining, ID-JAG and Transaction Tokens are all in the working group and all define the same decision moment. There is now a recognizable shape to generalize over, which there wasn’t three years ago.

Agents made delegation urgent. When the thing holding the token is an autonomous process acting for a human, “which subset of this person’s authority does this agent get, for how long, against which audience” stops being a thought experiment. It is the central question, and it is asked at issuance.

There is a fourth reason, which is that others are arriving at the same intersection independently. Karl McGuinness’s Access Request and Approval Profile puts an AuthZEN PDP behind an authorization server for the case where access is denied but can be requested - and its OAuth completion mode lands on precisely the moment this series is about, from an entirely different starting point. When two people show up at the same corner from different directions, the corner is probably real.

So: the interop demo works, and there is no specification of it. Nothing says how an authorization server should form that evaluation request, what it may do with the response, or - the part I think matters most - what an implementation is forbidden from ignoring. A demo is not an interoperability contract. The time to write the contract is now.

The series

This is the first of five posts working through what that contract looks like.

  1. This one. Why authorization happens at two surfaces, and why only one of them has an interface.
  2. The decision every OAuth spec declines to make. Four specifications that name the issuance decision and step around it, and the five proprietary vendor hooks that filled the gap - none of which can safely say no.
  3. Two questions, not one. Issuance authority versus access authority, why may_act is the tell, and what happens to a naive mapping when the request has no scopes at all.
  4. The answer is not a boolean. How a decision narrows a token, and why “may an implementation ignore this field?” has a mechanical answer rather than a judgment call.
  5. Claims enrichment is a search. Formalizing the December 2025 interop scenario: AuthZEN search endpoints populating the groups, roles, and entitlements claims of RFC 9068.

The framework itself is written up as an Internet-Draft, AuthZEN Profile for OAuth 2.0 Token Issuance. The source, and somewhere to file disagreements, is on GitHub.